多环境管理:dev/staging/prod 的测试环境治理

“这个环境是谁动过?”“staging 怎么和 prod 不一样?”“我本地好的,怎么一上环境就挂?”——环境问题是团队协作里最阴性的成本:它不产生 bug 单,但消耗每个人每天 30 分钟的排查时间。2026 年把多环境管理做成了一套显式机制,这篇讲四个核心动作。

一、环境不是"一台服务器",是"一份声明"

混乱的根源是把环境理解成"那台机器"。正确模型:环境 = 配置声明 + 数据声明 + 依赖声明,三者全部版本化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# envs/staging.yaml(进 git,变更走 MR)
name: staging
deploy:
git_ref: release-2026.07 # 部署的代码版本(由发布系统写入)
image: registry/app:20260710-a3f9c21
config:
base: shared.yaml # 基础配置(端口、超时、日志级别)
overrides:
ws_mock: enabled # staging 特有:Mock WS 供帧
data_pool: staging_shared # 数据池声明
dependencies: # 依赖环境(声明式,启动时校验)
- name: user-service
version: 2026.07-rc2
health: /healthz
data:
seed_version: seed-2026.07.03 # 数据快照版本(见"数据工厂"那篇)
refresh: weekly # 每周一从 prod 脱敏同步

核心收益:环境状态从"登录机器看"变成"读一个文件"。两个环境的行为差异 = 两份 yaml 的 diff,git blame 直接回答"谁在什么时候改了什么"。

二、环境指纹:每个测试报告必须携带

"我本地好的,环境上挂"的终结方案:测试运行时采集环境指纹,写进每一份报告

1
2
3
4
5
6
7
8
9
10
{
"env": "staging",
"fingerprint": {
"app_sha": "a3f9c21",
"config_hash": "9c2e17ab", // envs/staging.yaml + shared.yaml 的 hash
"seed_version": "seed-2026.07.03",
"deps": { "user-service": "2026.07-rc2" },
"collected_at": "2026-07-10T19:50:00+08:00"
}
}

排查时第一动作不是"复现",而是对比指纹

  • 失败 run 的 config_hash ≠ 预期 → 有人改了环境配置(blame 配置 MR);
  • seed_version 落后 → 数据漂移(该周刷新没执行);
  • 某个 dep 的 version 和计划不符 → 依赖被单独部署了。

指纹把"环境问题排查"从 2 小时玄学变成 5 分钟 diff。指纹采集是 pytest 的 session 级 fixture,一行 env_fingerprint.collect() 的事。

三、环境漂移检测:每天对账,不等出事

配置会"漂移":有人登录机器手改了 Nginx、直接 kubectl set env 覆盖了声明值。漂移检测 = 声明 vs 实际每日对账:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 每日 08:00 跑(CI job)
def drift_check(env):
declared = load_env_yaml(env)
actual = probe_actual(env) # 拉取:镜像 sha / 配置项 / 依赖版本 / 数据版本号
diffs = []
if declared.deploy.image != actual.image:
diffs.append("image 漂移:声明 %s 实际 %s" % ...)
for k in declared.config.overrides:
if declared.config.overrides[k] != actual.config.get(k):
diffs.append(f"config {k} 漂移")
if diffs:
notify_drift(env, diffs) # 告警 + 自动回滚工单
return 1
return 0

对账失败的两条出路:要么"声明错了"(补 MR 修正声明),要么"实际错了"(回滚机器)。两条路都强制收敛回"声明即事实"——漂移检测的价值不在报警,在于逼着每次变更都过 git

四、环境的生命周期:谁建、谁养、谁拆

环境资源(尤其 staging)最常见的浪费是"僵尸环境":项目结束了环境还在跑,占了 8C16G 三个月。生命周期四问写进平台:

问题 机制
谁建? 环境申请走平台(填声明 yaml),自动开资源 + 写权限
谁养? 声明里的 owner 字段,漂移告警/哨兵失败 @ owner
谁拆? 闲置检测:连续 14 天无测试 run + 无登录 → 冻结(关流量保数据);再 14 天 → 销毁(数据先备份 30 天)
例外? 长稳专用环境打标 exempt: soak,按长稳节奏豁免闲置检测

冻结比销毁重要:环境销毁后重建要 2 小时(数据、证书、网络白名单),冻结(保留资源、关入口)成本是 0。"先冻后销"把误杀成本降到最低——真要用,一键解冻。

五、一个容易忽略的点:环境的"语义"要统一

最后一条最软但最值钱:每个环境能测什么,要全员共识。我们的约定:

  • dev:单服务联调,数据随便造,不保证依赖完整性;
  • staging:全链路 + Mock 外部依赖(WS/支付/短信全走 Mock),功能结论的合法出处
  • prod-canary:只读流量 + 金丝雀,性能/容量结论的合法出处
  • prod任何测试动作需审批,只允许白名单的只读探针。

约定落在平台里:CI profile 声明目标环境,dev 环境的"全链路回归"跑不出来(依赖声明校验不过)——用机制固化语义,比培训十遍都管用。

六、小结

环境治理四个动作:声明化(环境 = 版本化的 yaml,不是机器)、指纹(每份报告带环境指纹,排查变 diff)、对账(每日漂移检测,逼变更过 git)、生命周期(owner + 闲置冻结销毁)。环境问题的本质是"状态不可见",四个动作都在把状态变成可见、可追溯、可问责的东西。