2403 字
约 8 分钟
14
S27-A22 · 复现包:别人为什么跑不出你的数

一次压测报了"P99 280ms",另一个同事拿同样的脚本在自己机器上跑出 620ms。两人对着日志核对了半天——脚本一样、参数一样、目标服务一样,而结果差一倍。最后发现:一个人的机器上"压测工具"是 4 线程,另一个是默认单线程。

实测的"可复现"不是"给脚本",而是"给一个能一键跑起来的东西"。这个"组成"很容易被简化成"我给你脚本":而"脚本"只承载了五分之一的信息——其它四项(环境、数据、判定、记录)都在"作者的脑子里"。
而这个东西的组成比多数人以为的多:脚本、参数、环境、数据、以及"怎么判断跑对了"。


1. 复现包的五件套

项 缺了会怎样
脚本 + 参数 手动补参数,容易不同
运行环境(工具版本/机器规格) 数不同但不知道为何
数据(输入与数据规模) 结果不可比
判定标准(怎么算"跑对") 跑完了也不知道是否一致
记录(时间/提交号) 无法回溯

第四项最被忽略:没有"判定标准"时,"跑出来了"与"跑对了"是两件事第四项的"缺失"有个典型后果:对方跑出"完全不同的数",而双方都认为自己对——因为"对"的定义从未被写下来。
——于是"对方说他的数不一样"变成了"各说各话"。

2. "一键跑"的三种形态

形态 成本
脚本 + README 低(易漂移)
容器镜像(含工具与数据) 中(最稳)
编排文件(compose/k8s) 高(含服务依赖)

第二项的"收益"在于"默认值被固定":工具版本、参数、数据都写死在镜像里,于是"没人能"不小心"改了默认值"——而"默认值不同"正是最常见的不可复现来源。
第二项是"最稳的性价比":把"工具、参数、数据样例"都打进镜像,于是"谁跑都是同一套"。

第三项在"需要被测服务"时必要:**用 compose 起"服务 + 数据库 + 压测端"**第三项的"边界"在"真实规模":compose 起的库只有"小数据",压不出"真实数据量下的瓶颈"——所以它复现"功能与配置",幅射不到"容量"。
,于是"环境"也被复现。

// compose 起的"最小复现环境"(三件)
services:
  db:   { image: "mysql:8.0.34", env_file: db.env }
  app:  { build: ".", depends_on: ["db"], ports: ["8080:8080"] }
  bench:{ image: "bench:wrk-4.2.0", depends_on: ["app"], command:
          ["wrk","-t4","-c100","-d60s","--latency","http://app:8080/api"] }
// 收益:一条 docker compose up 就复现"服务 + 压力"

3. "数据"怎么随包走

做法 适用
生成脚本(确定性种子) 大数据量(推荐)
小样本文件(随镜像) 小数据量
导出快照 + 恢复脚本 真实分布

第一项的"验证"很便宜:跑两次生成脚本,比对 md5sum——不同就说明"有非确定性来源"(时间、随机、并发顺序)。
第一项的关键是"确定性":同一个种子生成同一份数据——否则"每次跑数据都不同",结果不可比。

第三项在"必须是真实分布"时用:快照里的数据量要够(否则"压不出瓶颈")第三项的"敏感数据"处理是"脱敏但保形"(字段长度/分布保持,值替换)——因为"形状"影响性能,"值"不影响。
,而它带来"体积与敏感数据"的问题。

// 确定性数据生成的三个要求
1 固定随机种子(seed=42)
2 固定生成规则(字段分布、长度分布、空值比例)
3 固定规模(行数、并发 key 数)
// 判据:同一脚本跑两次,md5 相同

4. "判定标准"怎么写

内容 例子
预期量级 P99 在 200–400ms
关键前置条件 预热 60s、丢弃前 10% 样本
一致性检查 结果条数、校验和
允许的波动 两次自身差异 < 5%

第二项在"JVM/Go"这类"有 JIT 与预热"的服务上影响最大:不预热的前 30 秒可能是"慢 3 倍"——于是"包不包含预热"直接决定"数是不是可比"。
第二项是"最容易被漏"的:"预热"与"丢弃"会显著改变数字,而"没写"就等于"每个人自己决定"。

