系统性能优化

一、总体原则

衡量业务价值

  1. 思考本次优化的核心价值

当前的性能问题对业务有什么影响?本次性能优化可以带来什么收益?在做性能优化之前,这个问题一定要和上下游、上级领导及其他相关方达成一致。这样做基于两点考虑。

第一,明确了目标和收益,才能能调动各方资源一起来做这件事,有时候可能还涉及产品、业务上的一些问题。

第二,任何的变更都有可能引入风险。如果业务上没有诉求,改造又存在一定的风险,那么技术单方面去做这事的意义在哪里,要想清楚。

  1. 衡量投入产出是否值得?

明确了存在业务价值后,还需要评估性能优化的成本,包括人力资源投入,服务器等软硬件资源投入。为了拿到收益,这些投入是否值得?

抓主要矛盾

二八原则,关注耗时最大的那20%,优化空间的最大的,大概率就在其中。我们要抓大放小,集中资源解决主要的性能问题,高效的拿到收益。如果性能还不达标,再来一次,从剩下的部分再找出耗时最大的那20%,对它们进行优化。

以监控为眼

监控能帮助我们发现问题,分析性能瓶颈,也能直观的检验优化的效果。必要的监控是性能优化的前提条件。包括应用程序自己打的各个环节的监控,中间件的监控等。没有监控,没有曲线面板,那就是两眼一抹黑。

二、性能优化层次

系统设计

系统设计是所有优化层次的最上层,也是最重要的,对整个系统质量有决定性的影响。通过合理的系统设计,可以提前规避掉很多问题。如果在设计层面考虑不周,在其他层面很难进行弥补。

系统设计是性能优化的大头,也是本文讨论的重点。但在高并发,大流量,性能要求极高的场景下,其他层面的任何细节优化也不可忽视。

代码优化

代码优化是一件非常细节的事,考验基本功,对api的熟悉程度,并且也有很多小技巧,需要不断积累的。常见几类如下。

  1. 数据结构与算法
    比如什么时候用List、Set、Map这些基本数据结构,以及如何根据需要去扩展它们。查找也是一个常见操作,想想怎么减少遍历次数。

  2. 线程池
    怎么设置线程池的参数

  3. NIO
    如果有涉及网络开发,NIO、零拷贝、直接内存,这些是绕不开的话题。

  4. 并发编程
    高效线程协作,减少锁冲突,无锁并行。

数据库优化

后端服务一般涉及数据存储,比如MySQL、MongoDB、Redis等。

  1. 选择合适的存储

  2. 查询优化、索引优化

  3. 水平扩展、分库分表

JVM优化

  1. 调整内存、堆空间

  2. 选择更优秀的GC

操作系统优化

  1. 升级操作系统

  2. 系统级参数调优

硬件优化

  1. 垂直扩展,升级更好的硬件

  2. 横向扩展,加更多的服务器

三、系统设计优化思路

1. 任务编排

一个执行流程是由一系列的逻辑节点组成的,通过编排它们之间的关系,可以缩短整个执行路径,降低整个执行流程的耗时。

Buffer

协调上游与下游处理速率不一致,减少上游处理时间。上游写到Buffer就结束了,下游从Buffer读取数据接着处理。

例如

  1. MySQL的刷盘机制
    MySQL并没有在每次提交事务时,立即把更新的数据写到磁盘,只是写到buffer,之后再异步刷新脏页到磁盘,这样就可以合并操作,多次commit写一次磁盘。参考InnoDB事务机制

  2. PV/UV统计,写Redis定时刷DB。
    PV/UV统计的写QPS较高,如果写MySQL,行锁冲突严重。这类数据一致性要求较低,可以先写Redis,在Redis中计数,再定时把Redis的数据刷到DB。

并行

微观上多个任务同时执行,利用多核特性。扩展到多台服务器,也是在增大并行度。

