Appearance
性能优化方法论
定位:工程实践方法论(工程方法论系列收官篇之一)。性能优化不是技巧清单,是一条纪律链:先测量、再归因、按层下刀、验证固化。没有测量就没有优化,只有占卜。本篇与 观测性三件套(信号从哪来)、问题定位步骤(排查动作)互链。文末「速查附录」收纳早期速查版的四步要点。
第一定律:优化前先测量
没有 profile 的优化是占卜。 测量分两层:观测三件套负责"选地方",剖析三件套负责"出证据"。
| 层 | 回答 | 手段 | 输出形态 |
|---|---|---|---|
| 指标(面) | 慢在哪个接口/哪个资源 | Prometheus P99/错误率/饱和度 | 时序曲线 |
| 链路(线) | 慢在哪一段 span | OTel / Arthas trace | span 耗时排序 |
| 剖析(点) | 慢在哪一行/哪个对象 | 见下"剖析三件套" | 热点证据 |
剖析(profile)三件套——拿到了"面"和"线"之后的最后一公里:
| 剖析件 | 抓什么 | 典型工具 | 典型产出 |
|---|---|---|---|
| CPU 剖析 | 计算热点(哪段代码烧 CPU) | async-profiler 火焰图 / Arthas profiler | 最宽的塔=最大头的栈 |
| 内存剖析 | 对象增长与持有 | jmap + MAT 支配树、GC 日志 | 增量最大的对象及其 GC Roots 链 |
| 线程与锁剖析 | 等待与阻塞(IO/锁/池排队) | jstack(多次对比)、Arthas thread -b | BLOCKED 堆积、锁持有者 |
三个纪律:基线先行(优化前的数字先记下来,否则"快了"无法证明)。对比采样(两份间隔抓取对比,单次快照会误导)。测量环境要代表真实(压测数据要有真实流量画像,否则优化的是"另一个系统")。
第四条纪律:优化前先约定靶心——"P99 从 3.2s 降到不超过 500ms 即达标"。没有靶心的优化会在"再快一点"里无限滑行,投入产出在不知不觉中劣化。
测量的收敛顺序是一个漏斗,从面到点,代价递增、结论递实:
text
指标(面) 哪个接口慢?P99 3.2s,其他接口正常 ← 秒级,看大盘
↓
链路(线) 哪一段慢?下单 → 库存查询 span 1.8s ← 分钟级,拉一条 trace
↓
剖析(点) 为什么慢?CPU/内存/锁三件套各跑一遍 ← 需要动实例,低峰执行
↓
代码行 全表扫描 40 万行/次,缺联合索引 ← 证据闭合,才开始谈优化跳层的代价是真实的:跳过指标和链路直接抓 CPU,大概率抓到的是无关业务的编解码线程。跳过剖析直接改代码,就是"我觉得这里慢"的占卜开局。
分层框架:架构 > 算法 > 并发 > 参数 > 硬件
| 层 | 收益上限 | 典型动作 | 改动风险 |
|---|---|---|---|
| 架构 | 10×~100× | 加缓存、异步化削峰、读写分离、消灭跨机房/循环 RPC | 高:要方案评审 + 灰度 |
| 算法/数据结构 | 10×(热点路径上) | O(n²)→O(n log n)、命中索引、容器与数据布局选型 | 中 |
| 并发 | 数倍 | 串行 RPC 并行化、请求合并批处理、线程模型 | 中:引入竞态与超时语义变化 |
| 参数 | 10%~50% | 池大小/超时/批大小/GC 选项/内核参数 | 低 |
| 硬件 | <线性 | NVMe、升配、加核 | 花钱,且掩盖问题 |
投入产出排序规则:收益上限从上到下递减,改动成本也递减。所以决策不是"从下往上摘软柿子",而是——profile 说热点在哪层,就先打哪层。热点是一次 40 万行的全表扫描时(算法层),去调 JVM 参数(参数层)是安慰剂。热点是循环里 200 次跨机房 RPC 时(架构层),换更快磁盘(硬件层)是买拐杖治骨折。
帕累托:80% 的耗时来自 20% 的热点
- 耗时分布天然不均:80% 的总耗时集中在 20% 的接口/SQL/代码路径上。找到它们只要三张清单:慢查询 Top10、接口耗时 Top10、火焰图最宽塔。
- 每次只打最大头,打完重新测量。消掉一个热点后,第二名会升上来,且瓶颈类型可能换层(SQL 快了之后暴露锁等待)。分层框架要重走一遍。
- 帕累托也是止损依据:Top 清单打进长尾区(每项占比 <5%)时,剩余优化空间进入摊薄区。投入产出开始劣化,应转向固化与防退化。
- 清单要定期重算而不是一次性:流量结构随业务演化(新功能上线、渠道占比变化)。上季度的热点清单这季度可能已经失真——慢查询/慢接口 Top 清单按周自动生成,进稳定性周报。
走层案例:一次下单链路优化的三刀
text
起点:下单 P99 = 1.1s,SLO 承诺 500ms。基线入库,开始走层。
第1刀(架构层):链路 trace 显示串行调 3 个无依赖下游
(风控 200ms / 库存 190ms / 优惠券 210ms)
→ 三次 RPC 并行化:串行 600ms → 并行 220ms,P99 降到 640ms
第2刀(算法层):重新剖析,新最大头 = 优惠券查询 80ms/次
→ EXPLAIN 全表扫描 → 补联合索引 + 覆盖索引:80ms → 3ms,P99 降到 420ms
第3刀(参数层):并行化后连接池等待计数上涨(新瓶颈浮出)
→ 池 50→120、下游超时对齐其 P99:P99 降到 380ms,达标收手
要点:每一刀之前都重新测量——第1刀后瓶颈从"RPC 串行"(架构层)
换成了"全表扫描"(算法层),第2刀后又换成"池排队"(参数层)。
热点会换层,工具与手法跟着换,分层框架是地图不是一次性的 checklist。分层速查:每层的高频手法与库内延伸
| 层 | 高频手法 | 排查/验证工具 | 库内延伸 |
|---|---|---|---|
| 架构 | 缓存、异步化削峰、读写分离、消灭循环/跨机房 RPC | 链路 trace 看调用次数与跨度 | 缓存穿透-击穿-雪崩、多活容灾与单元化(跨机房 RT 账) |
| 算法 | 索引命中、复杂度降阶、批量化、数据布局 | EXPLAIN、火焰图 | 数据库怎么分库分表、索引 |
| 并发 | 串行调用并行化、请求合并、线程模型 | jstack 多次对比、Arthas thread -b | 问题定位步骤 现场保留一节 |
| 参数 | 池大小、超时、批大小、GC 选项 | 指标大盘、GC 日志、压测对比 | 观测性三件套 |
| 硬件 | 盘/核/网卡升级 | iostat、sar、全链路压测重校水位 | 3+1保障(容量水位) |
一次完整优化的叙事模板
text
现象:下单 P99 从 800ms 涨到 3.2s(量化 + 时间点 + 对齐变更时间线)
↓
测量:指标定位到下单接口;链路 Top span = 库存查询 1.8s;
剖析 = 新增查询条件无索引,全表扫描 40 万行/次(证据链闭合)
↓
假设:"X 变更导致 Y 路径劣化,表现为 Z"——因果链必须解释全部现象
(P99 涨而 P50 不动 ✓ 长尾特征;错误率 0 ✓ 纯读慢;对齐发布时间 ✓)
↓
验证:代价从低到高——EXPLAIN 复现 → 预发加索引 → 灰度 1% 新旧实例
同时刻 A/B 对比 P99(排除时段流量差异)
↓
固化:慢 SQL 门禁进 CI、性能基线入库、索引纳入巡检;
大变更(切库/升配)后重建基线(见 [数据迁移与切库方案](数据迁移与切库方案.md))叙事模板的价值不在"汇报好看",在于它强制每一步可证伪。假设写出来就可能被推翻,验证不通过就回滚假设而不是硬上。固化一步保证优化成果不随人员流动退化。它同时是述职/晋升里"性能优化"叙事的验收标准——还原不出这五段的优化故事,在评审上不成立。
追问:为什么分层框架把架构层排在最前,收益最大就一定先做吗?
不是按收益排,按"profile 指认的热点所在层"排。架构层收益上限最高,但若热点只是缺一个索引,上缓存架构是杀鸡用牛刀,还引入一致性复杂度。分层的真正含义是"下刀前先知道自己在哪一层、收益上限与风险各是什么"。最贵的错误不是选了低收益层,是用底层的廉价手段反复试探高层问题。例如调参治跨机房 RT、扩容治慢 SQL。
反模式
- 过早优化:Knuth 的完整语境是"不应在 97% 的时间里纠结那 3% 的热点"。不是"禁止优化",是"没有测量数据支撑的优化先不做"。启动期为想象中的百万 QPS 设计缓存分层,交付的是复杂度不是性能。
- 无基线优化:改前没记数,改后"感觉快了"。感觉不能进复盘,也不能进晋升材料。
- 多变量齐改:同时动缓存、参数、代码,好了不知道谁的功,坏了不知道谁的锅。这和 问题定位步骤 的"一次只改一个变量"是同一条纪律。
- 局部最优全局变差:单接口加缓存把下游缓存打穿。把定时任务从凌晨调到白天"提高资源利用率"挤爆白天 IO。优化必须回到全局大盘验证。
- 扩容当优化:加机器掩盖慢 SQL 与泄漏,成本线性上涨而问题指数积累。容量是最后一层,见分层框架最底行。
- 一次性优化:不设门禁不建基线,半年后新代码把老热点重新引进来。没有固化步骤的优化是租来的。
- 为想象流量优化:当前 QPS 500,为"将来可能的百万"上多级缓存与异地单元。容量问题用容量手段解(压测摸水位 + 弹性扩容),只有测量证明瓶颈真实存在,才进入优化流程。
- 只优化不定义慢:没人写下"P99 多少算慢、服务谁、什么场景",团队就永远在优化。SLO 先行,优化是对 SLO 的收敛,不是无目标的手艺活。
追问链
面试官:怎么证明是你的优化让接口变快了?
灰度 A/B:同一时刻新旧版本实例各承接一部分真实流量。对比 P99/P99 差值,排除时段、流量结构、机器差异。再跨一个业务周期(工作日/周末)重复验证趋势稳定。改前基线 + 同时刻对照,两个条件缺一个,因果都不成立。
面试官:优化到什么时候停?
三个停止条件取最先到达:SLO 达标(P99 满足业务承诺即停,不为简历多打 20ms)。边际收益劣化(热点清单打进长尾,每项 <5%)。机会成本(同样人力投稳定性/交付收益更大)。性能优化没有终点,但有明确的止损线——写不出下一步收益预估时就该停。
面试官:优化完怎么防止退化?
三道防线:慢 SQL/性能门禁进 CI(回归阶段拦截,超基线阈值不允许合并)。性能基线入库,版本间自动对比(P99 漂移 >10% 告警)。周期性压测巡检(大促前全链路压测重校水位,见 3+1保障)。退化的主要通道不是"老代码变慢",是"新代码重新引入老模式"——所以门禁比记忆可靠。
一句话总结
测量定位(指标选面、链路选段、剖析定点)→ 按层下刀(架构>算法>并发>参数>硬件,打 profile 指的那层)。→ 帕累托排序逐个消灭热点 → A/B 验证 → 门禁固化。 优化的专业性不体现在知道多少招,体现在每一步都有数字兜底。
速查附录:四步套路
收纳自早期速查版《性能优化套路》(原文已并入本篇)。适合排查时照着走一遍的最短路径:
- trace 定位:通过实际流量复现一次高耗时调用,拿到 traceId。
- RPC 耗时排序:按 traceId 拉全链路。统计链路各系统的 RPC 服务耗时与系统间交互耗时。再检查各系统连接耗时是否异常。最后将各 RPC 服务耗时按优先级排序,找最大头。
- 分层下刀(对应上文分层框架):
- 缓存——减少数据库访问次数与耗时计算量:线程内 invokeCache、分布式缓存 Redis、页面缓存这类手段;
- 多线程——增强处理速度,如单线程改多线程;
- 请求合并——减少循环调用次数,合并后一次转发;
- 异步化——缩小数据处理范围(减少查询内容、分页异步加载),逻辑简化。非核心逻辑走消息中间件解耦异步化。
- 复现验证:改完再次复现问题,观察优化后性能是否符合预期。不符合则回到第 1 步重新定位。
2026-10 新增(工程方法论系列)。