验收标准(先行):①单线程线程池连续两个请求,第二个不设上下文时必须报错而非读到残留;②请求头清单与文档逐一对应,SDK 注入头全部登记;③全局配置变更可查到操作者、时间与前后值;④客户端版本升级的请求头 diff 在上线前完成确认。
一、问题定义。 「无状态」指单个 HTTP 请求不依赖上一个请求,但调用链四层各有各的记忆:线程层(追踪号、租户标识的池化残留,串号来源,取决于调度、本地难复现)、连接层(认证态粘在复用连接上,池扩缩容引入偶发)、进程层(SDK 全局单例:令牌中控、代理、超时默认值,改一处全站生效)、注入层(SDK 默认请求头、UA、追踪头、重试策略——排查者最常不知道的一层)。
二、五条规范。 上下文 set/remove 收进同一个边界函数,业务代码只调边界;连接不承载业务态,状态显式传参;全局单例变更必留痕,统一封装、写日志、可回放;升级客户端先抓包 diff,逐头登记;读不到上下文当场报错,不带错前行。
三、实现。
_ctx = threading.local() def with_request_context(ctx, action): """唯一允许操作线程上下文的入口:进必 set,出必 remove""" _ctx.current = ctx try: return action() finally: _ctx.current = None del _ctx.current def submit_with_context(pool, ctx, fn): """跨线程提交:显式捕获、任务内重设、结束清理""" pool.submit(lambda: with_request_context(ctx, fn))
实现约束:上下文读取失败抛异常而非返回空值;封装层是对裸 set 的唯一入口,职责边界清晰——边界负责生命周期,业务只声明需要什么。
四、责任边界。 协议层的「无状态」由规范保证;调用链的「无隐式状态」只能由接入方逐层验收。约束清单解决「写在哪」,验收标准解决「何时收回、怎么证明」——两者齐备,偶发串号才有排查路径。
一条可独立摘录的口径是:排查偶发串数据只问四个问题——哪条线程、哪条连接、哪个单例、SDK 带了什么。