灰度判定接好了,名单在库里,切过去的会话跑起来一切正常。真集群上点了一次取消,接口返回成功,任务继续跑。另一组对外接口自己写了一份取消实现,它不看这次请求开头算出来的走哪边,直接去查容器。我的判断是:灰度能覆盖多少,由系统里同一个判定的副本数决定——漏掉一份,那条路径上的灰度就是假的。
这次灰度换的是任务跑在哪里,不搬数据
一条链路原来是每个会话起一个容器,要灰度切到一个固定的常驻服务。
约束先定下来:
- 灰度名单只认数据库里的一个组合键。配置只提供地址,不再有环境变量名单。
- 选择发生在每一轮新对话开始时,一轮开始之后不许中途切。
- 历史会话一样切。会话是持久的业务身份,不归属于某个实例。
- 原位续聊,不搬数据。两边用同一个会话标识、读同一个检查点、同一份文件视图,不做批量迁移,也不从审计日志重建上下文。
命中名单时跳过整套容器生命周期:不建容器、不复用容器、不落绑定记录。地址为空或者连不上,一律显式失败,禁止回落到容器解析器。
最后这条禁令是专门写的,守的是新路径。它守不到没走新路径的那些调用。
取消点下去,接口返回成功,任务还在跑
聊天接口接上了灰度判定,部署到真集群验证。点取消。
接口返回成功,任务照常跑完。
那一组对外接口有自己独立的取消实现,面向的也是同样这两类部署。它没看这次请求开头算出来的走哪边,自己又去查了一遍容器。灰度会话没有容器可查,上一节那条约束就是这么写的。
不崩溃,所以不会出现在任何一条错误日志里。症状只有一条:取消不生效。改法是让它读这次请求开头已经算好的那个结果,自己不判。
第二处是文件接口。成果面板里文件能看到,因为那份列表靠事件透传,不经过本服务解析。点预览就是 404——算路径的那个函数仍然只按旧链路的目录结构算。这一处原计划写的是「等需求出现再排期」,结果有人真的点了预览,排期还没开始。
两处都是在真集群上撞出来的。本地验证打的是文件、会话、健康这几个接口,没有真实业务流量,这条写在文档的验证盲区一栏里。
按接口写的测试问不出这种缺口。取消接口该测的是「能不能取消」,这件事它一直做得对。缺的那一问是「这条路径有没有读过这次请求算出来的走哪边」,它不落在任何一个接口头上。
要搜的是被开关替换掉的那个动作
搜灰度开关的名字没用。搜出来的全是已经接上的地方,而漏的那些,定义上就是没出现过这个名字的地方。
要搜的是开关替换掉的那个旧动作。这次灰度做的事是「不建容器、不查容器」,于是要数的是「查容器」:解析容器地址的函数、拼容器名的地方、取容器的那个客户端。每个调用它的地方算一个入口,逐个问一句——这条路径读没读这次请求算出来的走哪边。
结构上的做法是把判定收成一个值:一次请求开头解析一次,往下传。同一次请求的聊天、取消、文件三个接口共用这一个值,中途不许改,谁都不许在同一轮里重新读名单。
重新读会出两种不一致:这一轮开头走了常驻服务,中间名单被改,取消发去容器那边;或者前后两次读到不同结果,同一轮的文件视图和聊天上下文分别落在两条链路上。
同一个手法在另外几处用过,都是先数清楚有几个调用方,再修在它们汇聚的那一处:
- 一处远程执行命令的安全校验,放在起子进程的那个函数里抛错。创建、更新、连通性测试、发现、后台定时任务全部经过它。只在写入口拦不够,实测有两条路绕过去了,其中一条是更新接口的校验只在传特定字段时才触发,只传一个开关字段两个分支都不走。
- SSE 的收尾最初想插在主处理函数里,追踪之后发现至少 4 个成功出口分支,改成挂在所有分支最终都经过的那一个落盘函数上。
- 未声明的查询参数一律 422,校验挂在 router 级依赖上,新增端点自动纳管。
「至少 4 个」那句是重点。抽象点该放哪里不难想,难的是先把分叉数清楚。
代价,和没做的那一步
这个值要穿过所有下游调用的签名,每一层多一个参数。新加的接口忘了接它,照样漏。
文档里为此立的是一条规矩:后续再发现「独立实现忘了读这个值」的地方,按同一模式处理。规矩不是机制,会被忘。真要堵死,得让「查容器」这个动作在拿不到这个值的时候根本调不通,这一步没做。
一轮开始后不许中途切也有代价。把一个会话从名单里拿掉,正在跑的那一轮还是走老路,要等下一轮才换过来。
最后一条:我没把系统里所有读容器的地方数成一份完整清单。上面两处是撞出来的,不是搜出来的,所以我说不出还剩几处。