189 lines
7.4 KiB
Markdown
189 lines
7.4 KiB
Markdown
|
|
---
|
|||
|
|
tags:
|
|||
|
|
- MQ
|
|||
|
|
- 链路追踪
|
|||
|
|
- 可观测性
|
|||
|
|
- 分布式系统
|
|||
|
|
create time: 2026-05-24 19:52
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# MQ 消息轨迹与链路追踪
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
"消息发了,但对方说没收到。"这句话是分布式系统调试中最让人头疼的开场白。消息轨迹与链路追踪就是为了解决这个问题——让你清楚地知道一条消息从生产到消费的完整生命周期。
|
|||
|
|
|
|||
|
|
## 正文
|
|||
|
|
|
|||
|
|
### 为什么需要消息轨迹
|
|||
|
|
|
|||
|
|
在微服务架构中,一条消息从 Producer 发出,经过 Broker 存储,最终被 Consumer 消费处理。这个链路上任何一个环节出问题,排查起来都非常困难:
|
|||
|
|
|
|||
|
|
- 消息到底发出去了吗?Broker 收到了吗?
|
|||
|
|
- 消息被哪个 Consumer Group 消费了?
|
|||
|
|
- 消费成功了还是失败了?失败的原因是什么?
|
|||
|
|
- 一条业务消息从 A 服务到 B 服务到 C 服务,整条链路耗时多少?
|
|||
|
|
|
|||
|
|
传统的日志排查方式效率极低——你需要登录多台机器,grep 关键字,然后手动拼凑时间线。消息轨迹就是把这些信息结构化记录下来,让排查变成"查一下就知道"。
|
|||
|
|
|
|||
|
|
### 消息唯一标识设计
|
|||
|
|
|
|||
|
|
要追踪一条消息,首先得能唯一标识它。这里有三个关键标识:
|
|||
|
|
|
|||
|
|
**MessageID**:消息在 Broker 端的唯一标识。Kafka 用 `Topic + Partition + Offset` 三元组定位一条消息,RocketMQ 有独立的 MessageID。这是技术层面的唯一标识。
|
|||
|
|
|
|||
|
|
**业务 Key**:业务层面的唯一标识,比如订单号 `order_id`。一条消息可能因为重试产生多个 MessageID,但业务 Key 是不变的。排查问题时,通常从业务 Key 入手。
|
|||
|
|
|
|||
|
|
**TraceID**:链路追踪层面的标识,贯穿整个调用链。一个 TraceID 可以关联"HTTP 请求 → 消息发送 → 消息消费 → 数据库写入"这条完整链路。
|
|||
|
|
|
|||
|
|
三者的关系:MessageID 定位消息,业务 Key 定位业务实体,TraceID 串联链路。排查问题时,通常先用业务 Key 找到消息,再用 TraceID 看完整链路。
|
|||
|
|
|
|||
|
|
> [!question]
|
|||
|
|
> 如果一条消息因为消费失败被重试了 3 次,最终成功。从消息轨迹的角度看,这 3 次重试应该怎么记录?是 3 条独立轨迹还是一条轨迹的 3 个消费记录?
|
|||
|
|
|
|||
|
|
### 消息轨迹的三个阶段
|
|||
|
|
|
|||
|
|
一条消息的完整生命周期可以分为三个阶段,每个阶段都需要记录关键信息:
|
|||
|
|
|
|||
|
|
**生产轨迹**:Producer 发送消息时记录。包括发送时间、发送结果(成功/失败)、目标 Topic、消息 Key、消息大小、耗时。
|
|||
|
|
|
|||
|
|
**存储轨迹**:Broker 接收消息时记录。包括入库时间、分配的 Partition、分配的 Offset、Broker 节点地址。
|
|||
|
|
|
|||
|
|
**消费轨迹**:Consumer 消费消息时记录。包括消费时间、消费结果(成功/失败/重试)、消费耗时、Consumer 实例标识、处理异常信息。
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph LR
|
|||
|
|
Producer["Producer"] -->|"1. 发送消息"| Broker["Broker"]
|
|||
|
|
Broker -->|"2. 存储消息"| Storage["Storage"]
|
|||
|
|
Broker -->|"3. 投递消息"| Consumer["Consumer"]
|
|||
|
|
Producer -.->|"生产轨迹"| TraceDB["Trace Storage"]
|
|||
|
|
Broker -.->|"存储轨迹"| TraceDB
|
|||
|
|
Consumer -.->|"消费轨迹"| TraceDB
|
|||
|
|
TraceDB -->|"查询"| UI["Trace UI"]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### RocketMQ 消息轨迹原生支持
|
|||
|
|
|
|||
|
|
RocketMQ 内置了消息轨迹功能,这是它相比 Kafka 的一个差异化特性。启用方式非常简单:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Producer 端启用消息轨迹
|
|||
|
|
producer, _ := rocketmq.NewProducer(
|
|||
|
|
producer.WithNameServer([]string{"127.0.0.1:9876"}),
|
|||
|
|
producer.WithTrace(&primitive.TraceConfig{
|
|||
|
|
TraceTopic: "RMQ_SYS_TRACE_TOPIC", // 轨迹数据写入的 Topic
|
|||
|
|
Access: primitive.Local,
|
|||
|
|
}),
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
RocketMQ 的轨迹数据结构大致如下:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type TraceTransferBean struct {
|
|||
|
|
TimeStamp int64 // 时间戳
|
|||
|
|
TraceType string // 生产/消费
|
|||
|
|
MsgId string // 消息 ID
|
|||
|
|
Topic string // Topic
|
|||
|
|
MsgType string // 消息类型
|
|||
|
|
GroupName string // 消费组
|
|||
|
|
Keys string // 业务 Key
|
|||
|
|
CostTime int64 // 处理耗时
|
|||
|
|
Success bool // 是否成功
|
|||
|
|
TraceBeans []TraceBean // 轨迹详情
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
轨迹数据本身也被当作一条消息,写入一个专门的 Trace Topic。这样做的好处是轨迹记录不会影响主业务流程,轨迹数据也可以像普通消息一样被消费和分析。
|
|||
|
|
|
|||
|
|
### 链路集成:与分布式追踪系统对接
|
|||
|
|
|
|||
|
|
消息轨迹解决的是 MQ 维度的问题,但在微服务架构中,一条业务链路往往跨越 HTTP 调用、消息队列、数据库操作等多个维度。这时候需要将 MQ 轨迹集成到分布式追踪系统中。
|
|||
|
|
|
|||
|
|
主流的分布式追踪系统(Jaeger、Zipkin、SkyWalking)都支持通过 TraceID 串联跨服务的调用链。集成的关键是:**在消息 Header 中传播 TraceID**。
|
|||
|
|
|
|||
|
|
Producer 发送消息时,将当前 Span 的 TraceID 写入消息 Header:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"log"
|
|||
|
|
|
|||
|
|
"github.com/segmentio/kafka-go"
|
|||
|
|
"go.opentelemetry.io/otel"
|
|||
|
|
"go.opentelemetry.io/otel/propagation"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func sendMessage(ctx context.Context, topic string, value []byte) {
|
|||
|
|
// 从当前 Context 提取 TraceID,注入到消息 Header
|
|||
|
|
propagator := otel.GetTextMapPropagator()
|
|||
|
|
headers := make(map[string]string)
|
|||
|
|
propagator.Inject(ctx, propagation.MapCarrier(headers))
|
|||
|
|
|
|||
|
|
// 将 TraceID 作为消息 Header 发送
|
|||
|
|
kafkaHeaders := make([]kafka.Header, 0, len(headers))
|
|||
|
|
for k, v := range headers {
|
|||
|
|
kafkaHeaders = append(kafkaHeaders, kafka.Header{Key: k, Value: []byte(v)})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writer := &kafka.Writer{
|
|||
|
|
Addr: kafka.TCP("localhost:9092"),
|
|||
|
|
Topic: topic,
|
|||
|
|
}
|
|||
|
|
defer writer.Close()
|
|||
|
|
|
|||
|
|
err := writer.WriteMessages(ctx, kafka.Message{
|
|||
|
|
Value: value,
|
|||
|
|
Headers: kafkaHeaders,
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("send error: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func consumeMessage(ctx context.Context, msg kafka.Message) {
|
|||
|
|
// 从消息 Header 中提取 TraceID,恢复 Trace 上下文
|
|||
|
|
propagator := otel.GetTextMapPropagator()
|
|||
|
|
headers := make(map[string]string)
|
|||
|
|
for _, h := range msg.Headers {
|
|||
|
|
headers[h.Key] = string(h.Value)
|
|||
|
|
}
|
|||
|
|
ctx = propagator.Extract(ctx, propagation.MapCarrier(headers))
|
|||
|
|
|
|||
|
|
// 在这条 Trace 下记录消费 Span
|
|||
|
|
tracer := otel.Tracer("mq-consumer")
|
|||
|
|
_, span := tracer.Start(ctx, "consume-message")
|
|||
|
|
defer span.End()
|
|||
|
|
|
|||
|
|
// 业务处理...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这段代码展示了 TraceID 的传播机制:Producer 端通过 OpenTelemetry 的 Propagator 将 TraceID 注入消息 Header,Consumer 端从 Header 中提取 TraceID 并恢复 Trace 上下文。这样,一次 HTTP 请求触发的消息发送和消费,就能在 Jaeger 中看到完整的链路视图。
|
|||
|
|
|
|||
|
|
### 消息审计日志
|
|||
|
|
|
|||
|
|
除了技术排查,消息轨迹还有一个重要用途:**审计**。
|
|||
|
|
|
|||
|
|
在金融、电商等场景中,你需要知道"谁在什么时候发了什么消息"、"谁在什么时候消费了什么消息"。这些信息不仅是排查工具,也是合规要求。
|
|||
|
|
|
|||
|
|
审计日志通常记录以下信息:
|
|||
|
|
|
|||
|
|
- 操作人/操作服务的标识
|
|||
|
|
- 操作时间
|
|||
|
|
- 操作类型(生产/消费/删除)
|
|||
|
|
- Topic 和消息 Key
|
|||
|
|
- 消息摘要(不是完整内容,通常是前 256 字节 + Hash)
|
|||
|
|
|
|||
|
|
> [!question]
|
|||
|
|
> 消息轨迹会增加额外的存储和性能开销。如果你的系统每天产生 1 亿条消息,每条消息记录 3 条轨迹(生产/存储/消费),每天就有 3 亿条轨迹数据。生产环境应该如何平衡追踪粒度和性能?采样是一个好策略吗?
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[30-MQ-核心概念与选型]]
|
|||
|
|
- [[33-MQ-Kafka-架构与核心机制]]
|
|||
|
|
- [[35-MQ-与-CDC]]
|
|||
|
|
- [[36-MQ-监控指标与告警]]
|