资源描述
目录一、前言51.1 关于本书51.2 关于作者5二、性能优化的本质72.1 性能优化的根本目的是什么?72.2 出现性能问题的主要原因72.3 性能优化的核心环节82.4 寻找性能瓶颈8三、性能优化方法论的思想源泉103.1 核心思想103.1.1 开源和节流103.1.2 堆“硬件”、升“软件”103.1.3 权衡(trade-off)113.2 具体来源113.2.1 经典论断123.2.2 百花齐放13四、性能优化的核心思想144.1 增加资源144.1.1 增加机器144.1.2 升级配置154.2 减少耗时操作164.2.1 合并操作(化零为整)164.2.2 压缩214.2.3 复用224.2.4 减少 IO 操作234.2.5 减少上下文切换264.2.6 减少操作指令264.2.7 合理设置等待时间284.3 提高资源利用率284.3.1 空间换时间284.3.2 同步转异步384.3.3 串行转并行394.3.4 降低冲突的范围404.3.5 空间局部性424.4 其他424.4.1 提前处理424.4.2 实时转离线434.4.3 随机读写转顺序读写434.4.4 就近原则434.4.5 选择合适的数据结构和算法444.4.6 加限制条件(技术层面)444.4.7 CPU 密集型和 IO 密集型454.4.8 根据技术特点去优化454.4.9 全链路优化464.4.10 多种手段相结合464.5 产品层面474.5.1 加限制条件(产品层面)474.5.2 砍需求48五、注意事项495.1 避免过早优化495.2 考虑其他指标495.3 优化体验50六、实际案例516.1 案例描述516.2 设计分析516.3 案例总结54七、总结555一、前言一、前言1.1 关于本书本书主要分享自己在性能优化方面的一些思考。性能优化是 Java 程序员学习和工作进阶过程难以绕开的一个重要话题。很多人都想学好性能优化,希望能够在自己的工作中灵活运用,提升自己的技术水平,为用户提供良好的使用体验。然而,很多人在工作中设计技术方案或者编码时缺乏系统的、方法论级别的理论指导,导致需要考虑性能优化的场景时,缺乏优化思路。俗话说:“授人以鱼不如授人以渔”,本文不仅会讲性能优化有哪些具体的方法,还会讲解思想的来源。本书会先讲述性能优化方法论的主要思想源泉,性能优化的本质;然后分别讲述性能优化方法论的核心方法,以及性能优化的注意事项等内容。讲解过程中会结合常见的 Java 中间件进行一些举例说明;最后会结合具体的案例,帮助大家理解性能优化方法论如何落地。希望大家能够通过本书的学习,掌握性能优化的核心思路,帮助大家可以举一反三,可以从性能优化角度去学习 Java 中间件,去设计合理的性能优化技术方案。1.2 关于作者网名:明明如月,微信:mingmingruyuexz蚂蚁集团高级Java 工程师、阿里云开发者社区专家博主、CSDN 博客专家。一、前言二、性能优化的本质二、性能优化的本质2.1 性能优化的根本目的是什么?可能很多人没有认真思考过:“为什么我们需要进行性能优化?”这个问题。在我看来,性能优化是为了”解决良好的用户体验和资源的有限性之间的矛盾”。首先,我们性能优化一般都是追求更快的响应速度,通常最终目的是为了获得更好的用户体验。这里所说的资源是指广义上的资源,“资源的有限性”包括:资金成本的有限性,人力资源的有限性,服务器等硬件资源的有限性,时间的有限性等。如果资源是无限的,如开发周期很长,投入的人力很多,服务器资源充足,甚至服务器内存是无限大的,那么设计出来的产品可能就不需要花费太多精力去优化。2.2 出现性能问题的主要原因通常,产品发布早期由于用户量较少,不太容易出现性能问题或者通常不会太去关注性能问题;但随着业务的不断发展,性能问题就逐渐暴露出来。导致性能问题的原因有很多,常见的原因有:项目工期紧张,设计阶段技术方案考虑不充分;项目中使用了不合理的数据结构或算法;系统架构设计不合理;同步执行耗时任务;二、性能优化的本质二、性能优化的本质如使用 trace 命令,对某个耗时较长的接口进行分析:trace com.xxx.service.impl.AServiceImplrefresh,给出下面结果:根据上面的结果可以看出,com.yyy.service.impl.AServiceImpl:refreshSomeThings 耗时最长,可以继续再 trace 耗时最多的子函数,最终定位到最影响耗时的函数上。找到耗时最长的环节之后,根据具体代码推断出耗时长的原因,然后再针对性地进行优化。优化后可以通过性能测试、压力测试等手段验证性能优化的有效性。三、性能优化方法论的思想源泉三、性能优化方法论的思想源泉其实性能优化的主要方法也来源于此,性能优化的宏观思路就是“堆硬件,升软件”。这里的硬件指机器的数量和机器的配置等;软件包括优化算法、架构等。本质上和开源节流的思想是一致的。3.1.3 权衡(trade-off)大家找工作很难找到”钱多、事少、离家近”的工作。然而现实情况是,通常只能得其中一二。性能优化很多时候也是一种权衡,在性能优化的路上,通常要做:用户体验和成本的权衡,投入产出比的权衡。很多团队在产品研发初期人力资源有限,加上快速上线的压力,一般看重满足功能要求,只要不出现难以接受的性能问题即可。此外,项目也有优先级,可能另外一个项目优先级非常高,性能优化的事情可能就暂时延后。不管是通过”堆硬件”还是通过”升软件”的方式进行性能优化,都会面临着低延时和高成本的矛盾。然而一般来说,用户体验和成本成反比,架构师或者开发人员要做的是,根据产品发展阶段和资源的情况去考虑性能优化的问题。不管是从产品的发展阶段,还是团队内项目的优先级,还是技术角度上性能优化的投入和产出比来说,性能优化都不是简单地追求”最优化”,而是需要结合实际情况寻找优化的平衡点。3.2 具体来源三、性能优化方法论的思想源泉三、性能优化方法论的思想源泉3.2.2 百花齐放此外,专业基础是性能优化灵感的重要来源,如 CPU 的多级缓存思想、文件缓冲区的思想、进程调度算法等。不同的数据结构都有自己的特点,都是为了解决某一类问题,适合某些场景,有其优缺点。选择合适的数据结构也可以实现性能优化。不同的算法的时间和空间复杂度也各不相同,选择性能更优的算法也可以提高性能。很多架构就是为了性能优化而设计的,如读写分离、分库分表、分布式架构;很多中间件的设计中体现出诸多性能优化的思想,如 ES 的倒排索引、索引刷盘机制;JDK 源码和很多优秀的开源项目也包含着很多性能优化的典型实践,如内存缓存、分段加锁、写时复制等。四、性能优化的核心思想四、性能优化的核心思想通常很多站点都会在某些大型活动(如限时秒杀、微博热帖、高考查成绩、热门直播等)前或过程中,通过手动或者自动”扩容”增加更多机器的方式来支撑更多流量。4.1.2 升级配置增加配置,通常是更换性能更好的 CPU、增加内存、增加宽带、使用固态硬盘、增加磁盘空间等。下图为阿里云购买服务器页面的截图,其中的选项就是配置相关的关键指标。四、性能优化的核心思想四、性能优化的核心思想如下图所示,某个功能,需要在循环中请求十几次甚至几十次二方或者三方接口:假设每次耗时 100ms,请求 20 次,就是 2 秒钟。如下图所示,可以通过调用批量接口,发起一次网络请求获取十几条数据或者几十条数据:同样的功能,可能 100 ms 更多一点就搞定了。很多中间件在设计时,也会提供一些批量接口。如 hbase-client 中就提供了批量查询接口:org.apache.hadoop.hbase.client.Table#get(java.util.List)四、性能优化的核心思想四、性能优化的核心思想Java 中也提供了如 java.nio.ByteBuffer 和 java.io.BufferedOutputStream 等。下面是 BufferedOutputStream 的源码注释:从注释中也可以看出,应用可以使用该类将字节写入到这里,而不是每个字节都调用底层写入方法,本质上就是合并请求。四、性能优化的核心思想四、性能优化的核心思想4.2.2 压缩对要存储或传输的数据进行压缩,可以减少资源占用,提高传输速率。比如前端可以对 js 或者 css 文件进行压缩,来减少传输的数据量,加快资源加载速度。也可以在使用资源时,默认对资源自动压缩。如通过微信发送图片或者视频时,默认会自动压缩,必要时可以选择原图进行发送。查看时只加载预览图,在必要时可以选择查看原图或者选择清晰度更高的视频。如 QQ 空间相册、爱奇艺/B 站等视频的清晰度切换等。四、性能优化的核心思想四、性能优化的核心思想比如 Http 长连接就是在同一个连接中发起多次请求来提高性能的;数据库连接池,也是通过复用连接来提高性能的模式。4.2.4 减少 IO 操作下面只是给出几个实际中常见的具体案例,大家要根据实际情况进行举一反三。减少不必要的调用有时,前端由于架构设计不够合理等原因,会进行一些没有必要的同步调用,造成响应时间无辜被拖长。此时,需要去掉不必要的调用(尤其是同步调用)。做新的项目时,有时候需要协调多个下游,如果有些接口没有必要调用,就要减少调用。四、性能优化的核心思想四、性能优化的核心思想因此建议大家只打印必要的日志,对于大对象只打必要的字段。减少不必要的转换比如有时候需要将内存对象持久化到一些 KV 存储中,由于有些序列化方式需要实现序列化接口,而有些对象没有实现序列化接口从而不支持某种二进制序列化方式,有些人会选择先进行 JSON序列化成字符串然后再进行存储,这样做性能很差。如果 KV 存储要求实现序列化接口,如果想要序列化没有实现序列化接口的二方或者三方 jar 包中的类,可以定义一个具有相同属性的类,转换后再进行序列化。四、性能优化的核心思想四、性能优化的核心思想大家动手用 javap 进行反汇编之后你会发现,如果当前函数多次使用 data 时,第一种写法指令更少。第一种写法,第一次将 data 对象存储到当前栈帧的局部变量表中,使用时直接从局部变量表中获取,而第二种写法需要先装载 this 然后通过 getfield 方法来获取属性。想了解详细内容,可以参考我一篇专题文章:为什么推荐大家学习 Java 字节码有些前置判断应该放在尽量靠前面的位置,避免做了一系列不必要的操作。伪代码如下:四、性能优化的核心思想四、性能优化的核心思想空间换时间是性能优化最常用的手段之一。其中缓存就是空间换时间的一种典型应用。CPU 缓存、浏览器缓存、CDN 缓存、DNS 缓存、内存缓存、Redis 缓存等,它们都是将数据缓存在离使用者更近的地方,或者读取速度更快的存储介质中,通过空间换时间的方式实现性能优化的。在展开讲解之前,大家想想如果电商平台自营商品都从中央仓库发货,近的用户收货时间会短一些,远的用户收货时间可能会很长。四、性能优化的核心思想四、性能优化的核心思想前面给出了不同系统组件的操作时差图,下面给出一张 CPU 缓存、内存和磁盘操作的时差参考图4。鉴于内存的读写速度比磁盘快很多,通常设计方案时会选择将热点数据缓存在内存中,这样就可以加快访问速度。HBase 会在 RegionServer 中包含 BlockCache,当读请求到 HBase 后,会先尝试查询BlockCache,如果获取不到再去 HFile 和 MemStore 中获取,如果获取到了该数据,则返回给上游的同时将 Block 块缓存到 BlockCache 中6。四、性能优化的核心思想四、性能优化的核心思想此时,可以直接使用 java.lang.ThreadLocal 或者 java.lang.InheritableThreadLocal(可以完成父线程到子线程的值传递),也可以使用 com.alibaba.ttl.TransmittableThreadLocal(使用线程池等会池化复用线程的执行组件情况下,提供 ThreadLocal 值的传递功能,解决异步执行时上下文传递的问题。具体用法请参考其官方文档)。参考代码示例如下:另外一种空间换时间的方式是预留(侧重于空间维度)。四、性能优化的核心思想四、性能优化的核心思想可以将一些不容易变动且瞬间访问量可能较大的数据,可以预先加载到缓存中。可以将不变动的热点页面静态化,放到 CDN 节点中加速访问。比如在某个确定的环节,报表内容就已经可以确定下来,在用户需要下载报表之前提前将报表放在缓存或者专门的文件加速服务器中,这样用户来的时候就可以快速下载。其实上面的某东快速送货的案例也体现出了预留的思想。一般来说,热门商品不可能用户买的时候再去生产厂家订做,都会提前在仓库中储备一定量的货物,用户下单时直接发货即可。其实索引也是典型的空间换时间的做法。这里的索引包括文件系统索引、数据库系统索引等。他们的核心思想都是一样的,先为要检索的内容建好索引,在查找时根据索引快速寻找对应的数据。我们在开发时,通常会用到 MySQL、Oracle 数据库,为了提高查询速度,通常会设计索引,让索引能够覆盖我们的查询条件。以 MySQL 的 InnoDB 引擎来说,使用 B+树的结构为索引字段建好索引。在查询时如果命中索引,可以通过索引快速找到对应的数据。四、性能优化的核心思想四、性能优化的核心思想以上两种模式的共同点是每个节点都拥有相同的数据。在某种条件下,需要将存放在一个数据库中的数据分散存储到多个数据库(主机)上,达到分散单机设备负载的效果。就涉及到了数据的切分,主要包括水平拆分和垂直拆分。垂直切分,通过将不同业务的数据,放到不同的数据库中,实现不同业务之间数据库层面的隔离。水平切分可以按照某种规则将某些字段分散到多个库中,每个表中只包含一部分数据5。四、性能优化的核心思想四、性能优化的核心思想比如用户在系统上报名,后端在处理完报名逻辑后,如果报名成功需要给用户推送公众号发送报名成功的消息,此时直接返回给用户成功即可。发送公众号消息的任务可以放在线程池中异步执行。假如后端逻辑需要执行 100 毫秒,发送公众号消息需要执行 100 毫秒,如果同步发送消息,耗时为 200 毫秒。如果发送公众号消息的环节采用异步的方式,那么对用户而言只需要等待 100 毫秒即可。同步转异步还包括延迟加载。比如在领域驱动设计时,某个聚合根中的实体并不是所有场景都需要用到,构造该实体需要查询其他表,此时可以采用延迟加载的方式,在获取该属性时再去查询构造该实体。4.3.3 串行转并行如果某个任务包含多个步骤,串行执行时间时每个步骤耗时相加才是最终的耗时,容易超时或者体验不好。可以考虑将这些子任务使用 ForkJoinPool、Stream 并行流操作。还可以将大任务拆分成多个小任务,通过消息队列或者大数据框架并行执行。四、性能优化的核心思想四、性能优化的核心思想数据的操作一般就分为两类,一种是读,一种是写。在读多写少的场景下,可以通过读写锁、乐观锁的方式降低冲突的范围或者说降低冲突带来的性能损耗来提高性能。为了更好地帮助大家理解什么是降低锁的范围,下面举一个栗子:比如某个用户只能对某个商品下一个订单,为了防止用户重发下单一般需要先查询是否有订单,如果已经有一个订单则不允许再次下单。在高并发场景下,不加锁,同时有两个并发下单请求过来,都查询订单表,发现没有数据,都插入下单记录,这就尴尬。可能有经验的同学已经有思路了,加锁呗!有些同学可能就开始设计了 key:userId+_+shopId。(实际开发中可能很多人并不会这么傻,这里只是举个例子,帮助大家理解)产品要求的是不能对同一个商品重复下单,没说是不能对同一个店铺的不同商品重复下单。显然上面锁的范围扩大了。应该将用户 id 和 商品 id 组合在一起构成分布式锁的 key:userId+_+goodsId。四、性能优化的核心思想四、性能优化的核心思想时就可以节省大量的时间。4.4.2 实时转离线在实际业务开发中,实时计算出某个数据难以实现或者性能很差,如果产品能够允许的情况下,可以转为离线计算。4.4.3 随机读写转顺序读写随机 IO 读写速度和顺序 IO 读写速度差距较大。因此有可能的话,尽量将随机读写转为顺序读写。4.4.4 就近原则前面讲到使用缓存达到空间换时间的效果。但有时候还可以采用“就近原则”,使用”更近”的数据,调用”更近”的服务。一般来说,同一个虚拟机 同一台服务器 同一个集群 同一个机房 同一个城市 同国其他城市 跨国(表示优于)。四、性能优化的核心思想四、性能优化的核心思想不同的是,当字符串小于 1 M 时,扩容一倍;如果超过 1M,每次扩容 1M 的空间,最大长度为512 M。这里的最大长度限制就是为了保证不因单个 key 不至于过大,导致慢查询。4.4.7 CPU 密集型和 IO 密集型对于 CPU 密集型任务,可以通过增加机器和提高机器配置,使用大数据框架,升级架构等方式来解决。对于 IO 密集型操作,可以侧重考虑通过并行、异步、合并、预处理等方法进行性能优化。4.4.8 根据技术特点去优化具体到某个技术都有会辅助性能优化的命令或工具,需要大家自己去掌握。不同的工具性能优化也有自己的特点,如 MySQL 设计好索引、HBase 设计好 rowkey 规则等,需要针对性地学习。如 MySQL 可以使用 explain 指令分析语句性能进行分析;MySQL 创建联合索引时,将差异性最大的字段放在前面;比如业务上需要根据手机号后 5 位进行查询,除了设计表字段存储完整手机号外,可以额外设置一个字段仅存储后 5 位并加索引,这样就可以实现利用该索引提高查询速度;比如业务上需要将 MySQL 存储的大数据量导出时,避免使用 limit,可以通过每次查询修改where 条件来加快查询速度。如:SELECT*FROM sometable WHERE id#lastId ORDER BYid ASC LIMIT 20;四、性能优化的核心思想四、性能优化的核心思想比如在设计某个功能时,需要多表联查,此时可以通过在一个表中冗余另外一个表的字段来避免联查操作;还可以通过 ES 等技术做一个大宽表来避免 DB 层多表联查,来提高性能。比如将单库改成读写分离架构,或者改成分库分表架构,将单体应用拆分成多个微服务,甚至采用单元化架构等。4.5 产品层面不是所有的性能优化都是要通过技术手段来实现,如果技术手段不容易解决,可以考虑从产品设计层面来实现。4.5.1 加限制条件(产品层面)如果从技术层面性能优化不好实现,可以和产品经历协商,让产品经历做出一些妥协,加上一些限制条件,很多问题就迎刃而解。比如产品层面可以限制用户上传的图片、文件大小,限制录入的文本大小等。比如提供不同的版本,默认选择性能较好的版本。如视频网站提供不同的清晰度,默认展示普通清晰度的视频或者根据用户网速选择最优的清晰度。还可以和产品同学进行交流,砍掉某些不合理的搜索条件等来获得更好的性能体验。如微信发送图片时,即使发送时你选择原图,对方接收到的一般都是预览图,只有打开后选择”下载原图”才会真正去下载原图到本地。四、性能优化的核心思想五、注意事项五、注意事项5.1 避免过早优化在设计性能优化方案时,要注意避免过早优化,要考虑投入产出比。在产品初期,只要性能不会特别影响用户体验,通常不需要特别关注性能问题。可以把更多的精力投放到满足用户的核心需求,提高产品销量,让产品生存下来。5.2 考虑其他指标在考虑性能优化的同时要注意可读性、可用性、稳定性、正确性、可拓展性、安全性等。一般来说,软件设计的原则应该优先于性能优化的原则。即不能为了性能优化而破坏软件设计的原则,给后续的拓展维护埋坑。比如设计要符合“高内聚、弱耦合”、“封装复杂度”;比如设计模式的一些基本原则:“单一职责原则”、“迪米特法则”、“开闭原则”、“里氏替换原则”、“接口隔离原则”、“组合复用原则”等。在数据量不是超大的情况下,纯内存操作相对是比较快的,千万不要为了省一两次 for 循环而写出非常难以读懂的代码。如在使用缓存来加快访问速度时,要考虑必要性。还要考虑数据库和缓存的一致性问题。在实际工作中就遇到有同学为了提高性能,对某个看起来一定不会修改的数据加了缓存,超时时间设置好几十分钟,然而由于忽略了某个导致变更的入口,最终因缓存和实际数据不一致引发线上故障。这里有个小技巧,加缓存时一定要慎重思考缓存超期时间,如果没有太大把握,可以加个动态开关在出现问题时可以及时调整。如果没有太大把握,在能满足业务需求的情况下,尽量将超期时间设置短一些。五、注意事项六、实际案例六、实际案例6.1 案例描述下面给出一个模拟的业务场景,大家可以结合上面给出的性能优化核心思路,自己先设计一个性能优化的方案再和给出的方案进行对比,如果自己设计的方案更好,为你点赞;如果设计的方案没有给出去的方案更好,对比下差异。注:本文给出的方案未必是最佳,只是希望帮助大家理解性能优化思想如何应用。功能描述:用户浏览活动时根据是否订阅过该活动,来决定是否展示“订阅提醒”按钮。用户可以通过点击“订阅提醒”进行订阅,用户订阅后活动开始前 10 分钟发送微信公众号消息提醒用户参与活动。用户量可能较大,预计每个活动最大可能有几十万甚至上百万。同一个活动可以开展多轮,下一轮订阅的时候只发送当前轮次订阅的这一批人。需要思考的问题:数据存在哪里?如何尽快把消息发出去?大量的消息发送会不会超过下游承载能力?每次浏览活动时都要查询当前用户是否订阅过该活动,如果瞬间涌入大量流量怎么办?如何保证查询的性能?大量发送完毕的订阅数据如何清除?6.2 设计分析问题 1:数据存在哪里?六、实际案例六、实际案例六、实际案例七、总结七、总结本书主要讲述性能优化的本质,性能优化的思想来源,性能优化的常见思路,还讲到了性能优化的注意事项。最后结合一个具体的场景讲述性能优化如何落地。性能优化没有标准答案,优秀的程序员应该灵活将多种优化方案结合一起灵活运用,根据实际的场景做出权衡。另外,希望大家能够意识到,本专题所讲的性能优化方面的知识并非只是帮助我们去做技术方案,还建议大家用性能优化的视角来学习中间件、源码,你将收获更多。在这里推荐几本和性能优化相关的图书,大家感兴趣可以读读:性能之巅、图解性能优化、Web 性能权威指南、Java 性能权威指南、深入理解计算机系统等。本书主要根据个人的技术经验编写,难免会有一些纰漏,如有错误欢迎批评指正。最后,我想说:“热爱可抵漫长岁月”。希望大家能够在技术成长的道路上“Stay hungry,Stay foolish”,并且保持热爱,不断精进,一起加油!附本书的思维导图:七、总结56可以通过以下方式获取配套资源(含思维导图):性能优化方法论配套资源的原始文件。参考资料1 博文视点.性能之巅:洞悉系统、企业与云计算M.电子工业出版社.20152 林晓斌.MySQL 45 讲M3 钱文品.Redis 深度历险M.电子工业出版社.20194 日 小田圭二/日 榑松谷仁/日 平山毅/日 冈田宪昌.图解性能优化.人民邮电出版社.20175 Mycat 开源项目组.Mycat 权威指南M6 杨曦.HBase 不睡觉书M.清华大学出版社.20187 小孩子 4919.MySQL 是怎样运行的:从根儿上理解 MySQLM.人民邮电出版社.20208 Apache Dubbo 社区.本地调用M9 Abel Avran&Floyd Marinescu著 孙向辉.霍泰稳.李琨 译.领域驱动设计精简版。InfoQ 企业软件开发丛书M
展开阅读全文