业务系统性能改造手册 · 验收交付

做一次可归因的全量回归

2026-08-052 min read验收交付
摘要

在复核实验协议的基础上,用可复现版本、单项与累积实验、针对性交互实验和业务正确性检查,解释组合改造的贡献、代价与回退依据。

做一次可归因的全量回归

从 02-01 到 05-02,订单系统先后减少请求、缩小 SQL 工作量、批量访问、调整并发预算、拆分异步链路,又补上超时、隔离和入口容量保护。现在把所有开关打开,与最初版本各跑一轮,最终版确实更快,能否据此给每项改造记功?

不能。缓存可能遮住 SQL 改造的收益,批处理可能改变连接持有时间,异步化也可能只是把耗时移出响应窗口。实验若总按“旧版在前、新版在后”的顺序执行,缓存预热、数据增长和机器时段也会混进版本差异。

小编现阶段的判断是:全量回归的任务不是证明最终版本更快,而是说明哪些变化在什么条件下产生了什么结果,又付出了什么代价。 本文只设计统一验收与归因,不再重讲各项改造怎样实现。真实环境和数据尚未提供,所有结果字段保持待填。

一、先证明今天的实验仍与基线可比

01-01 的基线协议到了最终回归阶段仍然有效,但不能机械复制旧文件。几个月的改造过程中,Java 版本、依赖镜像、数据快照、压测脚本甚至指标查询都可能变化。第一步是生成一份“基线协议差异单”。

yaml
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 已关闭。

为每项改造建立状态清单:

yaml
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 路径,重试治理需要可靠事件身份,限流实验也承接已有的有界资源舱。这类单项实验应固定最小前置状态,并比较“前置状态”与“前置状态 + 目标改造”;贡献表同时记录这组参考状态,不能把结果记成目标改造脱离前提后的独立收益。

单项贡献写成成对比较:

text
1effect(A | prerequisites) 2 = metric(prerequisites + A) - metric(prerequisites)

差值的好坏方向取决于指标。延迟和错误通常越低越好,完成吞吐在正确性成立时越高越好,内存与运维成本则属于代价。不要把不同单位合成一个综合分。

3.2 累积实验回答“按上线顺序加入后还剩多少收益”

按真实依赖顺序逐项加入改造:S0 → S1 → S2 ... → Sfinal。每一步只新增一个变化,并从干净快照运行。某项在单独启用时有效,加入已有组合后边际收益可能缩小;这不一定说明它无用,可能是前一项已经消除了同一部分工作。

text
1marginal(A after S) = metric(S + A) - metric(S)

累积顺序会影响边际值,所以报告必须写明顺序和选择理由。不要把这一条顺序中的边际贡献解释成改造的永久属性。

3.3 针对性交互实验回答“两个变化是否互相增强或抵消”

全部改造做完整组合会迅速膨胀,通常没有必要。先根据架构关系和单项、累积结果选择怀疑存在交互的组合,例如缓存与请求合并、批处理与连接预算、异步化与限流。对两个二值改造 A、B,补齐四种状态:两者都关、只开 A、只开 B、两者都开。

text
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同一操作标识可对账,故障不扩散到无关路径

每个单元格都应引用已有文章的实验配置,而不是重新发明另一套参数。正式执行记录可以这样组织:

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

无效运行也要保留。压测端掉速、快照校验失败、监控缺口或共享依赖受干扰时,将该轮标成无效并重跑,不能从有效和无效轮次中挑出更符合预期的结果。

五、贡献表要保留波动、交互和代价

每个变体重复多少次、怎样汇总、业务上多大变化才值得保留,应在看结果前写进分析计划。先用历史重复运行估计自然波动,再定义项目能接受的实际差异;没有这些材料时,逐轮列出原始结果和范围,结论保持“待补样本”,不要制造显著性标签。

最终贡献表可以采用下面的结构:

yaml
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 会把这些操作整理成交付包,本篇只给出每项变化保留还是回退的证据。

一轮可信的全量回归,最终应能回答:最终组合是否在正常、峰值和故障场景满足承诺;每项改造独立改变了什么;加入现有组合后还剩多少边际效果;哪些变化互相增强或抵消;失败时怎样安全退回。回答不了的部分保留为限制或待补实验,不用一张“优化前后对比图”替它们下结论。

参考资料