1. 6.3 链路追踪实践

日志与指标之外,本节说明 zmsg.Message 内 Trace 字段zgate.Server.SetTraceHookzactor.SetTraceHooks 的配合方式,以及跨进程时字段如何随 Marshal/Unmarshal 保留。完整 OTel 导出需自行接 SDK(本仓库为 hook 位而非内置 Agent)。

1.1. 6.3.1 为什么需要链路追踪?

一个请求在 zhenyi 中可能经过多条路径:

客户端 → Gate(进程1) → NATS → IM Actor(进程2) → Chat Actor(进程3) → 客户端

当请求变慢或出错,怎么知道瓶颈在哪一段?

  • 日志:能看到单个节点的事件,但不知道上下游关系
  • 指标:能看到聚合统计,但不知道单个请求的路径

链路追踪解决的就是请求级别的问题:一个请求经过了哪些节点,每个节点花了多少时间。

1.2. 6.3.2 zhenyi 的追踪设计

zhenyi 实现了轻量的链路追踪,核心是 Trace ID 透传 + Span 钩子

1.2.1. Trace ID 透传

每条消息携带 128 位 Trace ID(TraceIdHi + TraceIdLo):

type Message struct {
    TraceIdHi uint64  // 高 64 位
    TraceIdLo uint64  // 低 64 位
    SpanId    uint64  // 当前 Span ID
    // ...
}

Trace ID 在请求入口生成,跨 Actor、跨进程传递时保持不变:

Gate 收到客户端消息
    ↓ 生成 TraceIdHi = 雪花算法 ID
    ↓ TraceIdLo = 0
消息投递到 IM Actor
    ↓ TraceIdHi/TraceIdLo 保持不变
    ↓ SpanId 更新为 IM Actor 的 ID
跨进程转发到进程 2
    ↓ TraceIdHi/TraceIdLo 透传
    ↓ 通过 NATS 的消息体携带

1.2.2. Span 钩子

zhenyi 不强制绑定 OpenTelemetry,而是提供钩子接口:

// 伪代码:tracer 为你的 OTel/自研实现
zactor.SetTraceHooks(
    func() bool { return /* 采样或全局开关 */ true },
    func(ctx context.Context, name string) (context.Context, func()) {
        ctx, span := tracer.Start(ctx, name)
        return ctx, func() { span.End() }
    },
    func(ctx context.Context, msg *zmsg.Message) context.Context {
        // 在此用 TraceIdHi/Lo 构造 trace.SpanContext 并 trace.ContextWithSpanContext(ctx, ...)
        return ctx
    },
    func(msg *zmsg.Message) {
        msg.TraceIdHi = zid.NextFast()
    },
)

SetTraceHooks 仅执行一次 sync.Once,须在任意 Actor Run 前注册。)

如果没调用 SetTraceHooksisTraceEnabled() 返回 false,所有追踪代码零开销跳过:

// Dispatcher 中的追踪代码
if isTraceEnabled() {
    ctx, endSpan = traceStartSpan(ctx, obs.spanName)
}
// isTraceEnabled() == false 时,这段代码完全不执行

1.3. 6.3.3 与 OpenTelemetry 的集成

如果需要完整的分布式追踪(导出到 Jaeger、Zipkin),可以集成 OpenTelemetry:

