You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

同一reactor-http-nio线程下Netty HttpClient WRITE事件丢失Sleuth TraceId

Sleuth TraceId在Netty复用线程的WRITE事件中丢失问题解决

问题分析

升级Spring Boot至2.7.6+(当前使用2.7.11)后,复用reactor-http-nio线程时,自定义LoggingHandler处理WRITE事件时丢失Sleuth TraceId,仅首次使用新线程的请求能正常打印TraceId,READ事件无异常。核心原因:

  • Netty Channel复用后,绑定在Channel pipeline中的PayloadLogger会常驻;后续请求的WRITE事件在Netty IO线程执行时,无法从ThreadLocal获取当前请求的Sleuth TraceContext——IO线程复用导致ThreadLocal残留旧上下文或已被清除。
  • READ事件正常是因为响应处理流程中,Reactor自动将请求上下文传播到了IO线程,而WRITE事件的触发时机未自动完成上下文传播。

解决方案

修改自定义LoggingHandler,从Reactor请求上下文而非ThreadLocal中获取TraceId,确保每次请求的WRITE事件都能拿到正确的TraceContext。

修改后的代码示例

import reactor.netty.ReactorNetty;
import reactor.util.context.Context;
import org.springframework.cloud.sleuth.TraceContext;
import org.springframework.cloud.sleuth.CurrentTraceContext;
import org.springframework.util.StringUtils;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.logging.LoggingHandler;
import reactor.netty.http.client.HttpClient;

@Component
public class PayloadLogger extends LoggingHandler {

    private static final String WEBCLIENT_LOGGING_READ_EVENT = "READ";
    private static final String WEBCLIENT_LOGGING_WRITE_EVENT = "WRITE";

    public PayloadLogger() {
        super(HttpClient.class, DEBUG, SIMPLE);
    }

    @Override
    protected String format(ChannelHandlerContext ctx, String event, Object arg) {
        String traceId = getTraceIdFromReactorContext(ctx);
        
        if (arg instanceof ByteBufHolder byteBufHolder && StringUtils.equalsAny(event, WEBCLIENT_LOGGING_READ_EVENT, WEBCLIENT_LOGGING_WRITE_EVENT)) {
            var logMsg = byteBufHolder.content().toString(UTF_8);
            
            if (WEBCLIENT_LOGGING_WRITE_EVENT.equals(event)) {
                return String.format("[TraceId: %s] Request Payload: %s", traceId, logMsg);
            }
            
            if (WEBCLIENT_LOGGING_READ_EVENT.equals(event)) {
                return String.format("[TraceId: %s] Response Payload: %s", traceId, logMsg);
            }
        }
        return String.format("[TraceId: %s] %s", traceId, super.format(ctx, event, arg));
    }

    private String getTraceIdFromReactorContext(ChannelHandlerContext ctx) {
        Context reactorContext = ctx.channel().attr(ReactorNetty.REACTOR_CONTEXT).get();
        if (reactorContext == null) {
            return "UNKNOWN";
        }
        
        TraceContext traceContext = reactorContext.getOrDefault(TraceContext.class, null);
        if (traceContext != null) {
            return traceContext.traceId();
        }
        
        try {
            traceContext = CurrentTraceContext.get().context();
            return traceContext != null ? traceContext.traceId() : "UNKNOWN";
        } catch (Exception e) {
            return "UNKNOWN";
        }
    }
}

额外优化建议

  • 避免向复用的Connection添加Handler:如果不需要全局复用PayloadLogger,可改为在每个请求的pipeline中临时添加,确保每次请求绑定当前上下文。示例:
    HttpClient.create()
        .doOnRequest((request, connection) -> {
            connection.addHandlerFirst(new PayloadLogger());
        });
    
    注意:频繁创建Handler实例存在性能损耗,需根据业务场景权衡。

验证步骤

  1. 启动应用,发起多次请求,确保Netty IO线程被复用。
  2. 检查WRITE事件日志,确认TraceId与当前请求一致。
  3. 验证READ事件日志的TraceId仍正常输出。

内容的提问来源于stack exchange,提问作者Rayyan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 06:47:41