改了 ≠ 生效了:为什么“改完了“不是完成判据

改了 ≠ 生效了:为什么“改完了“不是完成判据 改了 ≠ 生效了为什么改完了不是完成判据主题变更的生效性验证部署 · 配置先说结论有一次我修完一个生产缺陷重建镜像、重启服务、重跑验证 ——错误又出现了。代码是对的本地跑也是对的。问题出在更朴素的地方我改的那一份不是在线上真正干活的那一份。后来我想明白改完了这个判断之所以经常是错的是因为我用来确认它的证据几乎全都来自提交侧命令跑完了、重启成功了、接口返回true了。而这些证据只能证明我提交过证明不了谁在按它执行。这篇文章只讲两个入口 ——变更落到了几个实例、改的是不是被订阅的那一份以及一个可以复用的完成判据不看提交侧只看生效侧。一、入口一变更落到了几个实例现象一个热点写服务下单/秒杀链路有个数据写错的缺陷。我定位到根因、改代码、重新构建镜像、滚动重启然后重跑之前的验证脚本 ——错误一模一样地复现了。第一反应是我改错了于是回去逐行读代码。代码没问题。真相这个服务在生产上是双实例主实例 部署在另一台机器上的副本两个实例都连同一个消息队列消费同一类消息属于竞争消费┌──────────────────────────┐ 消息 ──────────► │ 消息队列一个队列 │ └───────────┬──────────────┘ │ 谁先抢到谁处理 ┌──────────────────┴──────────────────┐ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ 实例 A我改了 │ │ 实例 B旧版本 │ │ 新 jar ✅ │ │ 旧 jar ❌ │ └─────────────────┘ └─────────────────┘我只重建了实例 A实例 B 还在跑上一个版本的 jar。而这次验证触发的那条消息恰好被实例 B 消费了—— 跑的还是旧逻辑。证据很好找实例 B 的日志里有一条处理记录时间戳和我这次验证完全对得上。关键认知部署完成不是一个布尔值而是一个概率。只要这个服务有副本、且副本和主实例竞争消费同一队列那么线上跑的是哪个版本就变成了按消息来分的问题 —— 一半的消息走新逻辑、一半走旧逻辑而你随手验的那一次未必落在你想要的那一半上。处置两个实例换成同一份 jar并把副本的差异收敛成两个环境变量服务端口 链路追踪的服务名不再单独维护一套镜像 —— 从根上消灭两个版本并存的土壤。把部署完成从一句话变成一条命令逐实例打印四个事实 —— 容器内 jar 的指纹、宿主上 jar 文件的指纹、容器状态、健康检查返回码四者全绿且两个实例指纹相同才算完成。# 逐实例核对每个实例都跑两台指纹必须一致forcin实例A容器实例B容器;doecho-n$c容器内: ;dockerexec$cmd5sum /app/app.jarecho-n$c宿主文件: ;md5sum /data/app/jars/服务.jardockerinspect-f{{.State.Status}}$cdone⚠️顺带一个坑副本镜像的 tag 别写死日期。很多构建命令会覆盖同名 tag于是我明明构建了新版本这句话可能是假的 —— 你会拿到旧镜像层。共用镜像 环境变量差异比维护两套镜像便宜得多。二、入口二改的不是被订阅的那一份现象做并发压测前我要把一条限流阈值从 5 临时调大到 200。为了不改代码、不重启我走配置中心的热更新POST提交配置 → 返回true✅再GET读回来 →确实是 200✅然后压测跑起来成功吞吐恒定在 5一点没变。阈值看起来改成功了行为上跟没改一样。真相我改的不是应用订阅的那份配置。应用订阅的 dataId 是xxx-flow-rules没有扩展名而我提交的是xxx-flow-rules.json—— 因为仓库里那个文件就叫这个名字。于是我在配置中心里新建了一个同名的影子配置应用侧从头到尾没收到过通知。最直接的证据在消费侧应用的变更通知日志里规则 md5一直是旧的把它和配置中心上那份新内容的 md5 一对比差得明明白白。关键认知配置中心返回成功只说明它存下了不说明应用用了。这是提交侧 / 生效侧最容易混为一谈的一处因为配置中心既存配置、又能回读所以它的成功响应看起来就像一次端到端的确认 —— 但它确认的只是它自己那一侧。处置按应用配置里声明的 dataId逐字提交对照应用自己的配置项不是对照仓库文件名。提交后在消费侧取证应用侧的通知日志出现变更记录、md5 与预期一致。把影子配置当场删掉—— 否则它迟早会被下一个人包括未来的自己改到。 还有一个便宜的探针改完先跑最小的一档。阈值如果真的生效了最小的那一档就该变不变就停下来查生效性别把整轮阶梯都白跑一遍。三、四句可以复用的取证方式两个入口合起来其实就是一条原则完成判据只能落在生效侧。换了别的变更类型判据也跟着换变更类型到哪取证生效侧不要看什么提交侧版本 / 部署逐实例的 jar 指纹容器内 宿主文件必须一致“重启成功了”、“容器 running”配置消费侧的变更通知日志 / 行为指标配置中心的返回码、“我 GET 到了”数据 / 清理前后基线比对行数 关键SUM脚本自己打印的✅ 校验通过流程 / 编排对生产最终状态的独立复核脚本自己的输出、退出码四、可以抄走的 5 条自查我的服务有几个实例部署脚本是逐台核对指纹还是重启完就宣布完成副本和主实例是同一个镜像吗镜像 tag 里有没有写死的日期我改的 dataId / key是从应用配置里抄的还是从仓库文件名里猜的改完配置我去应用日志里确认它收到了吗服务端返回 200 ≠ 生效最后一次变更完成我是靠脚本的输出确认的还是靠对生产最终状态的独立取证确认的结语这两个坑的技术含量都不高 —— 没有一个需要读框架源码。它们的可怕之处在于它们是沉默的命令跑完了、返回true了、容器也起来了 ——提交侧的一切都在说成功了。所以现在我不再问自己我改了吗只问一句“生效侧现在是什么”