记录一次因为Dubbo自调用导致的traceId丢失的问题

64 阅读3分钟

一、发现问题

在某次排查bug的过程中发现日志中的traceId在调用链路中突然丢失,然后开始研究丢失原因。

image-20250227162738075.png

二、排查问题

查看之前的日志,发现这个问题主要集中在同一个服务中,并且问题不是偶现的,同一个请求路径多次调用都会出现这种问题。但是看整个调用链路中并没有手动清除过traceId,这时就有点摸不着头脑了,难道是在过滤器、拦截器中清除的,然后查看traceId工具类,发现remove方法仅在少有几个地方被调用。

image-20250227163412767.png

除去在业务方调用的几个之外就剩下Mq,Dubbo,Web这几个过滤器中,因为请求不涉及到消息,所以排除Mq,着重看WebTraceFilter和DubboProviderTraceFilter。

@Slf4j
@Configuration
@Order(-1000)
public class WebTraceFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws IOException, ServletException {
        try {
            String traceId = request.getHeader(MDCTraceUtils.TRACE_ID_HEADER);
            if (StrUtil.isEmpty(traceId)) {
                traceId = MDCTraceUtils.createTraceId();
            }
            MDCTraceUtils.putTrace(traceId);
            response.setHeader(MDCTraceUtils.TRACE_ID_HEADER,traceId);
            logger.info("请求地址:"+request.getRequestURI() + ",traceId:" + traceId);
            filterChain.doFilter(request, response);
        } finally {
            MDCTraceUtils.removeTrace();
        }
    }
}

在WebTraceFilter中是执行完controller逻辑之后才会到MDCTraceUtils.removeTrace();但是实际情况是还没开始执行controller的方法就已经丢失了,所以排除WebTraceFilter,最后就是dubbo过滤器了。这时回头看丢失traceId的地方,的确都是在调用dubbo接口后出现的,但并非所有调用了dubbo接口的地方都会出现这个问题。以下是dubbo过滤器中的代码。

@Slf4j
@Activate(group = CommonConstants.PROVIDER, order = 1)
public class DubboProviderTraceFilter implements Filter, Filter.Listener {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        String traceID = RpcContext.getServerAttachment().getAttachment(MDCTraceUtils.KEY_TRACE_ID);
        if (StringUtils.isBlank(traceID)) {
            MDCTraceUtils.addTrace();
        } else{
            MDCTraceUtils.putTrace(traceID);
        }
        Result result = invoker.invoke(invocation);
        MDCTraceUtils.removeTrace();
        return result;
    }
  }

乍一看也没啥问题啊?作为provider,在执行完逻辑后是应该清除traceId才对,因为dubbo默认会使用线程池,如果不清除掉,下一次请求可能会复用这个线程,导致traceId混用。

这时又陷入了沉思,为啥啊,为什么有的地方会丢失,有的地方不会呢?后来又翻了下日志,发现记录操作日志的AOP中也可能导致traceId丢失,一阵头大,仔细思考后它们发现有一个共性,出现丢失问题的地方都是调用了dubbo接口,并且都是自调用,比如调用这个dubbo接口的服务也是注册这个dubbo接口的服务(出现这种情况是因为我们把一些共有逻辑,比如checkApiPath这种方法写在了core包下,每一个服务都会依赖这个core包,但是checkApiPath方法又需要调用用户服务去查询一些数据)。这时灵光一现,dubbo自调用有没有可能还是用同一个线程呢(因为考虑到traceId是存在线程中的),再去看日志发现的确是同一个线程名,既然在同一个线程且还经过DubboProviderTraceFilter,那么按照DubboProviderTraceFilter之前的逻辑,那么肯定是会清除的。

到这里基本知道了traceId丢失的原因,那如何解决这个问题呢?话不多说,直接问DeepSeek

三、解决问题

image-20250227165610660.png

一针见血啊,因为Dubbo-3.x的优化为本地调用,这个优化也没有问题,本地调用肯定比走网络快的多,所以不能修改为远程调用。然后deepseek给出了解决方案, 即修改DubboProviderTraceFilter的invoke方法,判断是不是自调用,如果是就不清除traceId了。代码改后debug一下,traceId不会丢失了,完美!!


public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        String protocol = invoker.getUrl().getProtocol();
        if ("injvm".equals(protocol)) {// 根据协议判断是否自调用,远程调用的话是dubbo协议,自调用为injvm
            return invoker.invoke(invocation);
        }
        String traceID = RpcContext.getServerAttachment().getAttachment(MDCTraceUtils.KEY_TRACE_ID);
        if (StringUtils.isBlank(traceID)) {
            MDCTraceUtils.addTrace();
        } else{
            MDCTraceUtils.putTrace(traceID);
        }
        Result result = invoker.invoke(invocation);
        MDCTraceUtils.removeTrace();
        return result;
    }