网关全局请求 ID 生成:日志排查靠猜?X-Request-ID 统一注入 + 全链路透传!

公司线上出了个支付异常,三四个微服务都有日志,但各打各的,谁也不知道哪条日志和哪条日志是同一个请求。运维在 ELK 里翻了一个小时,靠着时间戳和 userId 硬猜,最后猜错了版本,回滚到错误的代码上又出了一波事故。要是每条日志头上都有一个贯穿全链路的请求 ID,一秒就能搜出这个请求的完整轨迹。

X-Request-ID 不是什么新技术,但落地起来细节不少。谁生成、怎么传、下游怎么接、日志怎么打,任何一个环节断了,链路就断了。今天把这些细节串起来。


谁负责生成 Request ID

生成责任只有一个:第一个接收到请求的网关。 客户端不生成,因为你不能信任客户端传上来的值——重复、太短、甚至包含注入字符。

如果客户端已经传了 X-Request-ID(比如从另一个系统透传过来的),网关应当尊重它,直接转发。但如果客户端没传,网关必须生成一个。

请求到达 Gateway
  │
  ├─ 检查 Header: X-Request-ID
  │    ├─ 有值 → 直接用(跨系统透传)
  │    └─ 没值 → 生成新 ID
  │
  └─ 写入 Header → 转发到下游微服务

ID 格式建议用 UUID 去横线,或者雪花算法,保证全局唯一。不要用自增数字——多网关部署的时候会重复。

String requestId = Optional.ofNullable(
        request.getHeader("X-Request-ID"))
    .orElse(UUID.randomUUID().toString().replace("-", ""));

怎么透传到下游

光网关生成不够,下游微服务得收到这个 ID 才有意义。透传分两个层面:

HTTP 层:Header 透传

网关在转发请求时,把 X-Request-ID 放到请求头里。下游微服务从 Header 里拿出来用。

网关侧只需要配一个 Filter:

@Component
public class RequestIdFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestId = resolveRequestId(exchange.getRequest());

        // 写入请求头,转发到下游
        ServerHttpRequest mutated = exchange.getRequest().mutate()
                .header("X-Request-ID", requestId)
                .build();

        return chain.filter(exchange.mutate().request(mutated).build());
    }
}

Feign/RestTemplate 层:自动透传

如果下游是走 Feign 调用的,需要一个 RequestInterceptor 自动把 Header 带过去:

@Bean
public RequestInterceptor requestIdInterceptor() {
    return template -> {
        String requestId = MDC.get("requestId");
        if (requestId != null) {
            template.header("X-Request-ID", requestId);
        }
    };
}

这样即使服务 A 通过 Feign 调服务 B,Request ID 也会自动透传。

消息队列层:放入消息体

RocketMQ / RabbitMQ 异步消息也需要携带 Request ID。建议在消息体里增加一个 traceId 字段,而不是放在消息扩展属性里——扩展属性不可靠,有些中间件会截断。


怎么跟日志关联

透传到了,还得让日志打印出来。这里用 MDC(Mapped Diagnostic Context):

// 在每个请求开始时,把 requestId 塞进 MDC
MDC.put("requestId", requestId);

// logback 配置里用 %X{requestId} 打印
// pattern: %d [%thread] %X{requestId} %-5level %logger - %msg%n

日志效果:

2026-06-06 22:00:01.234 [http-nio-8080-1] a1b2c3d4 INFO  OrderService - 创建订单
2026-06-06 22:00:01.456 [http-nio-8080-1] a1b2c3d4 INFO  PayService - 发起支付
2026-06-06 22:00:01.789 [http-nio-8080-1] a1b2c3d4 ERROR PayService - 支付失败

同一个 a1b2c3d4 串起了 OrderService 和 PayService 的所有日志。在 ELK 里搜这个 ID,整条链路的日志全部出来。

MDC 是基于 ThreadLocal 的,所以请求处理完一定要 MDC.clear(),否则会被线程池里的下一个请求污染。在 Filter 的 finally 块里清理最安全。


全链路方案总结

请求进入 Gateway
  ├─ 生成/接收 X-Request-ID
  ├─ 写入 MDC
  ├─ Header 转发到微服务 A
  │
  ├─ 微服务 A 收到
  │    ├─ Filter 提取 Header → MDC
  │    ├─ 日志自动带上 requestId
  │    ├─ Feign 调用微服务 B → Header 自动透传
  │    └─ MQ 发送 → traceId 放入消息体
  │
  ├─ 微服务 B 收到
  │    ├─ 同 A 的流程
  │    └─ MDC.clear() 在 finally 块
  │
  └─ 响应返回 Gateway
       └─ 响应头带上 X-Request-ID(方便前端排查)

最后一步容易被忽略:响应头也带上 X-Request-ID。这样前端报错的时候,你把 X-Request-ID 的值发给后端,后端一秒定位。


总结

X-Request-ID 的价值不是技术上的,是排查效率上的。没有它,跨微服务的问题排查靠猜。有了它,一秒搜出全链路日志。

三个关键动作:

网关注入——请求入口统一生成(或接收透传),Header 转发到下游。格式用 UUID 去横线。

MDC 关联——每个服务把 Header 里的 Request ID 放进 MDC,logback pattern 里用 %X{requestId} 打印。每个请求结束 MDC.clear()

全链路透传——HTTP(Header)、Feign(RequestInterceptor)、MQ(消息体),三种通信方式都要带上,缺一个就断链。

这套方案落地的代码量很小——一个全局 Filter、一个 RequestInterceptor、logback 里加一行 pattern。但效果立竿见影——下次排查问题不再靠猜。


有用的话转给还在用 timestamp + userId 硬猜日志的同事。


标题:网关全局请求 ID 生成:日志排查靠猜?X-Request-ID 统一注入 + 全链路透传!
作者:jiangyi
地址:http://jiangyi.space/articles/2026/06/11/1780758158139.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消