Trace ID、MDC 与上下文传播:关联标识怎样跨越异步边界
关联不是复制字符串,而是有作用域的上下文所有权。
Logback 的 MDC 手册 解释线程关联上下文;OpenTelemetry Java 的 Context API 定义跨调用栈、线程和进程传播的不可变上下文。
故障现场:入口线程写入 MDC 后把任务提交到线程池:工作线程看不到 traceId;若简单 put 而不 restore,复用线程又把上一个租户标识带给下一请求。
入口线程写入 MDC 后把任务提交到线程池:工作线程看不到 traceId;若简单 put 而不 restore,复用线程又把上一个租户标识带给下一请求。 先画出事件从业务动作到查询结果的完整路径,逐段记录输入、输出、队列水位、丢弃策略和关联键。最危险的不是显式报错,而是应用成功、遥测静默缺失;缺失必须变成可告警状态,不能用“平台偶尔延迟”长期解释。
权威状态与观测结论必须分开
入口只信任符合格式和策略的标准 Trace Context;业务 requestId 可另建,但不能替换 traceId。跨消息重试沿用业务 operationId,消费处理 Span 可用 Link 表达因果。 对每个结论列出反证:字段缺失时还能否区分业务拒绝与系统异常,关联断开时能否从消息或 operationId 恢复,采样后统计是否仍代表总体。信号只能证明其观测窗口内发生了记录,不能证明未记录的事件不存在。
建模主链与边界条件
Ingress extract
在 Ingress extract 阶段固定 owner、输入、输出、失败原因和容量上限。正向用例证明正常状态产生唯一且稳定的证据;反向用例分别删除上下文、制造未知字段、填满队列或扩大标签空间,断言缺口被计数且不会污染业务线程。字段名和结果枚举一旦进入告警与查询就是契约,修改必须兼容迁移。
Validate / create
在 Validate / create 阶段固定 owner、输入、输出、失败原因和容量上限。正向用例证明正常状态产生唯一且稳定的证据;反向用例分别删除上下文、制造未知字段、填满队列或扩大标签空间,断言缺口被计数且不会污染业务线程。字段名和结果枚举一旦进入告警与查询就是契约,修改必须兼容迁移。
Scope attach
在 Scope attach 阶段固定 owner、输入、输出、失败原因和容量上限。正向用例证明正常状态产生唯一且稳定的证据;反向用例分别删除上下文、制造未知字段、填满队列或扩大标签空间,断言缺口被计数且不会污染业务线程。字段名和结果枚举一旦进入告警与查询就是契约,修改必须兼容迁移。
Async capture
在 Async capture 阶段固定 owner、输入、输出、失败原因和容量上限。正向用例证明正常状态产生唯一且稳定的证据;反向用例分别删除上下文、制造未知字段、填满队列或扩大标签空间,断言缺口被计数且不会污染业务线程。字段名和结果枚举一旦进入告警与查询就是契约,修改必须兼容迁移。
Exit inject
在 Exit inject 阶段固定 owner、输入、输出、失败原因和容量上限。正向用例证明正常状态产生唯一且稳定的证据;反向用例分别删除上下文、制造未知字段、填满队列或扩大标签空间,断言缺口被计数且不会污染业务线程。字段名和结果枚举一旦进入告警与查询就是契约,修改必须兼容迁移。
Scope restore
在 Scope restore 阶段固定 owner、输入、输出、失败原因和容量上限。正向用例证明正常状态产生唯一且稳定的证据;反向用例分别删除上下文、制造未知字段、填满队列或扩大标签空间,断言缺口被计数且不会污染业务线程。字段名和结果枚举一旦进入告警与查询就是契约,修改必须兼容迁移。
从用户问题反推信号,而不是从采集能力出发
一条信号必须拥有完整生命周期
生命周期从调用点开始,经过过滤、上下文绑定、聚合或排队、编码、导出、存储、查询,最后进入告警和处置。任何一段都可能丢失:日志被级别或 AsyncAppender 丢弃,MDC 在线程切换后消失,指标被错误标签拆成海量序列,Span 因父上下文未注入成为孤儿,Exporter 队列满而只增加自身丢弃计数。只验证应用执行过埋点 API,无法证明信号可用。
基数、体积和保留期是运行时资源
敏感数据和攻击面进入信号设计
用故障注入验证信号而不是看一次漂亮页面
查询、告警与恢复必须共享同一语义
两个 Java 17 模型固定可重复见证
模型不依赖日志平台、指标后端或 Collector,只压缩该主题最关键的状态转换和量化判据。真实工程用 SLF4J、Logback、Micrometer 或 OpenTelemetry 驱动同一不变量。
javac --release 17 -Xlint:all -Werror examples/backend-development/observability/trace-mdc-context/ContextScopeDemo.java examples/backend-development/observability/trace-mdc-context/ExecutorPropagationDemo.java
java -cp examples/backend-development/observability/trace-mdc-context ContextScopeDemo
java -cp examples/backend-development/observability/trace-mdc-context ExecutorPropagationDemoouter=trace-a inner=trace-b restored=trace-a after=NONE leak=false
submitted=trace-17 workerBefore=NONE workerDuring=trace-17 workerAfter=NONE propagated=true输出变化必须能归因于信号契约、容量策略或业务规格,不能通过删除字段、扩大采样或无限重试吸收差异。
容量、发布与恢复门禁
传播载荷只保留 trace context 与经过治理的 baggage;每个 baggage 字段都会穿过网络并进入下游,必须限制键、长度、来源和保留期。 发布前用峰值输入和故障放大系数计算容量,在预发布环境制造后端慢、连接中断和停机,验证业务延迟、遥测缺口和恢复时间。从手写 MDC 迁移到 Observation/OpenTelemetry 时先保留桥接层,比较入口、线程池、HTTP 和消息四类边界的关联完整率,防止出现两套互不相认的 traceId。
门禁同时检查信号存在、语义正确、关联完整、基数受控、敏感字段缺失和故障时可恢复。围绕“上下文在入口建立、跨边界显式传播并在作用域结束恢复,既不丢失也不污染下一任务”记录 owner、预算、保留期、告警消费者和最近一次真实发现;没有消费者且不能回答诊断问题的信号应删除,而不是永久堆积。
