一、总体原则
衡量业务价值
- 思考本次优化的核心价值
当前的性能问题对业务有什么影响?本次性能优化可以带来什么收益?在做性能优化之前,这个问题一定要和上下游、上级领导及其他相关方达成一致。这样做基于两点考虑。
第一,明确了目标和收益,才能能调动各方资源一起来做这件事,有时候可能还涉及产品、业务上的一些问题。
第二,任何的变更都有可能引入风险。如果业务上没有诉求,改造又存在一定的风险,那么技术单方面去做这事的意义在哪里,要想清楚。
- 衡量投入产出是否值得?
明确了存在业务价值后,还需要评估性能优化的成本,包括人力资源投入,服务器等软硬件资源投入。为了拿到收益,这些投入是否值得?
抓主要矛盾
二八原则,关注耗时最大的那20%,优化空间的最大的,大概率就在其中。我们要抓大放小,集中资源解决主要的性能问题,高效的拿到收益。如果性能还不达标,再来一次,从剩下的部分再找出耗时最大的那20%,对它们进行优化。
以监控为眼
监控能帮助我们发现问题,分析性能瓶颈,也能直观的检验优化的效果。必要的监控是性能优化的前提条件。包括应用程序自己打的各个环节的监控,中间件的监控等。没有监控,没有曲线面板,那就是两眼一抹黑。
二、性能优化层次
系统设计
系统设计是所有优化层次的最上层,也是最重要的,对整个系统质量有决定性的影响。通过合理的系统设计,可以提前规避掉很多问题。如果在设计层面考虑不周,在其他层面很难进行弥补。
系统设计是性能优化的大头,也是本文讨论的重点。但在高并发,大流量,性能要求极高的场景下,其他层面的任何细节优化也不可忽视。
代码优化
代码优化是一件非常细节的事,考验基本功,对api的熟悉程度,并且也有很多小技巧,需要不断积累的。常见几类如下。
-
数据结构与算法
比如什么时候用List、Set、Map这些基本数据结构,以及如何根据需要去扩展它们。查找也是一个常见操作,想想怎么减少遍历次数。 -
线程池
怎么设置线程池的参数 -
NIO
如果有涉及网络开发,NIO、零拷贝、直接内存,这些是绕不开的话题。 -
并发编程
高效线程协作,减少锁冲突,无锁并行。
数据库优化
后端服务一般涉及数据存储,比如MySQL、MongoDB、Redis等。
-
选择合适的存储
-
查询优化、索引优化
-
水平扩展、分库分表
JVM优化
-
调整内存、堆空间
-
选择更优秀的GC
操作系统优化
-
升级操作系统
-
系统级参数调优
硬件优化
-
垂直扩展,升级更好的硬件
-
横向扩展,加更多的服务器
三、系统设计优化思路
1. 任务编排
一个执行流程是由一系列的逻辑节点组成的,通过编排它们之间的关系,可以缩短整个执行路径,降低整个执行流程的耗时。
Buffer
协调上游与下游处理速率不一致,减少上游处理时间。上游写到Buffer就结束了,下游从Buffer读取数据接着处理。
例如
-
MySQL的刷盘机制
MySQL并没有在每次提交事务时,立即把更新的数据写到磁盘,只是写到buffer,之后再异步刷新脏页到磁盘,这样就可以合并操作,多次commit写一次磁盘。参考InnoDB事务机制 -
PV/UV统计,写Redis定时刷DB。
PV/UV统计的写QPS较高,如果写MySQL,行锁冲突严重。这类数据一致性要求较低,可以先写Redis,在Redis中计数,再定时把Redis的数据刷到DB。
并行
微观上多个任务同时执行,利用多核特性。扩展到多台服务器,也是在增大并行度。
例如
-
CPU密集型计算任务
一般的归并排序算法只能单核跑,拆分成子任务,扔到ForkJoinPool,利用多核心并行计算提高效率。还可以用MapReduce,把计算分发到不同的服务器。 -
后端服务
集群化部署的后端服务,不同的请求分散到不同的节点上处理,从这个视角上看,不同服务器的请求之间也是并行处理的。
并发
宏观上多个任务同时推进,利用系统的线程切换。在IO密集型场景下很实用。
例如
- 单核并发提效
用多线程来执行一批耗时的RPC任务,当CPU把一个线程推进到等待RPC结果响应后,CPU切换到另外一个线程,把它也推进到同样等待的状态,如此就使得所有任务都尽快发起外调,然后所有线程同时等待返回结果。从微观上看,任务是串性的;从宏观上看,任务几乎是并行的。
并发也可以提高CPU利用率,更好的利用系统资源。上面的例子,如果只是单线程无并发,那么不仅这批任务执行慢,CPU利用率也非常低。
现在服务器一般都是多核的,能支撑更多的并发。一般的后端服务都是IO密集型的,所以RPC线程池都有数百个线程,以支持更好的并发,更好的利用CPU资源。
异步
异步任务不阻塞当前线程的执行。当前线程如果不关心异步任务的结果,不需要等待任务的完成,那么可以省略掉异步任务执行这段耗时。
例如
-
异步写离线数据、打日志
-
触发预热
上游在调用下游之前的某一阶段,异步触发下游进行预热。上游也并不关心下游的预热结果。
2. 空间换时间
用额外的空间,换取读请求效率的提升。
Cache
避免每次读都那么费时。费时可能是因为读DB/磁盘,RPC/网络,复杂的计算。
例如
-
缓存DB数据/RPC数据
放到Redis或者本地缓存,加速查询。 -
缓存计算结果
定时计算结果,不必每次读取实时计算。
索引
占用额外空间,保存索引数据,便于快速查找。
例如
-
MySQL索引文件
MySQL的二级索引需要额外的空间存储,二级索引加速查询,避免遍历。 -
映射表
或者叫索引表。假设订单按照userId分表,如果需要查询某个店铺的订单,那么要全表扫描,再聚合。可以维护一份店铺与订单的映射关系,相当于建了一个外置索引。先查到店铺有哪些订单ID,再根据订单ID查详情。
副本
提供数据副本,分担不同场景下的查询压力。
例如
-
MySQL主从复制
普通读请求走从库查询,分担查询压力。如果有强一致的,可以绑定走主库查询。 -
ES作为数据副本
不仅同一个存储内部有数据副本,不同的存储之间也可以有数据副本。
DB数据同步到ES,DB提供在线查询,ES提供后台搜索。
3. 重写轻读
适用于读多写少的场景。写逻辑重一点,把耗时的部分放到写逻辑中,让读请求尽可能的简单且快速。
重写轻读可能和空间换时间同时出现,重写,很可能会写一个额外的数据结构,以供查询时使用。比如下面的汇总表、字段冗余,前面的索引,既体现了重写轻读,也体现了空间换时间。
异步推数
数据产生的过程可能比较重,但是查询数据非常简单。
例如
-
维护大宽表
比如把商户维度的各个业务数据聚合到一起,方便查询。商户基础、商户签约数据,商户单量统计,不同业务数据的生产周期生产方式不一样,但查询很简单,只要商户id就能查到所有需要的数据。 -
特征平台
异步生产特征数据,放到Redis,提供查询。
汇总表
减少group by和count操作,在写数据时维护好特定维度的汇总表。
字段冗余
减少join操作,代价是更新会变得麻烦,一个数据变化,可能要更新多张表的重复数据。几乎不变的数据适合做字段冗余。
4. 复用
避免重复进行耗时的操作。
参数复用
上游已经有的数据,带给下游,避免重复查询。
结果复用
已经计算过的结果记录起来,以便下次复用,避免重复计算。
线程复用
比如,线程池
连接复用
比如,数据库连接池
对象复用
比如,RPC Consumer Bean
4647

被折叠的 条评论
为什么被折叠?



