
微服务架构下一个用户请求可能经过网关、订单服务、库存服务、支付服务、通知服务……五六个服务串联处理。一旦出了问题排查起来简直是灾难• 网关日志里搜不到订单服务日志里也搜不到不知道请求到底流到了哪• 同一个请求在不同服务的日志里没有关联标识只能靠时间戳大概对齐误差几秒就对不上• 异步线程、MQ 消息里 traceId 丢了链路直接断成两截• 前端报错了只拿到一个 500开发想排查连请求标识都没有只能让用户再试一次复现。这就是没有全链路 traceId 的痛。而 Spring Cloud Gateway 作为微服务架构的入口是 traceId 传递的第一站也是最关键的一站。但 Gateway 是基于 WebFlux 响应式编程的传统的 ThreadLocal MDC 方案在响应式环境下直接失效很多人照搬 Servlet 那套写法结果 traceId 时有时无链路断断续续。一、为什么 Gateway 的 traceId 传递和 Servlet 不一样在说方案之前先搞清楚一个核心问题为什么传统的 traceId 方案在 Spring Cloud Gateway 里不好使1.1 Servlet 时代的 traceId 方案在传统的 Spring MVCServlet架构里traceId 传递很简单1. 用 Filter 拦截请求从请求头获取 traceId没有就生成一个2. 把 traceId 放到 ThreadLocal 里3. 日志框架通过 MDCMapped Diagnostic Context读取 ThreadLocal 里的 traceId打印到日志中4. 调用下游服务时从 ThreadLocal 取出 traceId放到请求头里传递5. 请求结束后清除 ThreadLocal避免内存泄漏。这套方案的核心依赖是ThreadLocal——因为 Servlet 是一个请求一个线程从头到尾都在同一个线程里执行ThreadLocal 能稳定保存上下文。1.2 Gateway 是响应式的ThreadLocal 失效了Spring Cloud Gateway 基于 Spring WebFlux而 WebFlux 基于 Reactor 响应式编程。响应式编程的核心特点是请求处理不绑定固定线程操作可能在不同线程间切换。请求进来 → 线程A处理过滤器 → 线程B处理路由 → 线程C调用下游 → 线程D处理响应同一个请求在不同阶段可能由不同线程处理。这时候 ThreadLocal 就废了• 线程 A 里 set 的 traceId线程 B 里 get 不到• MDC 基于 ThreadLocal日志里的 traceId 时有时无• 下游调用时取 traceId可能取到 null链路直接断了。1.3 解决方案Reactor ContextReactor 提供了Context机制专门用于在响应式流中传递上下文。它和 ThreadLocal 类似但不绑定线程而是绑定到响应式流Subscriber不管线程怎么切换Context 都能跟着流走。// 写入 Context Mono.just(data) .contextWrite(context - context.put(traceId, traceId)) .flatMap(data - { // 从 Context 读取 return Mono.deferContextual(contextView - { String traceId contextView.get(traceId); return doSomething(traceId); }); });所以 Spring Cloud Gateway 的 traceId 传递核心就是用 Reactor Context 替代 ThreadLocal 作为 traceId 的载体在过滤器中写入和读取同时通过钩子机制同步到 MDC 用于日志打印。二、整体架构traceId 全链路流转在写代码之前先看清楚 traceId 在整个微服务链路中的流转过程客户端请求 │ ▼ ┌─────────────────────────────────────────┐ │ Spring Cloud Gateway │ │ 1. GlobalFilter 拦截请求 │ │ 2. 从请求头 X-Trace-Id 获取没有则生成 │ │ 3. 写入 Reactor Context │ │ 4. 同步到 MDC日志打印 │ │ 5. 转发请求时把 traceId 放到请求头 │ │ 6. 响应时把 traceId 放到响应头返回客户端│ │ 7. 请求结束清除 MDC │ └─────────────────────────────────────────┘ │ 请求头携带 X-Trace-Id ▼ ┌─────────────────────────────────────────┐ │ 下游服务订单/库存/支付... │ │ 1. Filter 拦截从请求头获取 traceId │ │ 2. 放入 ThreadLocal / MDC │ │ 3. 日志自动打印 traceId │ │ 4. 调用下一个服务时从 MDC 取出放入请求头 │ │ 5. 异步/MQ 场景手动传递 traceId │ │ 6. 请求结束清除 │ └─────────────────────────────────────────┘核心设计原则•网关是入口traceId 在网关层生成或接收是全链路的起点•请求头传递服务间通过 HTTP 请求头X-Trace-Id传递这是最通用的方式•日志可追溯每个服务的日志都打印 traceId通过 traceId 能串联全链路•响应返回网关把 traceId 放到响应头返回给前端前端报错时可以带上 traceId 找开发排查。三、网关层实现GlobalFilter Reactor Context3.1 技术栈确认dependencies !-- Spring Cloud Gateway -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency !-- 日志默认 logback -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /dependency /dependencies3.2 TraceId 常量与工具类先定义统一的常量和工具类全链路共用public class TraceIdConstants { /** 链路追踪 ID 请求头 */ public static final String TRACE_ID_HEADER X-Trace-Id; /** MDC 中的 key */ public static final String TRACE_ID_MDC_KEY traceId; /** Reactor Context 中的 key */ public static final String TRACE_ID_CONTEXT_KEY traceId; /** traceId 长度 */ public static final int TRACE_ID_LENGTH 16; }public class TraceIdUtil { /** * 生成 traceId用 UUID 去掉横线取前 16 位 * 也可以用雪花算法、ObjectId 等只要全局唯一即可 */ public static String generateTraceId() { return UUID.randomUUID().toString().replace(-, ).substring(0, 16); } /** * 校验 traceId 格式是否合法 */ public static boolean isValid(String traceId) { return StrUtil.isNotBlank(traceId) traceId.length() 32; } }3.3 核心TraceId 全局过滤器这是整个方案的核心负责 traceId 的生成、Context 写入、MDC 同步、请求头透传、响应头返回。Component Slf4j public class TraceIdGlobalFilter implements GlobalFilter, Ordered { /** * 过滤器顺序尽量靠前在路由转发之前执行 */ Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE 10; } Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); // 1. 从请求头获取 traceId没有则生成 String traceId request.getHeaders().getFirst(TraceIdConstants.TRACE_ID_HEADER); if (!TraceIdUtil.isValid(traceId)) { traceId TraceIdUtil.generateTraceId(); log.debug(网关生成新的 traceId: {}, traceId); } // 2. 把 traceId 放到请求头传递给下游服务 ServerHttpRequest mutatedRequest request.mutate() .header(TraceIdConstants.TRACE_ID_HEADER, traceId) .build(); // 3. 把 traceId 放到响应头返回给客户端 exchange.getResponse().getHeaders().add(TraceIdConstants.TRACE_ID_HEADER, traceId); // 4. 写入 Reactor Context并通过钩子同步到 MDC final String finalTraceId traceId; return chain.filter(exchange.mutate().request(mutatedRequest).build()) // 写入 Reactor Context整个响应式流都能读取 .contextWrite(context - context.put(TraceIdConstants.TRACE_ID_CONTEXT_KEY, finalTraceId)) // 关键用 doOnEach 钩子在每个信号发出时同步 MDC .doOnEach(signal - { if (signal.isOnNext() || signal.isOnComplete() || signal.isOnError()) { MDC.put(TraceIdConstants.TRACE_ID_MDC_KEY, finalTraceId); } }) // 请求结束后清除 MDC避免线程复用导致的脏数据 .doFinally(signalType - MDC.remove(TraceIdConstants.TRACE_ID_MDC_KEY)); } }3.4 关键技术点详解上面的代码有几个关键技术点必须理解清楚否则 traceId 时有时无关键点1为什么用 doOnEach 同步 MDCMDC 基于 ThreadLocal而响应式流在线程间切换。doOnEach会在每个信号onNext/onComplete/onError发出时触发不管当前在哪个线程都会执行MDC.put。这样就能保证日志打印的那一刻MDC 里有 traceId。.doOnEach(signal - { if (signal.isOnNext() || signal.isOnComplete() || signal.isOnError()) { MDC.put(TraceIdConstants.TRACE_ID_MDC_KEY, finalTraceId); } })关键点2为什么用 doFinally 清除 MDC响应式编程中线程是池化复用的一个请求结束后线程可能被下一个请求复用。如果不清除 MDC下一个请求可能读到上一个请求的 traceId导致日志串号。doFinally会在流结束成功/失败/取消时触发确保 MDC 被清除。关键点3为什么用 contextWrite 写入 Reactor ContextcontextWrite是 Reactor 提供的写入 Context 的操作符它会把数据写入到响应式流的上下文中。后续的操作符可以通过deferContextual读取 Context 中的 traceId比如在自定义过滤器、限流逻辑中需要 traceId 时。// 下游过滤器读取 traceId 示例 public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { return Mono.deferContextual(contextView - { String traceId contextView.getOrDefault(TraceIdConstants.TRACE_ID_CONTEXT_KEY, unknown); log.info(当前请求 traceId: {}, traceId); return chain.filter(exchange); }); }关键点4为什么用 request.mutate() 修改请求头ServerHttpRequest是不可变的不能直接修改请求头。通过mutate()创建一个新的请求对象添加 traceId 请求头再通过exchange.mutate().request(...).build()替换 exchange 中的请求。这样下游路由转发时就会携带 traceId 请求头。3.5 日志配置logback 打印 traceId配置 logback在日志格式中加上 traceIdconfiguration appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n /pattern /encoder /appender root levelINFO appender-ref refCONSOLE/ /root /configuration关键是%X{traceId}这是 logback 的 MDC 变量输出语法会自动读取 MDC 中 key 为traceId的值。配置后网关日志输出效果2026-08-23 10:30:00.123 [reactor-http-nio-3] [a1b2c3d4e5f6g7h8] INFO c.e.g.filter.TraceIdGlobalFilter - 请求路由到订单服务四、下游服务实现接收 透传 日志网关把 traceId 放到请求头了下游服务需要接收、打印日志、继续传递给下一个服务。4.1 Servlet 下游服务Spring MVC大部分下游服务是 Spring MVCServlet架构用 Filter 实现Component Order(Ordered.HIGHEST_PRECEDENCE) public class TraceIdFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; HttpServletResponse httpResponse (HttpServletResponse) response; // 1. 从请求头获取 traceId String traceId httpRequest.getHeader(TraceIdConstants.TRACE_ID_HEADER); if (!TraceIdUtil.isValid(traceId)) { traceId TraceIdUtil.generateTraceId(); } // 2. 放入 MDC日志自动打印 MDC.put(TraceIdConstants.TRACE_ID_MDC_KEY, traceId); // 3. 响应头也返回 traceId httpResponse.setHeader(TraceIdConstants.TRACE_ID_HEADER, traceId); try { chain.doFilter(request, response); } finally { // 4. 请求结束清除 MDC MDC.remove(TraceIdConstants.TRACE_ID_MDC_KEY); } } }4.2 WebFlux 下游服务响应式如果下游服务也是 WebFlux和网关一样用 Reactor ContextComponent public class TraceIdWebFilter implements WebFilter, Ordered { Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE 10; } Override public MonoVoid filter(ServerWebExchange exchange, WebFilterChain chain) { String traceId exchange.getRequest().getHeaders().getFirst(TraceIdConstants.TRACE_ID_HEADER); if (!TraceIdUtil.isValid(traceId)) { traceId TraceIdUtil.generateTraceId(); } // 响应头返回 traceId exchange.getResponse().getHeaders().add(TraceIdConstants.TRACE_ID_HEADER, traceId); final String finalTraceId traceId; return chain.filter(exchange) .contextWrite(context - context.put(TraceIdConstants.TRACE_ID_CONTEXT_KEY, finalTraceId)) .doOnEach(signal - { if (signal.isOnNext() || signal.isOnComplete() || signal.isOnError()) { MDC.put(TraceIdConstants.TRACE_ID_MDC_KEY, finalTraceId); } }) .doFinally(signalType - MDC.remove(TraceIdConstants.TRACE_ID_MDC_KEY)); } }4.3 服务间调用RestTemplate / WebClient 透传下游服务调用下一个服务时需要把 traceId 从 MDC 取出放到请求头。RestTemplate 拦截器Component public class TraceIdRestTemplateInterceptor implements ClientHttpRequestInterceptor { Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { String traceId MDC.get(TraceIdConstants.TRACE_ID_MDC_KEY); if (StrUtil.isNotBlank(traceId)) { request.getHeaders().add(TraceIdConstants.TRACE_ID_HEADER, traceId); } return execution.execute(request, body); } }注册到 RestTemplateBean public RestTemplate restTemplate() { RestTemplate restTemplate new RestTemplate(); restTemplate.setInterceptors(List.of(new TraceIdRestTemplateInterceptor())); return restTemplate; }WebClient 过滤器Bean public WebClient webClient() { return WebClient.builder() .filter((request, next) - { String traceId MDC.get(TraceIdConstants.TRACE_ID_MDC_KEY); if (StrUtil.isNotBlank(traceId)) { ClientRequest mutatedRequest ClientRequest.from(request) .header(TraceIdConstants.TRACE_ID_HEADER, traceId) .build(); return next.exchange(mutatedRequest); } return next.exchange(request); }) .build(); }注意WebClient 是响应式的MDC 在响应式环境下可能失效。如果是 WebFlux 服务调用建议从 Reactor Context 读取 traceId而不是 MDC。五、异步场景traceId 最容易丢的地方同步调用的 traceId 传递很简单但异步场景Async、线程池、MQ 消息是 traceId 最容易丢失的地方。5.1 Async 异步方法Async方法在新线程执行ThreadLocal 里的 traceId 不会自动传递。解决方案自定义 TaskDecorator在任务执行前把 traceId 传递过去。public class TraceIdTaskDecorator implements TaskDecorator { Override public Runnable decorate(Runnable runnable) { // 从当前线程调用方线程获取 traceId String traceId MDC.get(TraceIdConstants.TRACE_ID_MDC_KEY); return () - { try { // 在异步线程中设置 traceId if (StrUtil.isNotBlank(traceId)) { MDC.put(TraceIdConstants.TRACE_ID_MDC_KEY, traceId); } runnable.run(); } finally { MDC.remove(TraceIdConstants.TRACE_ID_MDC_KEY); } }; } }配置到线程池Configuration EnableAsync public class AsyncConfig { Bean(asyncExecutor) public ThreadPoolTaskExecutor asyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(500); executor.setThreadNamePrefix(async-); // 关键设置 TaskDecorator自动传递 traceId executor.setTaskDecorator(new TraceIdTaskDecorator()); executor.initialize(); return executor; } }5.2 手动线程池如果是自己创建的线程池ThreadPoolExecutor用同样的思路包装 Runnablepublic class TraceIdRunnableWrapper implements Runnable { private final Runnable delegate; private final String traceId; public TraceIdRunnableWrapper(Runnable delegate) { this.delegate delegate; this.traceId MDC.get(TraceIdConstants.TRACE_ID_MDC_KEY); } Override public void run() { if (StrUtil.isNotBlank(traceId)) { MDC.put(TraceIdConstants.TRACE_ID_MDC_KEY, traceId); } try { delegate.run(); } finally { MDC.remove(TraceIdConstants.TRACE_ID_MDC_KEY); } } public static Runnable wrap(Runnable runnable) { return new TraceIdRunnableWrapper(runnable); } }使用时executor.execute(TraceIdRunnableWrapper.wrap(() - { // 异步逻辑MDC 里有 traceId log.info(异步任务执行); }));5.3 MQ 消息传递MQ 消息是跨服务的traceId 需要放到消息头或消息体里传递。以 RocketMQ 为例// 发送消息时把 traceId 放到消息属性 public void sendMessage(String topic, String body) { String traceId MDC.get(TraceIdConstants.TRACE_ID_MDC_KEY); Message message new Message(topic, body.getBytes(StandardCharsets.UTF_8)); if (StrUtil.isNotBlank(traceId)) { message.putUserProperty(TraceIdConstants.TRACE_ID_HEADER, traceId); } rocketMQTemplate.syncSend(topic, message); } // 消费消息时从消息属性取出 traceId 放入 MDC RocketMQMessageListener(topic order-topic, consumerGroup order-group) public class OrderConsumer implements RocketMQListenerMessageExt { Override public void onMessage(MessageExt message) { String traceId message.getUserProperty(TraceIdConstants.TRACE_ID_HEADER); if (!TraceIdUtil.isValid(traceId)) { traceId TraceIdUtil.generateTraceId(); } MDC.put(TraceIdConstants.TRACE_ID_MDC_KEY, traceId); try { // 消费逻辑 String body new String(message.getBody(), StandardCharsets.UTF_8); log.info(消费消息: {}, body); } finally { MDC.remove(TraceIdConstants.TRACE_ID_MDC_KEY); } } }六、与 Spring Cloud Sleuth / SkyWalking 的对比很多人会问既然有 Sleuth、SkyWalking 这些链路追踪框架为什么还要自己实现 traceId6.1 方案对比维度手动实现 traceIdSpring Cloud SleuthSkyWalking实现成本中等需要写过滤器低引入依赖自动生效中等需要部署 AgenttraceId 生成自己控制自动生成B3 格式自动生成日志集成自己配置 MDC自动集成 MDC自动集成链路可视化无只能靠日志搜需配合 Zipkin自带 UI功能强大性能开销极低低中等Agent 埋点侵入性代码级侵入依赖级侵入无侵入Agent适用场景简单链路追踪、自研体系Spring Cloud 生态复杂微服务、需要可视化6.2 选型建议•简单项目、只需要日志 traceId手动实现就够了轻量可控•Spring Cloud 生态、需要 Zipkin 可视化用 Sleuth和 Gateway 集成好•中大型微服务、需要全链路拓扑和性能分析用 SkyWalking功能最强大•混合方案用 SkyWalking 做全链路追踪同时自己在网关生成 traceId 返回给前端前端报错时可以直接用 traceId 去 SkyWalking 搜。注意如果用了 Sleuth 或 SkyWalking它们会自动处理 traceId 传递不需要自己写过滤器。但如果需要自定义 traceId 格式、或者需要把 traceId 返回给前端还是需要做一些定制。全文总结全链路 traceId 传递看起来就是生成一个 ID放到请求头里传下去但真正落地时处处是坑。最核心的认知是Spring Cloud Gateway 是响应式的传统的 ThreadLocal 方案直接失效。必须用 Reactor Context 作为 traceId 的载体配合doOnEach钩子同步到 MDC才能保证日志里稳定打印 traceId。在此基础上还需要覆盖• 下游服务的接收和透传Servlet 用 FilterWebFlux 用 WebFilter• 服务间调用的自动传递RestTemplate/WebClient 拦截器• 异步场景的 traceId 保持TaskDecorator、Runnable 包装• MQ 消息的跨服务传递消息属性• 响应头返回给前端排查问题的关键。把这些环节都覆盖到才能真正实现一次请求全链路可追溯。出了问题拿一个 traceId从网关到订单到库存到支付所有服务的日志一键串联排查效率从几小时降到几分钟。traceId 是微服务架构的基础设施看似不起眼却是线上排查问题最得力的工具。把基础打扎实系统的可观测性才能上一个台阶。微服务架构下可观测性是系统稳定性的基石。traceId 传递、日志规范、链路追踪、监控告警每一项都是线上排查问题的利器。后续持续更新微服务实战专栏Spring Cloud Gateway 高级用法、全链路日志规范、分布式链路追踪落地、微服务监控告警体系全套生产干货。喜欢微服务、网关、可观测性、后端架构内容欢迎点赞、收藏、关注持续跟进后端进阶开发专栏