业务系统性能改造手册 · 性能基线
用指标链定位首个性能瓶颈
从负载拐点出发,把业务延迟、调用阶段、资源饱和和受控实验串成可证伪的证据链,选出第一个值得处理的瓶颈。
压测期间应用 CPU 升到高位,同时出现几条慢 SQL,线程池队列也开始增长。应该先改 SQL、加线程,还是扩容?只看这组现象,三个答案都缺证据。
CPU 高可能是系统正在有效工作,也可能是无效重试消耗;慢 SQL 可能拖住请求,也可能只占很小的流量;线程排队既可能是原因,也可能是下游变慢后的结果。监控面板把它们放在同一时间轴上,不等于已经给出了因果关系。
小编现阶段更看重一条能被推翻的指标链:负载发生变化时,业务结果在哪里开始恶化,时间花在哪个调用阶段,哪个有限资源进入饱和,再通过受控实验让候选原因按预期变化。本文只定位第一个值得处理的瓶颈,不讨论缓存、批量、异步或扩容方案。
一、先找到系统失去线性响应的位置
上一篇已经构造了可重复的订单负载,并按档位保存每轮结果。诊断从这些结果开始,而不是先打开 CPU 面板找一条最显眼的曲线。
把每个有效档位放到同一张记录中:
| 负载档位 | 实际启动速率 | 完成吞吐 | P95/P99 | 错误率 | 未启动/在途请求 | 运行有效性 |
|---|---|---|---|---|---|---|
${STAGE_A} | ${STARTED_RATE} | ${THROUGHPUT} | ${P95} / ${P99} | ${ERROR_RATE} | ${COUNTS} | ${VALID} |
${STAGE_B} | ${STARTED_RATE} | ${THROUGHPUT} | ${P95} / ${P99} | ${ERROR_RATE} | ${COUNTS} | ${VALID} |
本文没有演示数字,因为我们尚未运行案例工程。需要观察的是相邻档位之间的形态变化:目标到达率继续增加,完成吞吐是否仍近似同步增长;P99 是否先于 P95 明显拉长;超时或连接失败是否开始出现;压测端有没有产生 dropped_iterations。压测端先饱和的档位应当作无效输入,不能拿来判断服务端上限。
“拐点”也不等于某个固定百分比。可以把它定义成一组项目内可验证的条件,例如:完成吞吐不再跟随实际启动速率增长,同时尾延迟或错误率持续恶化。条件和接受范围要在看结果前写进实验协议,避免看到曲线后再挑一个对结论有利的位置。
拐点只回答系统从哪一档开始失去线性响应。它还没有回答时间耗在哪里。
二、把一次请求拆成能够对账的时间
订单接口的客户端耗时可以粗略写成:
1客户端观察耗时
2 = 网络与入口等待
3 + 应用入口排队
4 + 业务代码执行
5 + 数据库连接等待与 SQL 执行
6 + Redis / 消息队列 / 下游调用等待
7 + 响应返回这段分解不要求所有时间严格相加成一个完美公式。异步执行、并行调用和采样误差都会让简单相加失真。它的用途是建立对账意识:客户端 P99 变长时,服务端 trace、方法计时和组件指标里应该能看到相应阶段的变化;若看不到,就先检查观测缺口,不要补一个听起来合理的解释。
2.1 先按业务动作分组
上一篇的脚本已经给列表、详情、提交和状态查询打了 action 标签。诊断时继续按这个标签拆开吞吐、延迟和错误。混合场景的总体 P99 上升,可能只是某个低比例写请求恶化,也可能是所有读取都受到共享资源影响;两者的处理优先级不同。
每类动作至少要能关联到以下信息:
- 客户端的开始时间、终态、耗时和运行编号;
- 应用入口的请求标识、路由、状态码与总耗时;
- trace 或等价调用记录中的主要阶段耗时;
- 同一采样窗口内的线程、连接、队列和依赖组件指标。
关联不一定依赖某个特定观测产品。关键是统一运行编号与时间窗口,并保留足以从业务结果回查调用阶段的标识。日志、指标和 trace 的时钟不一致时,先校准时间,避免把相邻两次请求拼成一条“证据链”。
2.2 区分执行时间和等待时间
“数据库阶段耗时很长”还不够具体。应用可能花时间等待连接,也可能拿到连接后 SQL 才慢。类似地,线程池任务耗时增长,可能是任务执行变慢,也可能是提交后在队列里等了很久。
因此阶段指标至少拆到能支持判断的粒度:
1requestStageEvidence:
2 runId: ${RUN_ID}
3 action: ${ACTION}
4 window: ${START_END_TIME}
5 client:
6 completedThroughput: ${VALUE}
7 p95: ${VALUE}
8 p99: ${VALUE}
9 terminalCounts: ${COUNTS}
10 application:
11 entryQueueWait: ${METRIC_OR_TRACE_QUERY}
12 businessExecution: ${METRIC_OR_TRACE_QUERY}
13 database:
14 connectionAcquireWait: ${METRIC_OR_TRACE_QUERY}
15 statementDuration: ${METRIC_OR_TRACE_QUERY}
16 executor:
17 queueWait: ${METRIC_OR_TRACE_QUERY}
18 taskDuration: ${METRIC_OR_TRACE_QUERY}
19 dependencies:
20 redisDuration: ${METRIC_OR_TRACE_QUERY}
21 messagePublishDuration: ${METRIC_OR_TRACE_QUERY}
22 downstreamDuration: ${METRIC_OR_TRACE_QUERY}
23 coverage:
24 sampledRequests: ${COUNT}
25 missingStages: ${LIST}${METRIC_OR_TRACE_QUERY} 应记录实际查询或原始文件位置,不能只写“看监控”。若 trace 采用抽样,还要记录抽样规则和覆盖量。稀疏样本可以支持候选判断,但不能假装代表全部 P99 请求。
三、资源指标要和等待现象一起看
找到变慢阶段后,再检查支撑这个阶段的有限资源。CPU、堆内存、数据库连接、应用线程、队列容量、Redis 连接和下游并发都可能成为约束,但“数值很高”不是统一的饱和定义。
饱和应落到资源的服务能力和等待后果上:
- CPU 接近分配上限的同时,运行队列或节流增加,业务执行阶段随负载拉长;
- 数据库连接池长期接近上限,同时获取连接等待和超时增长;
- 线程池活动线程触顶,同时队列长度与排队时间增长;
- 下游并发或连接占满,同时调用耗时、超时和上游线程占用一起增加;
- 消息队列生产或消费受限,同时投递耗时或积压持续增长。
本文不写“达到多少就算饱和”。不同资源配额、采集周期和业务目标对应不同阈值。证据单要保存配置上限、观测值、等待指标和业务影响,阈值来源则引用本项目的容量约定。
3.1 用时间顺序排除明显的伴随指标
候选原因至少应在业务恶化之前或同时发生,并随负载档位呈现一致关系。若 P99 已经恶化数分钟后 CPU 才上升,CPU 更可能是后续工作堆积的结果。若连接占用一直很高,但获取等待没有变化,它也不足以单独解释新增延迟。
时间顺序仍只是筛选条件。两个指标同时变化,可能共同受第三个变量驱动。比如下游变慢会同时拉长请求、占住应用线程并推高连接使用量;直接把线程数当根因,会把结果当成原因。
3.2 先检查观测本身有没有撒谎
性能诊断很容易被采集方式带偏。常见问题包括:
- 平均值掩盖尾部,或者不同接口被聚合到一个序列;
- 监控采集间隔大于短时拥塞持续时间;
- 指标标签维度过粗,无法区分运行编号和业务动作;
- trace 丢失慢请求,日志只记录成功请求;
- 连接池“使用中”与“等待获取”采用了不同时间窗口。
发现这些缺口时,当前结论应降级为候选,不要用更多推理补齐缺失数据。先修观测,再重跑相同负载。
四、用受控实验尝试推翻候选原因
到这一步,我们可能得到这样的候选:订单提交在拐点后主要增加了数据库连接等待,连接池使用量同时接近配置上限。它比“数据库有问题”具体,但仍不能直接成为瓶颈结论。
下一步要设计一个只改变候选约束的实验,并提前写下预期:若候选成立,哪些业务指标和阶段指标应改变;若没有改变,怎样否定或降低它的优先级。
先把两类实验分开。降低到达率后连接等待和尾延迟一起消失,只能复现业务拐点,说明问题依赖负载强度;应用线程、数据库、下游和队列压力都同时下降,它不能证明数据库连接就是首个约束。
验证数据库连接等待,要保持到达率停在同一个有效退化档位,并固定数据快照、缓存、SQL、请求组合和下游配置。可以在隔离环境中,对应用连接池的可用连接预算做一次有上限的短时调整。开始前先确认数据库连接上限仍有安全余量,设定连接数、数据库 CPU、I/O、锁等待和错误率的停止条件;受控运行结束后立即恢复原预算,并按基线协议恢复数据状态。
这个动作也不是“连接池越大越好”的优化建议。它只是定向扰动候选等待路径。若调整后连接获取等待按预测下降,SQL 执行耗时与数据库资源没有转坏,订单提交的尾延迟或完成吞吐也同步改善,候选得到支持。若连接预算确实改变,获取等待却没有下降;或者获取等待下降但业务结果不变;又或者压力转移成 SQL 变慢、锁等待或数据库资源恶化,都不能把原候选升级为瓶颈结论。
环境不允许安全调整时,可以使用隔离副本或可回退的受控等待注入,定向改变连接获取路径。做不到定向扰动,就把结论留在“候选”,不要拿降压结果补足因果证据。
实验记录可以这样写:
1falsificationExperiment:
2 candidateId: ${CANDIDATE_ID}
3 hypothesis: ${RESOURCE_CONSTRAINT_CAUSES_STAGE_WAIT_AND_BUSINESS_DEGRADATION}
4 baselineRunId: ${BASELINE_RUN_ID}
5 controlledRunId: ${CONTROLLED_RUN_ID}
6 onlyPlannedChange: ${BOUNDED_CONNECTION_BUDGET_OR_TARGETED_WAIT_PERTURBATION}
7 purpose: candidate_attribution
8 safety:
9 isolatedEnvironment: ${TRUE_OR_FALSE}
10 databaseHeadroomEvidence: ${QUERY_OR_FILE}
11 stopConditions: ${CONNECTION_CPU_IO_LOCK_ERROR_LIMITS}
12 rollbackValue: ${ORIGINAL_VALUE}
13 maxDuration: ${DURATION}
14 fixedConditions:
15 snapshotId: ${SNAPSHOT_ID}
16 cachePolicy: ${CACHE_POLICY}
17 requestMix: ${REQUEST_MIX}
18 arrivalRate: ${SAME_VALID_DEGRADED_RATE}
19 sqlAndCodeArtifact: ${DIGEST}
20 downstreamProfile: ${PROFILE}
21 predictionIfSupported:
22 businessMetric: ${EXPECTED_DIRECTION}
23 connectionAcquireWait: ${EXPECTED_DIRECTION}
24 statementDuration: ${EXPECTED_STABLE_DIRECTION}
25 databaseResources: ${EXPECTED_SAFE_RANGE}
26 rejectionRule:
27 - connection_budget_changed_but_acquire_wait_did_not_follow_prediction
28 - acquire_wait_changed_but_business_result_did_not_follow_prediction
29 - pressure_shifted_to_statement_lock_or_database_resource_saturation
30 observed: ${TO_FILL_AFTER_RUN}
31 decision: ${SUPPORTED_WEAKENED_OR_REJECTED}这个实验不要求一次证明“唯一根因”。真实系统可能同时受多个约束。它只需要判断当前候选能否解释已观察到的主要恶化,以及解除这个约束后,业务结果是否按预测方向变化。
还有一种有价值的结果:资源指标改变了,业务吞吐和尾延迟却没有实质变化。这说明该资源可能接近上限,但不是当前最先限制业务结果的因素。把它写进排除记录,避免团队过几天又从同一张高位曲线重新猜一次。
五、把结论写成一张可复核的证据单
本文所说的“首个瓶颈”,指当前环境、数据、负载和目标下,最先限制业务结果且已有验证证据的约束。它不等于整个系统永远最慢的组件;负载模型或资源预算改变后,结论需要重新验证。
下面的证据单把事实、推断、排除过程和下一步验证分开:
1bottleneckEvidence:
2 id: ${EVIDENCE_ID}
3 experimentId: ${EXPERIMENT_ID}
4 scope:
5 environment: ${ENVIRONMENT_REFERENCE}
6 snapshotId: ${SNAPSHOT_ID}
7 cachePolicy: ${CACHE_POLICY}
8 loadScenario: ${SCENARIO_ID}
9 action: ${ACTION_OR_ALL}
10
11 businessTurningPoint:
12 lastLinearStage: ${RUN_ID}
13 firstDegradedStage: ${RUN_ID}
14 evidence:
15 throughput: ${FILES_OR_QUERIES}
16 p95P99: ${FILES_OR_QUERIES}
17 terminalCounts: ${FILES_OR_QUERIES}
18 loadGeneratorValidity: ${FILES_OR_QUERIES}
19
20 candidate:
21 statement: ${SPECIFIC_RESOURCE_AND_WAIT_PATH}
22 affectedStage: ${REQUEST_STAGE}
23 saturationEvidence: ${CONFIG_LIMIT_AND_OBSERVATION}
24 temporalRelation: ${BEFORE_OR_ALONGSIDE_DEGRADATION}
25 falsificationExperiment: ${EXPERIMENT_REFERENCE}
26 conclusion: ${SUPPORTED_WEAKENED_OR_REJECTED}
27
28 alternatives:
29 - candidate: ${ALTERNATIVE}
30 evidenceFor: ${EVIDENCE}
31 evidenceAgainst: ${EVIDENCE}
32 status: ${OPEN_OR_REJECTED}
33
34 priority:
35 businessImpact: ${AFFECTED_ACTIONS_AND_FAILURES}
36 confidence: ${EVIDENCE_COVERAGE_AND_UNCERTAINTY}
37 changeRisk: ${KNOWN_RISK}
38 decision: ${WHY_THIS_IS_FIRST}
39
40 nextValidation:
41 plannedChange: ${ONE_CHANGE}
42 expectedBusinessEffect: ${DIRECTION_NOT_FAKE_NUMBER}
43 expectedStageEffect: ${DIRECTION}
44 rollbackCondition: ${CONDITION}
45
46 limitations:
47 - ${MISSING_DATA_OR_SCOPE_BOUNDARY}优先级不能只按“优化起来容易”排序。先看它影响哪些业务动作、是否位于首个拐点,再记录证据覆盖和改动风险。置信度不做拍脑袋分级,直接写清是否有重复实验、阶段耗时覆盖是否足够、受控实验是否命中预测,以及还缺哪些数据。
候选排除记录也属于产物。至少保留候选名称、支持证据、反对证据、验证动作和当前状态。被否定的假设不会让诊断失败;它缩小了下一轮搜索范围。
六、到什么程度才可以开始改
准备进入改造前,证据单至少要回答:
- 哪个有效负载档位首次出现业务吞吐、尾延迟或错误恶化;
- 哪类业务动作受到影响,时间主要增加在哪个调用或等待阶段;
- 哪个有限资源与该阶段等待同时进入约束,配置上限和观测来源是什么;
- 哪个单变量实验支持、削弱或否定了候选;
- 为什么它比其他候选更值得先处理,当前结论有哪些限制;
- 下一次改造只改变什么,预期哪些指标朝什么方向变化,何时回退。
缺少前两项,团队还不知道系统在哪里坏掉;缺少受控实验,结论仍停在相关性;缺少回退和预期,下一轮改造就无法验收。
本文没有得出“订单系统的瓶颈是数据库”之类的结论,因为磁盘上没有真实压测结果、trace 或资源数据。我们得到的是一条可以执行的诊断路径,以及能容纳真实证据的记录格式。等案例工程跑出数据后,只有通过这条路径留下的首个约束,才有资格进入后续改造清单。