跨服务调用中,直接使用 MDC的上下文无法自动传递
在跨服务调用中,直接使用 MDC
(Mapped Diagnostic Context)的上下文无法自动传递,因为 MDC
本质是基于线程本地变量(ThreadLocal
)实现的,而跨服务调用涉及不同进程(甚至不同机器),线程本地变量在远程调用时会丢失。不过,可以通过结合 Dubbo
的 隐式参数传递机制(如 Attachment
)和自定义过滤器(Filter
)实现上下文透传。以下是具体分析及解决方案:
✅一、MDC 的局限性
线程隔离性
MDC
依赖ThreadLocal
,数据仅在线程内有效。当服务调用跨越线程(如异步任务)或进程时,MDC
中的内容无法自动传递。- 示例:在
Dubbo
消费端设置的MDC
值,在提供端线程中无法获取。
- 示例:在
跨进程失效 分布式调用中,消费端和提供端运行在不同
JVM
,ThreadLocal
存储的数据天然隔离,无法共享。
✅ 二、跨服务传递上下文的正确方案
需结合 Dubbo
的隐式参数传递 和 自定义过滤器 实现透传,核心步骤如下:
1. 消费端过滤器:将 MDC 值注入 Dubbo Attachment
@Activate(group = {CommonConstants.CONSUMER})
@Slf4j
public class DubboConsumerAuthFilter implements Filter {private static final String CONTEXT_KEY = "contextKey"; // 自定义上下文键名@Overridepublic Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {// 1. 设置追踪信息(如链路追踪ID)TraceUtils.setRpcTrace(invocation);// 2. 设置业务上下文if (ObjectUtil.isNotEmpty(ContextHolder.getContext()) && StrUtil.isNotEmpty(ContextHolder.getContext().getAccountCode())) {// 获取当前线程的上下文对象Context context = ContextHolder.getContext();// 序列化为JSON字符串String contextJsonStr = JSONObject.toJSONString(context);// 通过Attachment传递到服务提供方invocation.setAttachment(CONTEXT_KEY, contextJsonStr);}// 3. 发起Dubbo远程调用Result result = invoker.invoke(invocation);return result;}
}
- 原理:将
MDC
中的值(如trace_id
)通过invocation.setAttachment()
存入Dubbo
的调用附件,随请求发送到提供端 。
2. 提供端过滤器:从 Attachment 提取并设置 MDC
@Activate(group = {CommonConstants.PROVIDER})
public class DubboProviderFilter implements Filter {@Overridepublic Result invoke(Invoker<?> invoker, Invocation invocation) {// 从Attachment提取上下文String contextJson = invocation.getAttachment("contextKey");if (StrUtil.isNotBlank(contextJson)) {Context context = JSON.parseObject(contextJson, Context.class);ContextHolder.setContext(context); // 设置到本地线程上下文}try {return invoker.invoke(invocation);} finally {ContextHolder.remove(); // 清理防止线程池污染}}
}
- 原理:提供端过滤器解析请求附件中的值,写入本地
MDC
,确保后续日志能打印trace_id
。
3. 注册过滤器
在 src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter
文件中声明:
consumerMDCFilter=com.example.ConsumerMDCFilter providerMDCFilter=com.example.ProviderMDCFilter
⚙️ 三、注意事项
线程池污染问题 提供端需在
finally
中清理MDC
,避免线程复用导致上下文残留。Dubbo 版本差异
- Dubbo 2.x:使用
RpcContext.getContext().setAttachment()
。 - Dubbo 3.x:直接操作
invocation.setAttachment()
,避免因内置过滤器执行顺序导致失效 。
- Dubbo 2.x:使用
日志框架配置 修改
logback.xml
,在日志模板中加入%X{trace_id}
:
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%X{trace_id}] %msg%n</pattern>
- 异步调用场景 若消费端使用异步调用(如
@Async
),需通过TransmittableThreadLocal
代替ThreadLocal
,或手动传递MDC
值 。
🔍 四、其他替代方案
- 使用
RpcContext
直接传递 若不依赖MDC
,可直接通过RpcContext
在消费端设置参数,在提供端读取:
// 消费端RpcContext.getClientAttachment().setAttachment("key", "value");// 提供端String value = RpcContext.getServerAttachment().getAttachment("key");
但需注意 Attachment
在一次调用后会被清空。
- 分布式追踪工具集成 如
SkyWalking
、Zipkin
等,通过Agent
自动注入traceId
,无需手动传递 。
✅ 总结
- MDC 本身无法跨进程传递,需结合
Dubbo
的Attachment
机制和自定义过滤器实现透传。 - 关键步骤: 消费端过滤器将
MDC
→Attachment
→ 提供端过滤器将Attachment
→MDC
。 - 生产建议: 在提供端清理
MDC
、使用Dubbo 3.x
时优先操作invocation
而非RpcContext
,并配置日志模板输出trace_id
。