业务系统性能改造手册 · 性能基线

用指标链定位首个性能瓶颈

2026-08-053 min read性能基线
摘要

从负载拐点出发,把业务延迟、调用阶段、资源饱和和受控实验串成可证伪的证据链,选出第一个值得处理的瓶颈。

压测期间应用 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。压测端先饱和的档位应当作无效输入,不能拿来判断服务端上限。

“拐点”也不等于某个固定百分比。可以把它定义成一组项目内可验证的条件,例如:完成吞吐不再跟随实际启动速率增长,同时尾延迟或错误率持续恶化。条件和接受范围要在看结果前写进实验协议,避免看到曲线后再挑一个对结论有利的位置。

拐点只回答系统从哪一档开始失去线性响应。它还没有回答时间耗在哪里。

二、把一次请求拆成能够对账的时间

订单接口的客户端耗时可以粗略写成:

text
1客户端观察耗时 2 = 网络与入口等待 3 + 应用入口排队 4 + 业务代码执行 5 + 数据库连接等待与 SQL 执行 6 + Redis / 消息队列 / 下游调用等待 7 + 响应返回

这段分解不要求所有时间严格相加成一个完美公式。异步执行、并行调用和采样误差都会让简单相加失真。它的用途是建立对账意识:客户端 P99 变长时,服务端 trace、方法计时和组件指标里应该能看到相应阶段的变化;若看不到,就先检查观测缺口,不要补一个听起来合理的解释。

2.1 先按业务动作分组

上一篇的脚本已经给列表、详情、提交和状态查询打了 action 标签。诊断时继续按这个标签拆开吞吐、延迟和错误。混合场景的总体 P99 上升,可能只是某个低比例写请求恶化,也可能是所有读取都受到共享资源影响;两者的处理优先级不同。

每类动作至少要能关联到以下信息:

  • 客户端的开始时间、终态、耗时和运行编号;
  • 应用入口的请求标识、路由、状态码与总耗时;
  • trace 或等价调用记录中的主要阶段耗时;
  • 同一采样窗口内的线程、连接、队列和依赖组件指标。

关联不一定依赖某个特定观测产品。关键是统一运行编号与时间窗口,并保留足以从业务结果回查调用阶段的标识。日志、指标和 trace 的时钟不一致时,先校准时间,避免把相邻两次请求拼成一条“证据链”。

2.2 区分执行时间和等待时间

“数据库阶段耗时很长”还不够具体。应用可能花时间等待连接,也可能拿到连接后 SQL 才慢。类似地,线程池任务耗时增长,可能是任务执行变慢,也可能是提交后在队列里等了很久。

因此阶段指标至少拆到能支持判断的粒度:

yaml
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 变慢、锁等待或数据库资源恶化,都不能把原候选升级为瓶颈结论。

环境不允许安全调整时,可以使用隔离副本或可回退的受控等待注入,定向改变连接获取路径。做不到定向扰动,就把结论留在“候选”,不要拿降压结果补足因果证据。

实验记录可以这样写:

yaml
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}

这个实验不要求一次证明“唯一根因”。真实系统可能同时受多个约束。它只需要判断当前候选能否解释已观察到的主要恶化,以及解除这个约束后,业务结果是否按预测方向变化。

还有一种有价值的结果:资源指标改变了,业务吞吐和尾延迟却没有实质变化。这说明该资源可能接近上限,但不是当前最先限制业务结果的因素。把它写进排除记录,避免团队过几天又从同一张高位曲线重新猜一次。

五、把结论写成一张可复核的证据单

本文所说的“首个瓶颈”,指当前环境、数据、负载和目标下,最先限制业务结果且已有验证证据的约束。它不等于整个系统永远最慢的组件;负载模型或资源预算改变后,结论需要重新验证。

下面的证据单把事实、推断、排除过程和下一步验证分开:

yaml
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}

优先级不能只按“优化起来容易”排序。先看它影响哪些业务动作、是否位于首个拐点,再记录证据覆盖和改动风险。置信度不做拍脑袋分级,直接写清是否有重复实验、阶段耗时覆盖是否足够、受控实验是否命中预测,以及还缺哪些数据。

候选排除记录也属于产物。至少保留候选名称、支持证据、反对证据、验证动作和当前状态。被否定的假设不会让诊断失败;它缩小了下一轮搜索范围。

六、到什么程度才可以开始改

准备进入改造前,证据单至少要回答:

  1. 哪个有效负载档位首次出现业务吞吐、尾延迟或错误恶化;
  2. 哪类业务动作受到影响,时间主要增加在哪个调用或等待阶段;
  3. 哪个有限资源与该阶段等待同时进入约束,配置上限和观测来源是什么;
  4. 哪个单变量实验支持、削弱或否定了候选;
  5. 为什么它比其他候选更值得先处理,当前结论有哪些限制;
  6. 下一次改造只改变什么,预期哪些指标朝什么方向变化,何时回退。

缺少前两项,团队还不知道系统在哪里坏掉;缺少受控实验,结论仍停在相关性;缺少回退和预期,下一轮改造就无法验收。

本文没有得出“订单系统的瓶颈是数据库”之类的结论,因为磁盘上没有真实压测结果、trace 或资源数据。我们得到的是一条可以执行的诊断路径,以及能容纳真实证据的记录格式。等案例工程跑出数据后,只有通过这条路径留下的首个约束,才有资格进入后续改造清单。