例如

  1. CPU密集型计算任务
    一般的归并排序算法只能单核跑,拆分成子任务,扔到ForkJoinPool,利用多核心并行计算提高效率。还可以用MapReduce,把计算分发到不同的服务器。

  2. 后端服务
    集群化部署的后端服务,不同的请求分散到不同的节点上处理,从这个视角上看,不同服务器的请求之间也是并行处理的。

并发

宏观上多个任务同时推进,利用系统的线程切换。在IO密集型场景下很实用。

例如

  1. 单核并发提效
    用多线程来执行一批耗时的RPC任务,当CPU把一个线程推进到等待RPC结果响应后,CPU切换到另外一个线程,把它也推进到同样等待的状态,如此就使得所有任务都尽快发起外调,然后所有线程同时等待返回结果。从微观上看,任务是串性的;从宏观上看,任务几乎是并行的。

并发也可以提高CPU利用率,更好的利用系统资源。上面的例子,如果只是单线程无并发,那么不仅这批任务执行慢,CPU利用率也非常低。

现在服务器一般都是多核的,能支撑更多的并发。一般的后端服务都是IO密集型的,所以RPC线程池都有数百个线程,以支持更好的并发,更好的利用CPU资源。

异步

异步任务不阻塞当前线程的执行。当前线程如果不关心异步任务的结果,不需要等待任务的完成,那么可以省略掉异步任务执行这段耗时。

例如

  1. 异步写离线数据、打日志

  2. 触发预热
    上游在调用下游之前的某一阶段,异步触发下游进行预热。上游也并不关心下游的预热结果。

2. 空间换时间

用额外的空间,换取读请求效率的提升。

Cache

避免每次读都那么费时。费时可能是因为读DB/磁盘,RPC/网络,复杂的计算。

例如

  1. 缓存DB数据/RPC数据
    放到Redis或者本地缓存,加速查询。

  2. 缓存计算结果
    定时计算结果,不必每次读取实时计算。

索引

占用额外空间,保存索引数据,便于快速查找。

例如

  1. MySQL索引文件
    MySQL的二级索引需要额外的空间存储,二级索引加速查询,避免遍历。

  2. 映射表
    或者叫索引表。假设订单按照userId分表,如果需要查询某个店铺的订单,那么要全表扫描,再聚合。可以维护一份店铺与订单的映射关系,相当于建了一个外置索引。先查到店铺有哪些订单ID,再根据订单ID查详情。

副本

提供数据副本,分担不同场景下的查询压力。

例如

  1. MySQL主从复制
    普通读请求走从库查询,分担查询压力。如果有强一致的,可以绑定走主库查询。

  2. ES作为数据副本
    不仅同一个存储内部有数据副本,不同的存储之间也可以有数据副本。
    DB数据同步到ES,DB提供在线查询,ES提供后台搜索。

3. 重写轻读

适用于读多写少的场景。写逻辑重一点,把耗时的部分放到写逻辑中,让读请求尽可能的简单且快速。
重写轻读可能和空间换时间同时出现,重写,很可能会写一个额外的数据结构,以供查询时使用。比如下面的汇总表、字段冗余,前面的索引,既体现了重写轻读,也体现了空间换时间。

异步推数

数据产生的过程可能比较重,但是查询数据非常简单。

例如

  1. 维护大宽表
    比如把商户维度的各个业务数据聚合到一起,方便查询。商户基础、商户签约数据,商户单量统计,不同业务数据的生产周期生产方式不一样,但查询很简单,只要商户id就能查到所有需要的数据。

  2. 特征平台
    异步生产特征数据,放到Redis,提供查询。

汇总表

减少group by和count操作,在写数据时维护好特定维度的汇总表。

字段冗余

减少join操作,代价是更新会变得麻烦,一个数据变化,可能要更新多张表的重复数据。几乎不变的数据适合做字段冗余。

4. 复用

避免重复进行耗时的操作。

参数复用

上游已经有的数据,带给下游,避免重复查询。

结果复用

已经计算过的结果记录起来,以便下次复用,避免重复计算。

线程复用

比如,线程池

连接复用

比如,数据库连接池

对象复用

比如,RPC Consumer Bean

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值