第四项是"可比性"的前提:先证明"自己跑两次接近",再谈"和别人比"。第四项的"门槛"建议写死:自身两次差异 > 5% 时,"与别人比"的结论不可用——先修"自身波动",再谈"横向对比"。

// 一份"判定标准"(四行,贴进 README)
run: 预热 60s → 采样 300s → 丢弃前 30s
expect: p99 200–400ms, tps 1800–2200
invariant: 成功率 ≥ 99.5%,结果条数与输入一致
self_variance: 两次同环境 p99 差异 < 5%

"复现包"的目录结构应当"自解释"——别人不用问就知道从哪开始:

// 复现包的结构(五项与目录对应)
bench-repro/
  README.md        # 一行命令 + 判定标准 + 预期区间
  bench.sh         # 脚本(第一行 echo 完整命令)
  data/gen.py      # 确定性数据生成(固定 seed)
  env/Dockerfile   # 工具与版本(含参数默认值)
  out/RESULT.md    # 本次记录(提交号/机器/命令原文/数字)
// 判据:新手照 README 一行命令能跑出"可比的数"

5. "记录"最小集

记录 为什么
提交号/镜像 tag 代码与工具版本
机器规格 核数/内存/网络
时间与时区 与"其它变更"对齐
运行命令原文 便于重放

第四项的"自动化"很轻:脚本第一行就 echo 出完整命令(含所有参数),于是"日志里自带记录"。
第四项很实际:"参数在 README 里写着"与"实际敲的命令"常常不同——所以记"原文"。

"数差一倍"时先看"三个默认值",比"核对代码"快得多:

// 三个最容易不同的"默认值"
1 压测工具的线程/连接数(wrk: -t/-c)
2 客户端的 keep-alive(影响是否复用连接)
3 服务端的"预热状态"(是否刚启动)
// 用法:这三项对齐后,绝大多数"差一倍"会消失或变成"差 10%"

6. 排错:五种"复现失败"

现象 成因 处理方向
数差一倍 工具线程/参数不同 容器化 + 显式参数
结果不可比 数据不同 确定性生成
跑完不知道对不对 缺判定标准 写预期与不变量
换机器就不同 规格不同 记录规格 + 同规格对比
过一周就忘了怎么跑 无记录 记录命令原文

第五项的"修复成本"很高:没有记录时,"当时的数"就只是"一个传说"——而它会被引用成"基线"。

最后一个"交付形态"建议:把复现包当成"实测记录的附件",而不是"另一个仓库"。

// 两种交付形态的差别
散落:脚本在 A 目录、结论在文档、数据在某人的机器 → 三个月后无人能跑
打包:一个目录含"脚本 + 数据生成 + 环境 + 结论" → 三个月后一条命令复现
// 判据:三个月后的自己,能不能"不问任何人"重跑一遍

7. 边界:什么情况不必做复现包

场景 为什么 做法
一次性粗测(只看量级) 不需精确 记时间与机器即可
只给自己用 自己记得 简单脚本
差异极大(10 倍级) 工具差异影响小 粗测即可
已在 CI 里固定跑 环境已受控 跟随 CI

判断口径一句话:"这个数会不会被别人引用、或与将来的自己对比"。最后一条与"基线"有关的边界:"基线"不是"一个数",而是"一个数 + 复现它的方法"。只给数不给方法,"基线"在下一次就被质疑——而"质疑"会消耗比"做复现包"更多的时间。
会(基线、对外报数、跨版本对比),就必须给"能一键跑的复现包";不会(一次性粗看),记时间与机器就够。


8. 自测

  1. 复现包的五件套里,哪一项最被忽略?"跑出来了"与"跑对了"的差别是什么?
  2. "容器镜像"为什么是"最稳的性价比"?它在什么情况下还不够?
  3. "确定性数据生成"的三个要求是什么?为什么"随机种子"是关键?
  4. "判定标准"的四个内容里,哪一项最容易被漏?它为什么显著影响数字?
  5. 为什么"记录命令原文"比"README 里的参数"更可靠?
  6. 判断"是否需要复现包"的那个问题是什么?
S27-A22 · 复现包:别人为什么跑不出你的数
https://www.clxhxhhr.top/posts/6613/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。