一、如何做性能优化
所谓的优化,不应该只是局限于让耗时P99、或CPU使用率、或SLI等指标变好。而是应该更广义地去看,即:现状存在什么问题、如何进行优化、如何保证不劣化、为什么无法优化。
性能指标
有很多指标可以评判一个服务/系统的性能如何,最直观的比如:一秒内最多能处理多少请求。不同服务/系统所关注的指标也会不同,比如Redis系统更加关注读写请求量、内存使用率等;Mysql系统更加关注磁盘IO、连接数、慢请求等。而且大部分指标需要关联性地去审视,如果单看某一指标,往往意义不大,比如不同CPU使用率下的接口耗时往往是不同的。
本文主要会讨论业务系统中常见的、且需要及时优化的指标,具体有:
-
响应时间
- server接口耗时P99
- 端TTI P90(端耗时可以在server接口无变更的情况时,通过预处理、视觉等手段进行优化)
-
资源利用率
- CPU利用率
- 内存利用率
- 带宽
-
Runtime
- CPU受限次数
- GC次数和耗时
找出问题
得先要找出当前系统存在的问题,才能进一步聚焦优化方向并制定合适的方案及目标。
-
看代码找出架构不合理的地方——对于程序员是最直接的办法
-
火焰图是个非常好用且简单的工具——目前用的是Go语言,并且公司相关的基建也是面向Golang的
- 火焰图一般就是看CPU Profiling、Heap Profiling
- 代码中锁用的多的情况下,还可以检查下互斥锁的持有者和开销、阻塞情况
-
通过Metrics打点可以进行耗时统计,如
- 接口整体耗时
- 在不同场景的不同耗时
- 接口各阶段的耗时分布
- 长尾耗时分布和对应的request等指标
-
服务默认监控
- CPU使用率、内存使用率
- Goroutine数量
- GC次数和耗时
- CPU受限情况
-
链路分析(看代码、看trace,或框架提供的工具)发现重复调用、串行调用等问题
- 某场景下,一次请求的上下游交互
- 多次请求后,绘制上下游交互的统计全貌
-
压测更易暴露问题
接口优化思路
在没有明确或没有找到问题时,最先应当做的是去对现状进行分析。 找出服务存在的问题或可优化的地方,再针对地进行优化、效果测试、结果分析。比如序列化占用过多CPU以影响耗时、获取下游数据时的调用调度不合理。
而对于没有明显思路的情况下,我这里罗列一些可能存在优化的方向:
-
基建函数或功能库滥用
-
日志。其实在线上提供足够的、常见的debug信息即可,为了应对线上问题排查。现状常常是打了非常多的日志,而这些日志对oncall排查毫无帮助。尤其是需求测试增加的debug日志,应该在上线前删除。
-
metrics。打点观测和日志一样的,我建议是需要观测什么就打什么点,比如业务大盘或监控大盘需要关注的数据。而不是一个功能或需求无节制地打上好多点。一段时间后就不再使用的、废弃的,也应当主动去删除。
-
系统调用。类似于env.IsPPE()查询系统环境变量,用多了也是会影响服务的,比如X服务就发现env.IsPPE占用了1%的CPU。
-
deep copy。深拷贝其实是一个耗时消耗比较大的功能,曾遇到过通过深拷贝进行字段打包,其耗时超过10ms。
-
序列化,也是非常占用系统资源的操作。
- 减少序列化次数,比如tcc提供getter工具,直接本地缓存对象。遇到过一个bug是:tcc存放太多数据,且每次读取都进行序列化,导致接口耗时明显上涨。
- 减少序列化成本,比如使用sonic库代替原生json库、thrift的序列化消耗远小于json
-
-
链路治理
- 上下游交互合理性:是否重复调用、是否冗余调用。在交易营销链路治理时,发现提单页胡乱调用营销接口,有一半是重复调用和冗余调用。
- 上下游交互的数据,减少数据量可提升效率和稳定性
-
服务部署和接口交互
- sidecar 合并部署,将数据传输从网络改为本地线程
- sdk 代替 rpc
- 多服务 合并编译,相当于从两个服务变成一个服务
- Tango语言特性(字节自己魔改的编译)
-
串行调用改并行
- 通常情况,是获取下游数据( RPC 、DB、 Redis )的调用 编排 不合理,多个执行节点可以优化成并发执行。
- 有一种串行「硬改」为并行的方式:以“流量放大/流量浪费”为代价,预先获取全部数据。例如“先读AbTest再获取实验券”改成“同时读AbTest和全部实验券,再判断用什么券”。
- 第二种串行「硬改」为并行的方式:将一个完整功能的接口拆分为多个更 细粒度 的逻辑接口,由上游组装控制逻辑接口的组合,以实现上游的调度并行。提单页这期的耗时优化,就是这个思路,将凑单模块拆分成多段逻辑分开执行。
- 注意,对数据打包进行并发处理好像没什么用(在优化RenderPromotion商列算价接口时,对耗时较大的字段打包(>5ms)操作进行了并发处理,在ppe压测发现没有明显受益)。
-
同步改异步
- 针对耗时较长且结果非强依赖的逻辑,可以考虑异步执行。异步的实现方式,可以用线程池,也可以用消息队列,还可以用一些调度任务框架。
- 数据(db、redis)多机房同步或强一致关闭。效果:能优化几毫秒;需要考虑:不同机房数据不一致是否存在问题?
-
空间换时间
- 合理使用 缓存 , 可以根据缓存命中率和带来的收益来评判ROI。
- 减少GC次数。tango的一项功能:超过X内存后才进行GC,避免频繁GC、减少GC带来的耗时
- 内存预分配,小对象集合做预分配
-
数据预处理
- 提前把需要查询的数据,提前计算好,放入缓存或者表中的某个字段。
- 由客户端携带上次请求或上跳页面的结果,减少本次请求中的部分逻辑。
-
池化思想
- 数据库连接池、线程池、对象池,本质是预分配与循环使用,作用都是避免重复创建对象或创建连接,可以重复利用,避免不必要的损耗。
- LocalCache优化,避免本地对象影响GC
-
CPU受限优化,这是一个不怎么起眼的指标,却会严重影响接口错误率和耗时p99
- 通常是由问题代码导致cpu集中短时间内被消耗完(容器部署模式)
- 调大机器规格
-
AbTest使用优化(通常,业务服务都需要使用AbTest进行需求结果指标的评判,尤其C端)
- 使用vm_agent代替rpc调用
- 一次请求链路上,避免重复请求,同时也可以保障数据一致性。有两个方法:(a)通过接口参数进行传递;(b)第一次请求后使用redis进行缓存(不推荐)。
- 减少AbTest返回的数据量
防劣化手段
-
定期review接口性能变化
- 在购物车耗时优化项目里,会在每双周review购物车链路上的核心依赖服务的耗时p99。现在感觉每周周会review下核心接口的耗时变化,是可以及时发现很多问题的,比如发现某次上线导致明显裂化、某段时间qps明显变化等。
- 上线时,检测上线前后的耗时、CPU等指标的对比。通常用小流量来观测就行。
- 性能平台会出触发每日压测,来记录性能变化,包括cpu、mem、耗时。不过建议是用单独启用一个性能压测集群,可以保证稳定的qps。
-
监控灵敏度提升,比如接口耗时。但是很容易产生大量的噪音。
-
防劣化工具建设,在各个研发节点辅助发现服务性能劣化、性能劣化归因分析
- 《TT&抖音服务端性能防劣化最佳实践》文中画的防劣化框架,非常好看。但是我觉得这个方案和能力是有点偏理想的,还是得靠组织内或SRE同学有意识地去观测、去运用工具。(图略......)
端优化思路
涉及客户端相关的优化操作,目前我接触过:
-
预测用户动线,并预处理
- 预测用户(最大可能性的)的下一操作行为,提前进行下一操作行为的接口调用+缓存数据
-
Feed冷启动时避免请求推荐
- 可以在缓存上次关闭前得到的推荐信息
- 可以使用非推荐数据,比如广告、热门榜
-
server接口chunk返回数据,端渲染时间提前
代码优化
其实代码和性能没有太大关系,即便是反复缠绕、混乱不堪的代码,也只是让人难以读懂,但若其整体架构合理、链路合理,就基本不会造成什么性能问题。而上文所说的一些优化方向,在写出优雅且合理代码时,自然而然就不存在劣化风险,也就不需要什么待优化了。比如开发写代码时多思考下需要日志和metrics来干嘛,就可以避免胡乱加日志、无限制加metrics;比如在技术方案设计时多了解代码现状和上下游逻辑,就可以避免接口重复调用、冗余调用。
当然人无完人,写下的代码或多或少、或局部或全局,且随着时间推移,都是会产生问题的,因此需要我们周期性地review和重构。听百度的朋友说,百度内部在尝试利用AI生产需求代码:通过“需求文档+技术方案+代码规范”等物料,生成对应的代码。我不知道效率和代码可用率是多少,但我问了个问题,那些堆叠的繁琐的甚至不可考古的业务逻辑,AI怎么处理的。朋友说历史代码和老服务,只能手动维护,AI可写不了。另外,我也很好奇,AI在生产代码时,是否会考虑到性能,而规避可能引起性能问题的代码,甚至进行主动优化。
说回主题,代码造成的性能劣化是很难当场发现或排查破案的。尤其是有的实现,局部去看是没有问题的,比如在每个模块中调用下游获取某对象详情(甚至这是符合xx范式的)从而导致重复调用。有的问题只会在长尾或特殊场景才会触发问题,比如在长尾请求中出现for循环百万次的badcase。或者全局看、或者积少成多、或者上下游逻辑变更,有些内容才会演变成线上问题。关于这块怎么优化,我想到的是预防+发现:技术方案设计和编写代码时,可以站在性能的角度上多思考思考其合理性(事前);在上线前,可以进行压测试分析;在上线后,观测线上变化(事后)。
二、形而上学的理论
三、具体案例分析
从服务或项目维度出发,记录优化思路、方法、开展过程
内部项目和逻辑就不展开了.....