如果我来做性能优化

123 阅读11分钟

一、如何做性能优化

所谓的优化,不应该只是局限于让耗时P99、或CPU使用率、或SLI等指标变好。而是应该更广义地去看,即:现状存在什么问题、如何进行优化、如何保证不劣化、为什么无法优化。

性能指标

有很多指标可以评判一个服务/系统的性能如何,最直观的比如:一秒内最多能处理多少请求。不同服务/系统所关注的指标也会不同,比如Redis系统更加关注读写请求量、内存使用率等;Mysql系统更加关注磁盘IO、连接数、慢请求等。而且大部分指标需要关联性地去审视,如果单看某一指标,往往意义不大,比如不同CPU使用率下的接口耗时往往是不同的。

本文主要会讨论业务系统中常见的、且需要及时优化的指标,具体有:

  • 响应时间

    • server接口耗时P99
    • 端TTI P90(端耗时可以在server接口无变更的情况时,通过预处理、视觉等手段进行优化)
  • 资源利用率

    • CPU利用率
    • 内存利用率
    • 带宽
  • Runtime

    • CPU受限次数
    • GC次数和耗时

找出问题

得先要找出当前系统存在的问题,才能进一步聚焦优化方向并制定合适的方案及目标。

  1. 看代码找出架构不合理的地方——对于程序员是最直接的办法

  2. 火焰图是个非常好用且简单的工具——目前用的是Go语言,并且公司相关的基建也是面向Golang的

    1. 火焰图一般就是看CPU Profiling、Heap Profiling
    2. 代码中锁用的多的情况下,还可以检查下互斥锁的持有者和开销、阻塞情况
  3. 通过Metrics打点可以进行耗时统计,如

    1. 接口整体耗时
    2. 在不同场景的不同耗时
    3. 接口各阶段的耗时分布
    4. 长尾耗时分布和对应的request等指标
  4. 服务默认监控

    1. CPU使用率、内存使用率
    2. Goroutine数量
    3. GC次数和耗时
    4. CPU受限情况
  5. 链路分析(看代码、看trace,或框架提供的工具)发现重复调用、串行调用等问题

    1. 某场景下,一次请求的上下游交互
    2. 多次请求后,绘制上下游交互的统计全貌
  6. 压测更易暴露问题

接口优化思路

在没有明确或没有找到问题时,最先应当做的是去对现状进行分析。 找出服务存在的问题或可优化的地方,再针对地进行优化、效果测试、结果分析。比如序列化占用过多CPU以影响耗时、获取下游数据时的调用调度不合理。

而对于没有明显思路的情况下,我这里罗列一些可能存在优化的方向:

  1. 基建函数或功能库滥用

    1. 日志。其实在线上提供足够的、常见的debug信息即可,为了应对线上问题排查。现状常常是打了非常多的日志,而这些日志对oncall排查毫无帮助。尤其是需求测试增加的debug日志,应该在上线前删除。

    2. metrics。打点观测和日志一样的,我建议是需要观测什么就打什么点,比如业务大盘或监控大盘需要关注的数据。而不是一个功能或需求无节制地打上好多点。一段时间后就不再使用的、废弃的,也应当主动去删除。

    3. 系统调用。类似于env.IsPPE()查询系统环境变量,用多了也是会影响服务的,比如X服务就发现env.IsPPE占用了1%的CPU。

    4. deep copy。深拷贝其实是一个耗时消耗比较大的功能,曾遇到过通过深拷贝进行字段打包,其耗时超过10ms。

    5. 序列化,也是非常占用系统资源的操作。

      1. 减少序列化次数,比如tcc提供getter工具,直接本地缓存对象。遇到过一个bug是:tcc存放太多数据,且每次读取都进行序列化,导致接口耗时明显上涨。
      2. 减少序列化成本,比如使用sonic库代替原生json库、thrift的序列化消耗远小于json
  2. 链路治理

    1. 上下游交互合理性:是否重复调用、是否冗余调用。在交易营销链路治理时,发现提单页胡乱调用营销接口,有一半是重复调用和冗余调用。
    2. 上下游交互的数据,减少数据量可提升效率和稳定性
  3. 服务部署和接口交互

    1. sidecar 合并部署,将数据传输从网络改为本地线程
    2. sdk 代替 rpc
    3. 多服务 合并编译,相当于从两个服务变成一个服务
    4. Tango语言特性(字节自己魔改的编译)
  4. 串行调用改并行

    1. 通常情况,是获取下游数据( RPC 、DB、 Redis )的调用 编排 不合理,多个执行节点可以优化成并发执行。
    2. 有一种串行「硬改」为并行的方式:以“流量放大/流量浪费”为代价,预先获取全部数据。例如“先读AbTest再获取实验券”改成“同时读AbTest和全部实验券,再判断用什么券”。
    3. 第二种串行「硬改」为并行的方式:将一个完整功能的接口拆分为多个更 细粒度 的逻辑接口,由上游组装控制逻辑接口的组合,以实现上游的调度并行。提单页这期的耗时优化,就是这个思路,将凑单模块拆分成多段逻辑分开执行。
    4. 注意,对数据打包进行并发处理好像没什么用(在优化RenderPromotion商列算价接口时,对耗时较大的字段打包(>5ms)操作进行了并发处理,在ppe压测发现没有明显受益)。
  5. 同步改异步

    1. 针对耗时较长且结果非强依赖的逻辑,可以考虑异步执行。异步的实现方式,可以用线程池,也可以用消息队列,还可以用一些调度任务框架。
    2. 数据(db、redis)多机房同步或强一致关闭。效果:能优化几毫秒;需要考虑:不同机房数据不一致是否存在问题?
  6. 空间换时间

    1. 合理使用 缓存 可以根据缓存命中率和带来的收益来评判ROI。
    2. 减少GC次数。tango的一项功能:超过X内存后才进行GC,避免频繁GC、减少GC带来的耗时
    3. 内存预分配,小对象集合做预分配
  7. 数据预处理

    1. 提前把需要查询的数据,提前计算好,放入缓存或者表中的某个字段。
    2. 由客户端携带上次请求或上跳页面的结果,减少本次请求中的部分逻辑。
  8. 池化思想

    1. 数据库连接池、线程池、对象池,本质是预分配与循环使用,作用都是避免重复创建对象或创建连接,可以重复利用,避免不必要的损耗。
    2. LocalCache优化,避免本地对象影响GC
  9. CPU受限优化,这是一个不怎么起眼的指标,却会严重影响接口错误率和耗时p99

    1. 通常是由问题代码导致cpu集中短时间内被消耗完(容器部署模式)
    2. 调大机器规格
  10. AbTest使用优化(通常,业务服务都需要使用AbTest进行需求结果指标的评判,尤其C端)

    1. 使用vm_agent代替rpc调用
    2. 一次请求链路上,避免重复请求,同时也可以保障数据一致性。有两个方法:(a)通过接口参数进行传递;(b)第一次请求后使用redis进行缓存(不推荐)。
    3. 减少AbTest返回的数据量

