性能优化
收录于 工程治理体系
性能优化不是调参数,先看系统在做什么
性能优化很容易把几个不同问题混在一起。更稳的做法,是先分清系统有没有在重复付费,这是用户可感知还是不可感知优化,它在不在高价值路径上,以及这轮改动到底怎么衡量价值。
团队一聊性能优化,第一反应通常都差不多:
- CPU 高了,调线程。
- IO 高了,加机器。
- SQL 慢了,补索引。
这些动作当然不算错。问题在于,它们大多是在处理结果,不是在处理问题本身。
我后来比较在意的一件事,是别太快把“性能优化”理解成参数优化。参数、索引、线程池、缓存策略这些东西都重要,但它们更像手段,不是起点。该先看清的,是系统到底在做什么,资源到底花在了哪里。
但真往下做时,我发现“资源花在哪”这句话还是太粗了。一次性能讨论里,经常混着几件完全不同的事:有的是在问系统为什么越来越重,有的是在问用户是不是已经感知到慢了,有的是在问这件事值不值得现在救,还有的是在问做完以后拿什么证明。几件事搅在一起,最后就会变成每个人都在说优化,但说的不是同一个问题。
每次都这样。
后来我给自己定了一个更土的问法。性能讨论一开始,我通常会连着追几句:
- 这条路径是不是在重复付费。
- 这次改动到底是不是用户能感知到的。
- 就算要做,为什么是现在,为什么这个先做。
- 现在花掉的这笔钱,到底是系统为了跑起来必须交的税,还是结构已经开始稳定制造浪费。
- 做完以后,准备拿什么证明它真的有用。
这几句看着像一套框架,但我拿它主要是为了防止讨论滑偏。前两句在问问题类型,中间一句在问优先级,后两句才是在问动作和验证。顺序一乱,后面就很容易变成大家都在说优化,但其实各答各的。
下面这张图是这套追问的大致走法:
flowchart TD
Start["性能问题出现"] --> Q0["先看系统到底在做什么"]
Q0 --> Q1{"是不是在重复付费?"}
Q1 -->|"同一结果被反复付费"| A1["砍重复成本"]
Q1 -->|"不是重复消耗"| Q2{"用户能不能感知?"}
A1 --> Q3
Q2 -->|"可感知"| T2a["体验问题:盯延迟 / P95 / 失败率"]
Q2 -->|"不可感知"| T2b["承载问题:盯成本 / 容量 / 峰值水位"]
T2a --> Q3{"在不在高价值路径上?"}
T2b --> Q3
Q3 -->|"核心路径"| P["优先处理"]
Q3 -->|"边缘路径"| W["排后暂缓"]
P --> Q4{"成本性质?"}
W --> Q4
Q4 -->|"系统税"| S1["克制优化"]
Q4 -->|"结构性浪费"| S2["改流程 / 改边界"]
S1 --> V["定验证口径:量出这轮优化值不值"]
S2 --> V
先看系统是不是在重复付费
“重复消耗”这件事,必须先说清楚。它指的是同一个业务结果,被系统反复付了多次钱。
最常见的是下面几种:
- 一份数据被多个环节重复消费。
- 同一段内容被多次序列化和反序列化。
- 一个结果明明可以复用,却被不同服务各算一遍。
- 数据先完整写下去,后面另一条路径又重新读出来再加工。
这类问题和“某个点慢了一点”不一样。它更像是系统在为同一个结果反复付费。资源在花,流程也在跑,但新增的业务价值并没有同步增加。
这件事不先单独拎出来,后面很多优化都会滑到局部调参。你可以把某一跳做快一点,但只要整条路径还在重复付费,总成本结构通常不会真变。
我之前写 《ClickHouse 变成系统风险以后,别再把它当性能优化题》 时,其实就在处理这个问题。表面上看是 ClickHouse 查询重、IO 压力大,逼我改流程的,是同一份数据已经在别的路径里做过一轮处理,后面却还在沿另一条路再读、再解、再算一遍。那时候我想砍的就是这笔已经付过却还在重复付的钱,某条 SQL 快几十毫秒反而不是重点。
再常见一点的小例子,就是列表接口把分页数据和 count(*) 默认绑在一起。很多页面其实只需要先把前二十条返回来,结果系统每次都顺手把总数也算了。接口不一定慢到报警,但这笔成本会跟着每次请求稳定出现。遇到这种场景,先该问的是“这笔钱是不是每次都非付不可”,不是“count 能不能再调快一点”。
讨论一旦落到这里,很多调参动作才会露出它原来的位置:它可能有用,但它只是止痛片,不是手术。
这次到底是在解体验问题,还是承载问题
第二层要回答的问题是:这轮优化到底会不会被用户感知到。
我现在会先把优化分成两类。
第一类是可感知优化。 比如首页是不是更快出来,提交是不是更快完成,高峰期是不是不再抖,失败是不是明显变少。这类优化直接落在体验上,用户能感觉到。
第二类是不可感知优化。 比如 GC 压力小了一些,CPU 空出来一截,单机承载更稳,资源成本下来了。这类优化用户未必直接有感觉,但它可能换来更低成本、更大余量,或者为后面的增长留出空间。
这两类都值得做。不过判断方式不能一样。
可感知优化,核心看体验有没有实打实地改善。 不可感知优化,核心看它有没有换来明确收益,比如成本下降、容量上升、峰值更稳、故障更少。
这两类混着谈,讨论很快就会变怪:一边拿 CPU 降了 10% 证明优化成功,另一边却解释不清用户为什么还是觉得系统没变快。那问题出在你用机器指标回答了体验问题。
比如一个列表页把 count(*) 拆掉以后,真正该盯的是首屏是不是更早返回,而不是数据库图是不是变漂亮了。反过来,如果做的是批处理前移、缓存命中提升、GC 压力下降,那就别拿用户体感去要求它,应该老老实实看成本、容量和峰值水位。
真到会上,最怕的不是没人看指标,而是不同人拿着不同指标,在回答不同问题。
先保哪条链路,不是技术判断,是业务取舍
第三层要回答的,是优先级问题。
“高价值路径”这句话,讲的是业务取舍,跟资源类型、体验类型都不在同一层。
同样是可感知优化,支付提交链路和低频管理后台不是一个优先级;同样是不可感知优化,核心交易入口的峰值稳定性和一个边缘接口少几毫秒,也不是一个优先级。
所以我现在会多问一步:这次优化到底是不是落在高价值路径上。
这里通常要看三件事:
- 这条路径是不是核心业务结果会经过的地方。
- 一旦慢或者抖,用户、收入、转化或交付结果会不会直接受影响。
- 它是高频主路径,还是低频边缘路径。
优先级这一步要是放到最后才谈,“体验更差”和“业务更重要”就很容易被混成一回事。不是所有用户能感知的优化都该先做,也不是所有系统内部很重的问题都值得现在解决。优先级归根结底还是业务取舍。
同样是高峰期抖一下,支付提交链路和后台导出任务,业务的容忍度完全不是一回事。真排优先级时,这种差别比“谁看起来更卡”重要得多。
不是所有开销都该被叫浪费
第四层回答的,才是资源性质问题。
我现在会区分两个概念:系统税,和结构性浪费。
系统税是一个思考方向。 它指的是系统为了把自己运转起来,不得不支付的那部分成本,比如:
- 序列化和反序列化
- 网络传输
- 数据拷贝
- 协议转换
- 框架层 dispatch
- 锁、调度和上下文切换
它的意义是帮你分清一件事:现在花掉的 CPU、内存、IO,到底是在做业务本身,还是在替系统搬运、转换、协调。
结构性浪费则是另一回事。 它不是“系统有开销”这么简单,而是系统结构本身在稳定制造没有必要的成本。典型信号是:
- 反复等待和排队
- 明明能复用却重复计算
- 不必要的重试和补偿
- 缓存污染
- 无效读写
- 一条链路拆得太碎,导致跨层来回搬数据
所以两者的关系应该这样看:
- 系统税是在问,资源是不是花在系统运转上。
- 结构性浪费是在问,这些成本里有没有一部分本来就不该长期存在。
真把这两层混着用,做事时最容易出现两种误判:一种是明明只是系统为了跑起来要交一点税,你却急着把它都当成该重构的浪费;另一种是明明架构已经在稳定制造重复成本,你却把它当成正常开销一直忍着。前一种会让团队过度优化,后一种会让系统越来越重。
常有的事。
概念本身不重要,重要的是你接下来准备做什么。把一笔成本认成系统税,后面多半是克制地优化;把它认成结构性浪费,后面就该回头看流程和边界了。
指标不是为了把图补全,是为了证明这轮优化值不值
最后一层才是验证问题。
可观测性不是另一个优化点,它的作用是衡量前面几层判断到底有没有成立。
如果你说自己在解决重复消耗,那就要能看到链路耗时占比、重试率、重复处理量,或者关键资源的下降。
如果你说自己在做可感知优化,那就要看端到端延迟、P95 / P99、失败率和高峰期表现。
如果你说自己在做不可感知优化,那就要看成本、容量余量、峰值承载或者故障率是不是更好了。
否则整个过程很容易退化成两句很空的话:
“感觉这里有问题。” “优化完感觉好了。”
这时候问题在于你连“这次到底在证明什么”都没定义清楚。
图再全,也不能替你回答这轮优化到底值不值。它只能帮你把答案量出来。
我现在会怎么排一轮优化
说到底,性能优化应该先从业务问题出发,而不是从机器和参数出发。
因为优化的目的是解决客户已经感知到的问题,或者提前把会卡住业务增长的成本和风险拿掉。业务问题没定义清楚,后面的调参、扩容、重写,很多时候都只是把错误答案做得更快。
所以真到一轮优化要排优先级时,我现在通常会这样看:
- 先把问题说清楚。是页面慢、提交卡、高峰期抖、单位请求成本太高,还是一条路径已经开始拖住后面的业务迭代。
- 再看这是不是一条值得现在先救的业务路径。不是的话,哪怕技术上很别扭,也未必值得先动。
- 再判断这笔成本到底属于重复付费、系统税,还是别的正常开销。判断错了,动作就会跟着错。
- 最后才决定是调参数、改链路、换边界,还是先补观测,再拿对应指标去验。
这样看,“基于业务做优化”就不是一句空话了。它至少意味着:
- 先把业务结果写清楚,再看系统里哪一段成本和这个结果直接相关。
- 先把优先级排清楚,再决定是解体验问题、承载问题,还是重复付费问题。
- 先把验证口径定清楚,再决定这轮优化到底该看端到端体验、容量余量,还是重复成本有没有被砍掉。
所以现在再有人问我,性能到底该从哪儿下手,我一般不会先回线程、索引还是缓存。我更想先追问一句:这次到底是哪条业务结果出了问题,或者哪笔成本已经贵到不能再忍。这个问题说清了,后面的动作才有资格叫优化。