问题场景#
凌晨 3 点,报警响起:用户下单失败。你打开日志系统,面对的是:
[服务A] 10:23:45 用户请求下单
[服务B] 10:23:46 查询库存成功
[服务C] 10:23:46 支付调用失败
[服务A] 10:23:47 下单失败
[服务B] 10:23:47 另一个用户的请求...
[服务C] 10:23:47 又一个用户的请求...成千上万条日志交织在一起,哪条属于这个用户的请求?这就是分布式追踪要解决的核心问题。
什么是 Request ID?#
Request ID 是分配给每次请求的唯一标识符,它贯穿请求的整个生命周期:
用户请求 ──▶ 服务A ──▶ 服务B ──▶ 服务C ──▶ 数据库
│ │ │ │
└──────────┴─────────┴─────────┘
同一个 Request ID类比:就像快递单号,无论包裹经过多少个中转站,都能通过单号追踪完整路径。
设计原则#
1. 唯一性#
确保全局唯一,避免冲突:
// 推荐:UUID v4
requestID := uuid.New().String() // "550e8400-e29b-41d4-a716-446655440000"
// 或:Snowflake 算法(有序 + 唯一)
requestID := snowflake.Generate() // "1234567890123456789"
// 不推荐:时间戳(高并发会重复)
requestID := time.Now().UnixNano() // 危险!2. 透传性#
Request ID 必须在服务间自动传递:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 服务 A │────▶│ 服务 B │────▶│ 服务 C │
│ X-Req-ID│ │ X-Req-ID│ │ X-Req-ID│
│ =abc123 │ │ =abc123 │ │ =abc123 │
└─────────┘ └─────────┘ └─────────┘HTTP 方式:
// 服务 A:生成并传递
func handler(w http.ResponseWriter, r *http.Request) {
requestID := uuid.New().String()
r.Header.Set("X-Request-ID", requestID)
// 调用下游服务...
}
// 服务 B:接收并继续传递
func handler(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
// 记录日志...
// 调用下游时继续传递...
}gRPC 方式:
// 使用 metadata 传递
md := metadata.Pairs("x-request-id", requestID)
ctx := metadata.NewOutgoingContext(context.Background(), md)3. 生成时机#
| 场景 | 生成位置 |
|---|---|
| 用户请求 | 网关层(API Gateway) |
| 定时任务 | 任务启动时 |
| 消息消费 | 消息生产时生成,消费时透传 |
| 内部调用 | 沿用上游 Request ID |
实现方案#
方案一:网关统一生成#
用户 ──▶ API Gateway ──▶ 服务A ──▶ 服务B
│
▼
生成 Request ID
│
└──────────┴──────────▶ 所有服务使用同一个 ID# Nginx 配置
location / {
set $request_id $request_id; # 使用 Nginx 内置变量
proxy_set_header X-Request-ID $request_id;
proxy_pass http://backend;
}方案二:中间件自动处理#
// Gin 中间件示例
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 优先使用上游传递的 ID
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
// 存入 context,供后续使用
c.Set("requestID", requestID)
// 响应头返回,方便调试
c.Header("X-Request-ID", requestID)
c.Next()
}
}方案三:日志框架集成#
// 使用结构化日志
log := logger.WithField("request_id", requestID)
// 所有日志自动携带 request_id
log.Info("查询库存") // {"level":"info","request_id":"abc123","msg":"查询库存"}
log.Error("支付失败") // {"level":"error","request_id":"abc123","msg":"支付失败"}最佳实践#
1. 命名规范#
常用的 Header 名称:
X-Request-ID # 业界最常用
X-Trace-ID # 链路追踪系统
X-Correlation-ID # 关联 ID2. 日志格式#
统一使用 JSON 格式,便于日志系统检索:
{
"timestamp": "2026-03-19T10:23:45.123Z",
"level": "ERROR",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"service": "order-service",
"msg": "支付调用失败",
"error": "connection timeout"
}3. 链路追踪集成#
Request ID 可与 OpenTelemetry 等追踪系统配合:
Request ID (业务层) ──▶ 用户可读,日志检索
Trace ID (追踪层) ──▶ 系统生成,链路可视化// 关联 Request ID 和 Trace ID
span := trace.SpanFromContext(ctx)
span.SetAttributes(
attribute.String("request.id", requestID),
)4. 异步场景处理#
消息队列:
// 生产者
msg := kafka.Message{
Headers: map[string]string{
"X-Request-ID": requestID,
},
Value: orderData,
}
// 消费者
requestID := msg.Headers["X-Request-ID"]
ctx := context.WithValue(context.Background(), "requestID", requestID)异步任务:
// 将 Request ID 传递给 goroutine
go func(requestID string) {
log := logger.WithField("request_id", requestID)
// 异步处理...
}(requestID)实战案例#
问题定位#
有了 Request ID,问题定位变得简单:
# ELK 查询
request_id:"550e8400-e29b-41d4-a716-446655440000"
# 结果:按时间排序,完整链路一目了然
[10:23:45] 服务A 收到下单请求
[10:23:45] 服务A 调用库存服务
[10:23:46] 服务B 查询库存成功
[10:23:46] 服务A 调用支付服务
[10:23:46] 服务C 支付调用失败: connection timeout
[10:23:47] 服务A 下单失败完整示例#
package main
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
var logger *zap.Logger
func main() {
r := gin.New()
// Request ID 中间件
r.Use(func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
c.Set("requestID", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
})
// 日志中间件
r.Use(func(c *gin.Context) {
requestID, _ := c.Get("requestID")
logger.Info("请求开始",
zap.String("request_id", requestID.(string)),
zap.String("path", c.Request.URL.Path),
)
c.Next()
})
r.POST("/order", createOrder)
r.Run(":8080")
}
func createOrder(c *gin.Context) {
requestID, _ := c.Get("requestID")
// 调用下游服务时传递 Request ID
// req.Header.Set("X-Request-ID", requestID.(string))
logger.Info("创建订单",
zap.String("request_id", requestID.(string)),
)
c.JSON(200, gin.H{
"request_id": requestID,
"status": "success",
})
}总结#
| 要点 | 说明 |
|---|---|
| 唯一性 | UUID 或 Snowflake,避免冲突 |
| 透传性 | HTTP Header / gRPC Metadata 自动传递 |
| 生成位置 | 网关层统一生成,下游透传 |
| 日志集成 | 结构化日志,自动携带 Request ID |
| 异步处理 | 消息头 / Context 传递 |
Request ID 是分布式系统可观测性的基石。投入小、收益大——一根线串起整个调用链路。
