业务系统性能改造手册 · 验收交付
做一次可归因的全量回归
在复核实验协议的基础上,用可复现版本、单项与累积实验、针对性交互实验和业务正确性检查,解释组合改造的贡献、代价与回退依据。
做一次可归因的全量回归
从 02-01 到 05-02,订单系统先后减少请求、缩小 SQL 工作量、批量访问、调整并发预算、拆分异步链路,又补上超时、隔离和入口容量保护。现在把所有开关打开,与最初版本各跑一轮,最终版确实更快,能否据此给每项改造记功?
不能。缓存可能遮住 SQL 改造的收益,批处理可能改变连接持有时间,异步化也可能只是把耗时移出响应窗口。实验若总按“旧版在前、新版在后”的顺序执行,缓存预热、数据增长和机器时段也会混进版本差异。
小编现阶段的判断是:全量回归的任务不是证明最终版本更快,而是说明哪些变化在什么条件下产生了什么结果,又付出了什么代价。 本文只设计统一验收与归因,不再重讲各项改造怎样实现。真实环境和数据尚未提供,所有结果字段保持待填。
一、先证明今天的实验仍与基线可比
01-01 的基线协议到了最终回归阶段仍然有效,但不能机械复制旧文件。几个月的改造过程中,Java 版本、依赖镜像、数据快照、压测脚本甚至指标查询都可能变化。第一步是生成一份“基线协议差异单”。
1regressionComparability:
2 baselineExperiment: ${ORIGINAL_EXPERIMENT_ID}
3 regressionExperiment: ${CURRENT_EXPERIMENT_ID}
4 comparedAt: ${TIMESTAMP}
5 conditions:
6 applicationResources: ${SAME_OR_DIFFERENCE_WITH_EVIDENCE}
7 dependencyResourcesAndVersions: ${SAME_OR_DIFFERENCE_WITH_EVIDENCE}
8 networkTopology: ${SAME_OR_DIFFERENCE_WITH_EVIDENCE}
9 datasetSnapshotAndDistribution: ${SAME_OR_DIFFERENCE_WITH_EVIDENCE}
10 cacheStartPolicy: ${SAME_OR_DIFFERENCE_WITH_EVIDENCE}
11 requestMixAndArrivalModel: ${SAME_OR_DIFFERENCE_WITH_EVIDENCE}
12 downstreamAndFailureProfiles: ${SAME_OR_DIFFERENCE_WITH_EVIDENCE}
13 warmupAndSamplingRules: ${SAME_OR_DIFFERENCE_WITH_EVIDENCE}
14 metricDefinitionsAndQueries: ${SAME_OR_DIFFERENCE_WITH_EVIDENCE}
15 loadGeneratorCapacity: ${VALIDATION_EVIDENCE}
16 uncontrolledDifferences:
17 - difference: ${FACT}
18 likelyImpact: ${DIRECTION_OR_UNKNOWN}
19 mitigation: ${BLOCK_RANDOMIZE_RERUN_OR_LIMIT_CONCLUSION}
20 comparable: ${YES_NO_OR_LIMITED}
21 reason: ${EVIDENCE_BASED_REASON}发现差异后有三种处理方式:恢复旧条件;让所有版本在同一个新条件下重跑;无法恢复时缩小结论范围。不能拿历史基线的一组数字直接对比今天的最终版本,再用一段文字解释环境变化“影响不大”。
1.1 数据和缓存都可能携带上一轮实验
全量回归涉及订单创建、状态推进、Outbox、消费去重、重试和补偿。一次运行会改变数据库行数、状态分布、消息积压和缓存内容。每轮开始前要恢复同一数据快照,校验订单、事件、消费记录和失败区状态;结束后排空在途请求与消息,再恢复并复验。
缓存实验需要单独写明起点。冷缓存、按固定键集预热、沿用上一轮热缓存,是三个不同条件。预热完成标准也应基于吞吐、延迟和关键缓存指标进入稳定范围,而不是固定睡眠一段时间。
顺序本身也是干扰变量。持续升温、共享环境时段和依赖后台任务无法完全消除时,可以把同一批可比较运行划进一个区组,在区组内交替或随机安排候选版本,并记录实际顺序。NIST 的实验设计手册将这类非研究重点、却会影响结果的因素称为干扰因素;能够控制的因素可分区组处理,其余因素通过随机化减弱系统性偏差。
二、先造出真的能切换的改造状态
归因实验要求“关闭 A、保留 B”确实产生预期系统状态。一个布尔开关只改了 Controller 分支,后台扫描器仍在运行,或者数据库索引仍然存在,就不能称为 A 已关闭。
为每项改造建立状态清单:
1changeState:
2 id: ${CHANGE_ID}
3 article: ${ARTICLE_ID}
4 desiredState: ${ON_OR_OFF}
5 applicationArtifact: ${COMMIT_IMAGE_OR_DIGEST}
6 featureFlags: ${NAME_VALUE_AND_SOURCE}
7 schemaAndIndexes: ${VERSION_AND_REQUIRED_STATE}
8 backgroundWorkers: ${RUNNING_OR_STOPPED_WITH_EVIDENCE}
9 cacheAndMqState: ${POLICY_AND_EVIDENCE}
10 effectiveConfig: ${SANITIZED_SNAPSHOT_DIGEST}
11 startupAndRuntimeProof: ${LOG_METRIC_QUERY_OR_TEST}
12 semanticCheck: ${REQUEST_AND_EXPECTED_PATH}开关的关闭分支要尽量复现原逻辑,且不能顺手改变线程池、超时、日志级别或序列化方式。无法用运行时开关安全切换的变化,例如索引、表结构或消息状态机,可以使用独立版本节点或隔离环境复现。此时要保证构建方式、基础镜像和环境参数一致,并把无法消除的差异写入比较限制。
某些变化存在单向数据迁移。已经由异步流程推进过的订单,切回同步代码并不会自动还原成旧状态。实验编排必须从同一快照启动每个版本,不能在一个被新逻辑改过的数据集上直接关闭开关。
三、用三层实验回答不同问题
一次“最初版对最终版”只能回答组合结果。要拆开贡献,可以按单项、累积和针对性交互三层组织。三层实验回答的问题不同,不能拿其中一层补齐另外两层的证据。
3.1 单项实验回答“它独立改变了什么”
每个候选改造相对一组共同且可运行的参考状态执行,其余非前置改造关闭。对缓存,需要同时观察数据库查询减少、内存和偏差;对异步,需要同时观察响应延迟与最终完成;对限流,需要观察接受、拒绝和恢复,而不能只看成功请求 P99。
存在依赖时不能硬造“全关闭”版本。请求合并需要缓存 miss 路径,重试治理需要可靠事件身份,限流实验也承接已有的有界资源舱。这类单项实验应固定最小前置状态,并比较“前置状态”与“前置状态 + 目标改造”;贡献表同时记录这组参考状态,不能把结果记成目标改造脱离前提后的独立收益。
单项贡献写成成对比较:
1effect(A | prerequisites)
2 = metric(prerequisites + A) - metric(prerequisites)差值的好坏方向取决于指标。延迟和错误通常越低越好,完成吞吐在正确性成立时越高越好,内存与运维成本则属于代价。不要把不同单位合成一个综合分。
3.2 累积实验回答“按上线顺序加入后还剩多少收益”
按真实依赖顺序逐项加入改造:S0 → S1 → S2 ... → Sfinal。每一步只新增一个变化,并从干净快照运行。某项在单独启用时有效,加入已有组合后边际收益可能缩小;这不一定说明它无用,可能是前一项已经消除了同一部分工作。
1marginal(A after S) = metric(S + A) - metric(S)累积顺序会影响边际值,所以报告必须写明顺序和选择理由。不要把这一条顺序中的边际贡献解释成改造的永久属性。
3.3 针对性交互实验回答“两个变化是否互相增强或抵消”
全部改造做完整组合会迅速膨胀,通常没有必要。先根据架构关系和单项、累积结果选择怀疑存在交互的组合,例如缓存与请求合并、批处理与连接预算、异步化与限流。对两个二值改造 A、B,补齐四种状态:两者都关、只开 A、只开 B、两者都开。
1interaction(A, B)
2 = [metric(A_on, B_on) - metric(A_off, B_on)]
3 - [metric(A_on, B_off) - metric(A_off, B_off)]这个差分表示 A 的效果是否随 B 的状态改变。先看方向、重复运行范围和工程解释,再决定是否需要更正式的统计分析。NIST 对析因实验的定义同样把交互项与主效应分开;若没有覆盖必要组合,就不能声称已经估计交互。
四、回归矩阵要同时保护性能、正确性和恢复
前面各篇已经给出稳态、热点失效、写入失败、消息重复、慢消费、慢下游和突发流量等场景。全量回归不需要把所有参数做笛卡尔积,应挑选能够覆盖核心承诺和已知风险的代表场景。
| 场景 | 主要输入 | 性能与资源观察 | 正确性或稳定性门禁 |
|---|---|---|---|
| 正常业务混合负载 | 固定请求比例、数据分布与稳态到达率 | 完成吞吐、P95/P99、CPU、线程、连接、数据库与缓存成本 | 业务终态互斥、结果集与权限回归通过 |
| 安全区边缘与突发 | 阶梯升压、短突发、持续过载、降载 | 放行量、拒绝、队列年龄、尾延迟、资源饱和 | 核心写入不伪造成功,恢复后回到基线范围 |
| 热点缓存失效 | 固定热点键失效与并发读取 | 回源数、数据库峰值、单飞等待与内存 | 不同键不互阻,缓存偏差在契约内 |
| 异步重复与积压 | 重复投递、慢消费、持续失败 | 主工作/重试速率、最老事件年龄、清空时间 | 不丢事件、不重复副作用、未知态可查询 |
| 慢下游与结果未知 | 延迟、超时、响应丢失和恢复 | 隔离舱占用、残留任务、核心请求 P99 | 同一操作标识可对账,故障不扩散到无关路径 |
每个单元格都应引用已有文章的实验配置,而不是重新发明另一套参数。正式执行记录可以这样组织:
1regressionMatrixRun:
2 matrixVersion: ${VERSION_OR_DIGEST}
3 scenario: ${SCENARIO_ID}
4 variant: ${S0_S1_SINGLE_FINAL_OR_INTERACTION_CELL}
5 block: ${ENVIRONMENT_TIME_OR_DATA_BLOCK}
6 randomizedOrder: ${ACTUAL_SEQUENCE_POSITION}
7 artifactAndChangeStates: ${REFERENCES}
8 datasetAndCacheStart: ${REFERENCES}
9 warmupEvidence: ${REFERENCE}
10 runId: ${RUN_ID}
11 valid: ${TRUE_OR_FALSE}
12 invalidReason: ${NONE_OR_REASON}
13 outcomes:
14 throughputAndTerminalCounts: ${RAW_SUMMARY_REFERENCE}
15 latencyDistributions: ${RAW_SUMMARY_REFERENCE}
16 resources: ${QUERY_OR_FILE_REFERENCES}
17 correctness: ${TEST_AND_RECONCILIATION_REFERENCES}
18 recovery: ${DRAIN_AND_RETURN_TO_BASELINE_REFERENCE}
19 rawArtifacts:
20 location: ${PATH_OR_STORAGE_REFERENCE}
21 checksums: ${CHECKSUMS}无效运行也要保留。压测端掉速、快照校验失败、监控缺口或共享依赖受干扰时,将该轮标成无效并重跑,不能从有效和无效轮次中挑出更符合预期的结果。
五、贡献表要保留波动、交互和代价
每个变体重复多少次、怎样汇总、业务上多大变化才值得保留,应在看结果前写进分析计划。先用历史重复运行估计自然波动,再定义项目能接受的实际差异;没有这些材料时,逐轮列出原始结果和范围,结论保持“待补样本”,不要制造显著性标签。
最终贡献表可以采用下面的结构:
1changeContribution:
2 changeId: ${CHANGE_ID}
3 comparison:
4 baselineVariant: ${VARIANT}
5 changedVariant: ${VARIANT}
6 scenarioAndBlock: ${REFERENCES}
7 repeatedRunIds: ${RUN_IDS}
8 observed:
9 completedThroughput: ${PER_RUN_VALUES_AND_SUMMARY}
10 p95P99AllCompleted: ${PER_RUN_VALUES_AND_SUMMARY}
11 terminalCounts: ${PER_RUN_VALUES_AND_SUMMARY}
12 applicationAndDependencyResources: ${REFERENCES}
13 businessCorrectness: ${PASS_FAIL_AND_EVIDENCE}
14 recoveryTime: ${PER_RUN_VALUES_OR_NOT_APPLICABLE}
15 attribution:
16 singleEffect: ${VALUE_DIRECTION_AND_SCOPE_OR_NOT_ESTIMATED}
17 cumulativeMarginal: ${VALUE_DIRECTION_AND_ORDER_OR_NOT_ESTIMATED}
18 interactions:
19 - with: ${CHANGE_ID}
20 evidence: ${FOUR_CELL_RUN_REFERENCES}
21 result: ${REINFORCES_OFFSETS_OR_UNCLEAR_WITH_REASON}
22 cost:
23 memoryStorageConnections: ${MEASURED_VALUES}
24 consistencyAndOperationalWork: ${EVIDENCE}
25 limitations:
26 - ${UNCONTROLLED_FACTOR_SMALL_SAMPLE_OR_MISSING_METRIC}
27 decision: ${KEEP_ADJUST_ROLLBACK_OR_MORE_EVIDENCE}
28 reason: ${EVIDENCE_BASED_REASON}P95/P99 不要把每轮百分位数取平均后冒充合并样本的百分位数。保留各轮分布摘要;需要合并时使用同口径的原始请求样本并记录方法。错误请求也继续进入 01-01 定义的主延迟口径,不能因为最终版增加了明确拒绝,就只看成功请求延迟。
所谓“统计显著”也不能靠图形看起来分开。样本量、重复方式、独立性和分析方法尚未确定时,本文只做工程判断:结果是否越过预先定义的业务门槛,是否超出基线自然波动,是否在重复运行与不同区组中保持方向一致。若决策风险需要正式推断,应在执行前确定模型和样本计划,再由具备相应能力的人复核;不能看完结果后挑一种检验方法。
六、无收益和失败结果也要进入最终决策
某项改造可能降低数据库 QPS,却没有改善业务延迟;也可能在正常流量下有效,却在恢复阶段制造更长积压。这样的结果不能从最终报告里删掉。
决策至少有四种:
KEEP:正确性门禁通过,目标场景收益可重复,代价在已确认边界内。ADJUST:方向有价值,但参数、作用域或恢复行为仍需修改。ROLLBACK:没有达到实际收益门槛,或引入不可接受的正确性、资源和运维代价。MORE_EVIDENCE:比较条件、样本或观测不足,暂时不能归因。
回退结论要引用具体版本、开关、数据兼容要求和处理中工作。缓存可以关闭,新增索引还要评估 DDL;异步消费者可以停,但 Outbox、去重记录和未完成状态不能删除;限流参数可以恢复,明确拒绝和容量指标应继续保留。06-02 会把这些操作整理成交付包,本篇只给出每项变化保留还是回退的证据。
一轮可信的全量回归,最终应能回答:最终组合是否在正常、峰值和故障场景满足承诺;每项改造独立改变了什么;加入现有组合后还剩多少边际效果;哪些变化互相增强或抵消;失败时怎样安全退回。回答不了的部分保留为限制或待补实验,不用一张“优化前后对比图”替它们下结论。