防劣化手段

  • 定期review接口性能变化

    • 在购物车耗时优化项目里,会在每双周review购物车链路上的核心依赖服务的耗时p99。现在感觉每周周会review下核心接口的耗时变化,是可以及时发现很多问题的,比如发现某次上线导致明显裂化、某段时间qps明显变化等。
    • 上线时,检测上线前后的耗时、CPU等指标的对比。通常用小流量来观测就行。
    • 性能平台会出触发每日压测,来记录性能变化,包括cpu、mem、耗时。不过建议是用单独启用一个性能压测集群,可以保证稳定的qps。
  • 监控灵敏度提升,比如接口耗时。但是很容易产生大量的噪音。

  • 防劣化工具建设,在各个研发节点辅助发现服务性能劣化、性能劣化归因分析

    • 《TT&抖音服务端性能防劣化最佳实践》文中画的防劣化框架,非常好看。但是我觉得这个方案和能力是有点偏理想的,还是得靠组织内或SRE同学有意识地去观测、去运用工具。(图略......)

端优化思路

涉及客户端相关的优化操作,目前我接触过:

  1. 预测用户动线,并预处理

    1. 预测用户(最大可能性的)的下一操作行为,提前进行下一操作行为的接口调用+缓存数据
  2. Feed冷启动时避免请求推荐

    1. 可以在缓存上次关闭前得到的推荐信息
    2. 可以使用非推荐数据,比如广告、热门榜
  3. server接口chunk返回数据,端渲染时间提前

代码优化

其实代码和性能没有太大关系,即便是反复缠绕、混乱不堪的代码,也只是让人难以读懂,但若其整体架构合理、链路合理,就基本不会造成什么性能问题。而上文所说的一些优化方向,在写出优雅且合理代码时,自然而然就不存在劣化风险,也就不需要什么待优化了。比如开发写代码时多思考下需要日志和metrics来干嘛,就可以避免胡乱加日志、无限制加metrics;比如在技术方案设计时多了解代码现状和上下游逻辑,就可以避免接口重复调用、冗余调用。

当然人无完人,写下的代码或多或少、或局部或全局,且随着时间推移,都是会产生问题的,因此需要我们周期性地review和重构。听百度的朋友说,百度内部在尝试利用AI生产需求代码:通过“需求文档+技术方案+代码规范”等物料,生成对应的代码。我不知道效率和代码可用率是多少,但我问了个问题,那些堆叠的繁琐的甚至不可考古的业务逻辑,AI怎么处理的。朋友说历史代码和老服务,只能手动维护,AI可写不了。另外,我也很好奇,AI在生产代码时,是否会考虑到性能,而规避可能引起性能问题的代码,甚至进行主动优化。

说回主题,代码造成的性能劣化是很难当场发现或排查破案的。尤其是有的实现,局部去看是没有问题的,比如在每个模块中调用下游获取某对象详情(甚至这是符合xx范式的)从而导致重复调用。有的问题只会在长尾或特殊场景才会触发问题,比如在长尾请求中出现for循环百万次的badcase。或者全局看、或者积少成多、或者上下游逻辑变更,有些内容才会演变成线上问题。关于这块怎么优化,我想到的是预防+发现:技术方案设计和编写代码时,可以站在性能的角度上多思考思考其合理性(事前);在上线前,可以进行压测试分析;在上线后,观测线上变化(事后)。

二、形而上学的理论

三、具体案例分析

从服务或项目维度出发,记录优化思路、方法、开展过程

内部项目和逻辑就不展开了.....