网关全局请求 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
公众号:服务端技术精选
评论