你大概也遇到过这种早晨:例行滚动升级节点后,支付链路的 Pod 全部开始 502。镜像标签没变,应用版本没变,部署日志跟上周一模一样,Pod 健康,CPU 平稳,连支付提供方的健康检查都正常。你重启、回滚,查服务商状态页,最后发现罪魁祸首是一个只存在于刚被替换掉的旧节点上的环境变量。
这不是个例,而是配置漂移(configuration drift)的典型面目——某个配置值只存在于运行中的环境里,代码仓库里没有,于是任何一次重新创建实例的操作,都会把系统打回仓库存档的“原始版本”。
漂移是怎么累积的?很少是一刀切的大改动。更常见的是:有人在控制台滑了一下滑杆,有人对运行中的 Deployment 执行了 kubectl set env,有人因为 staging 环境值不对往某个 task definition 里塞了个变量,有人直接改了服务器上的 cron 表因为仓库版本有拼写错误。每次改动都很小,当下都有充分理由,但对当时不在场的人来说完全隐形。账单在很久以后送到,而且送到别的地方。
为什么这类故障特别难排查?因为平时依赖的信号全部失效。代码没变,部署日志干净;镜像标签没变,镜像仓库告诉你什么都没改;配置仓库没人碰,你以为的 diff 是空的。真正变的是代码脚下的“地基”——节点轮转、扩容、实例刷新、区域故障转移,这些操作都会用仓库版本去构建新实例,而不是沿用运行环境的版本。旧实例还带着热修,新实例没有。于是你会短暂地运行一个混合舰队,同一服务后面的两个 Pod 对同一个请求给出不同响应——这是问题最坏的形式。
症状通常表现为“硬故障+软解释”:一个明明已经消失的超时又复活了;功能开关自己关掉;连接池悄悄缩回原大小。如果你发现自己说“旧 Pod 上是好的”,那你就是在看漂移。
唯一能发现漂移的比较,是“运行状态”对“提交状态”。文档描述的是写文档那一刻的意图,写的人说不定已经离职。仓库才是源头,其他都是道听途说。所以要让运行环境自己描述自己,再跟仓库 diff。
具体做法:
```bash
# 导出实时环境变量,跟已提交的环境文件对比
kubectl exec deploy/payments -- env | sort > /tmp/live.env
grep -E '^[A-Z_]+=' config/payments.env | sort > /tmp/repo.env
diff -u /tmp/repo.env /tmp/live.env
```
关键在“定时跑”,不是着火时才跑。live.env 里有而 repo.env 里没有的、或者值不一致的,都是发现项。同样的方法适用于数据库参数、功能开关默认值、控制台管理的基础设施。
看这个例子:仓库里写的是 `clientTimeoutMs: 8000`,控制台热修改成了 20000。仓库还是 8000,所以每次从仓库构建出来的 Pod 都拿到 8000,而服务商依然慢——502 必然复现。
想根治,规则只有一条:**如果仓库里没有,它就不存在**。只存在于运行进程里的设置不是配置,是一次有保质期的未文档化变更,保质期就是下次重启。
紧急逃生门当然要有:凌晨两点手改一个值有时是正确的决定。但逃生门有价格——每次手改必须在同一班次内开一个针对仓库的 commit,链接到事件频道,理由写进消息。事故单直到 commit 合并才算关闭。没有 commit,就不算修好。这样一来,漂移就出现在人们唯一会看的地方:评审。评审者看到了改动,下一位运维工程师继承的是原因而不只是数值,下一次节点轮转部署出来的环境就能匹配正在运行的那个——因为正在运行的从来不是秘密。
这个思路能直接用在你的日常里:任何你有控制台权限、又同时管着代码仓库的系统,都值得做一次“运行环境自述 vs 仓库”的定时 diff。从环境变量开始,扩展到 feature flag、数据库参数、K8s 对象定义。要注意别把动态生成的配置(比如从 secret 注入的值)也纳入比较,否则噪声会淹没真信号。更重要的是,把“仓库没有就不存在”变成团队约定,让每一次手改都自动触发一个 commit 欠账。你不需要专门的漂移检测工具,一个 cron + diff 就能抓住大部分引信很长的事故。

内容与图片版权归原作者所有 · 原文: https://dev.to/_66d02d0cc1ece7d1137c5f/configuration-drift-is-a-production-incident-with-a-long-fuse-23b7