跳过正文
  1. 文章/

Request ID:分布式系统的案件编号

sun.ao
作者
sun.ao
我是 sun.ao,一名热爱技术的程序员,专注于 AI 和数智化领域。
目录

问题场景
#

凌晨 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  # 关联 ID

2. 日志格式
#

统一使用 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 是分布式系统可观测性的基石。投入小、收益大——一根线串起整个调用链路

扩展阅读
#

相关文章