1.3.1. 与 OTel 对接(原则)

  1. 使用 OTel SDK 创建 TracerProvider / Tracer(导出到 Jaeger/OTLP 等由运维选型)。
  2. SetTraceHooksctxFromMsg 中,将 msg.TraceIdHi / TraceIdLo(均为 uint64 规范地填入 trace.TraceID(16 字节),并构造 trace.SpanContext禁止把格式化字符串误当作 TraceIDFromBytes 的输入。
  3. SetTraceHook(Gate) 中,可在入站时把 当前 Span 上下文写回 zmsg.Message,保证与客户端或边缘网关约定一致。

1.3.2. Span 结构(示意)

集成后,每个 Handler 执行会创建一个 Span:

Gate.handler.1001 (收到登录请求)
    ├── IM.handler.2001 (验证用户)
    │     └── DB.query (查数据库)
    └── Gate.reply (回复客户端)

在 Jaeger UI 中可以看到完整的调用链和每个节点的耗时。

1.4. 6.3.4 日志关联

Trace ID 会自动注入到日志中:

// Handler 中记录日志
func (a *IMActor) HandleLogin(ctx context.Context, msg *zmsg.Message) {
    zlog.Info("user login",
        zap.Uint64("traceIdHi", msg.TraceIdHi),
        zap.String("userId", userId))
}

输出:

{"level":"info","msg":"user login","traceIdHi":1234567890123,"userId":"abc123"}

在 ELK 或 Loki 中通过 traceIdHi 可以搜到同一条请求的所有日志:

traceIdHi:1234567890123

1.4.1. Context 方式注入

业务代码也可以从 context 获取 Trace ID:

fields := zlog.TraceZapFields(ctx)
zlog.Info("processing", fields...)

或者手动提取:

if tf, ok := zlog.TraceFieldsFromContext(ctx); ok {
    // tf.TraceIdHi, tf.TraceIdLo, tf.SpanId, tf.MsgId, tf.ActorId
}

1.5. 6.3.5 跨进程追踪

跨进程转发时,Trace ID 通过消息体携带:

// 发送端(Gate)
msg.TarActor = target.Id
buf, _ := msg.MarshalPooled()
zbus.DefaultBus.Broadcast(topic, buf.B)  // buf 包含 TraceIdHi/TraceIdLo

// 接收端(IM Actor)
func handler(topic string, data []byte) {
    msg := zmsg.GetMessage()
    msg.Unmarshal(data)  // 还原 TraceIdHi/TraceIdLo
    actor.Push(zmodel.ActorCmd{Type: zmodel.CmdTypeMsg, Msg: msg})
}

若已接入 OTel,可在订阅回调内 ctxFromMsg + startSpan 创建子 Span(具体顺序依你的包装代码而定)。

1.6. 6.3.6 采样策略

全量追踪开销大(每个 Handler 都创建 Span),生产环境通常采样:

SetTraceHooks 第一个参数 enabled 中接 OTel Sampler 或简单随机开关即可。

建议:

  • 开发环境:100% 采样,方便调试
  • 生产环境:0.1%~1% 采样,或根据错误率动态调整

1.7. 6.3.7 性能开销

场景 开销
未注册 SetTraceHooks isTraceEnabled() 为 false,追踪分支不执行
注册 hook 且 enabled()==false 一次布尔与函数间接;量级远小于典型业务 handler
采样命中 依赖 OTel/SDK 与 Span 属性数量,需实测

未安装 hook 时 isTraceEnabled() 为 false,分发器内追踪分支不执行。

1.8. 6.3.8 实际案例

场景:用户登录慢。

  1. 在 Jaeger 中搜索 traceIdHi = 1234567890123
  2. 看到调用链:
Gate.handler.1001 (120ms)
    ├── IM.handler.2001 (100ms)  ← 瓶颈
    │     └── DB.query (95ms)   ← 真正的问题
    └── Gate.reply (5ms)
  1. 定位到 DB 查询慢,检查 SQL 或索引

场景:消息丢失。

  1. 通过日志 traceIdHi 找到相关日志
  2. 发现消息在 Gate 路由后没有到达 IM Actor
  3. 检查 NATS 指标,发现 zhenyi_nats_publish_errors_total 增加
  4. 定位到 NATS 连接断开

1.9. 6.3.9 本节要点

  1. 载体TraceIdHi / TraceIdLo / SpanIdzmsg 线格式中持久化,NATS/总线传 字节流 即带上。
  2. 入口:Gate SetTraceHook;Actor 管线 SetTraceHooks(一次性)。
  3. 日志zlog.TraceZapFields / TraceFieldsFromContext;Loki/ELK 用 traceIdHi 等关联。
  4. OTel:在 hook 内自洽转换 16 字节 TraceID;避免错误 API 拼字符串。
  5. 采样:由 enabled 或 OTel Sampler 决定。

6.4 节:依赖拓扑、K8s 探针与 SetDraining 等运维约定。

results matching ""

    No results matching ""