玩轉 Go 鏈路追蹤

前言

鏈路追蹤是每個微服務架構下必備的利器,go-zero 當然早已經爲我們考慮好了,只需要在配置中添加配置即可使用。

關於 go-zero 如何追蹤的原理追溯,之前已經有同學分享,這裏我就不再多說,如果有想了解的同學去 https://mp.weixin.qq.com/s/hJEWcWc3PnGfWfbPCHfM9g 這個鏈接看就好了。默認會在 api 的中間件與 rpc 的 interceptor 添加追蹤,如果有不瞭解 go-zero 默認如何使用默認的鏈路追蹤的,請移步我的開源項目 go-zero-looklook 文檔 https://github.com/Mikaelemmmm/go-zero-looklook/blob/main/doc/chinese/12-%E9%93%BE%E8%B7%AF%E8%BF%BD%E8%B8%AA.md。

今天我想講的是,除了 go-zero 默認在 api 的 middleware 與 rpc 的 interceptor 中幫我們集成好的鏈路追蹤,我們想自己在某些本地方法添加鏈路追蹤代碼或者我們想在 api 發送一個消息給 mq 服務時候想把整個鏈路包含 mq 的 producer、consumer 穿起來,在 go-zero 中該如何做。

場景

我們先簡單講一下我們的小 demo 的場景,一個請求進來調用 api 的 Login 方法,在 Login 方法中先調用 rpc 的 GetUserByMobile 方法,之後在調用 api 本地的 local 方法,緊接着調用 rabbitmq 傳遞消息到 mq 服務。

go-zero 默認集成了 jaeger、zinpink,這裏我們就以 jaeger 爲例

我們希望看到的鏈路是

api.Login -> rpc.GetUserByMobile

也就是 api 衍生出來三條子鏈路,api.producerMq 有一條調用 mq.Consumer 的子鏈路。

我們想要將一個方法添加到鏈路中需要兩個因素,一個 traceId,一個 span,當我們在同一個 traceId 下開啓 span 把相關的 span 都串聯起來,如果想形成父子關係,就要把 span 之間相互串聯起來,因爲「微服務實踐」公衆號中講解原理太多,我這裏就簡單提一下不涉及過多,如果不是特別熟悉原理可以看文章開頭推薦的文章,這裏我們只需要知道 traceIdspanId 關係就好。

核心業務代碼

1、首先 API 中 LoginLogic 代碼

type LoginLogic struct {
  logx.Logger
  ctx    context.Context
  svcCtx *svc.ServiceContext
}

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
  return &LoginLogic{
    Logger: logx.WithContext(ctx),
    ctx:    ctx,
    svcCtx: svcCtx,
  }
}

type MsgBody struct {
  Carrier *propagation.HeaderCarrier
  Msg     string
}

func (l *LoginLogic) Login(req *types.RegisterReq) (*types.AccessTokenResp, error) {
  resp, err := l.svcCtx.UserRpc.GetUserByMobile(l.ctx, &usercenter.GetUserByMobileReq{
    Mobile: req.Mobile,
  })
  if err != nil {
    return &types.AccessTokenResp{}, nil
  }

  l.local()

  tracer := otel.GetTracerProvider().Tracer(trace.TraceName)
  spanCtx, span := tracer.Start(l.ctx, "send_msg_mq",  oteltrace.WithSpanKind(oteltrace.SpanKindProducer))
  carrier := &propagation.HeaderCarrier{}
  otel.GetTextMapPropagator().Inject(spanCtx, carrier)

  producer := rabbit.NewRabbitmqPublisher(RabbitmqDNS)
  msg :=  &MsgBody{
    Carrier: carrier,
    Msg:     req.Mobile,
  }
  b, err := json.Marshal(msg)
  if err != nil{
    panic(err)
  }

  if err := producer.Publish(spanCtx, ExchangeName, RoutineKeys, b); err != nil {
    logx.Errorf("Publish Fail , msg :%s , err:%v", msg, err)
  }
  span.End()

  return &types.AccessTokenResp{
    AccessExpire: resp.User.Id,
  }, err
}

func (l *LoginLogic) local() {
  tracer := otel.GetTracerProvider().Tracer(trace.TraceName)
  _ , span := tracer.Start(l.ctx, "local", oteltrace.WithSpanKind(oteltrace.SpanKindInternal))
  defer span.End()
  
  // 執行你的代碼 .....
}

2、rpc 中 GetUserByMobile 的代碼

func (s *Logic) GetUserByMobile(context.Context, *usercenterPb.GetUserByMobileReq) (*usercenterPb.GetUserByMobileResp, error) {
  vo := &usercenterPb.UserVo{
    Id: 1,
  }
  return &usercenterPb.GetUserByMobileResp{
    User: vo,
  }, nil
}

3、mq 中 Consumer 的代碼

type MsgBody struct {
  Carrier *propagation.HeaderCarrier
  Msg     string
}

func (c *consumer) Consumer(ctx context.Context, data []byte) error {
  var msg MsgBody
  if err := json.Unmarshal(data, &msg); err != nil {
    logx.Errorf(" consumer err : %v", err)
  } else {
    logx.Infof("consumerOne Consumer  , msg:%+v", msg)

    wireContext := otel.GetTextMapPropagator().Extract(ctx, msg.Carrier)
    tracer := otel.GetTracerProvider().Tracer(trace.TraceName)
    _, span := tracer.Start(wireContext, "mq_consumer_msg", oteltrace.WithSpanKind(oteltrace.SpanKindConsumer))

    defer span.End()
  }

  return nil
}

代碼詳解

1、go-zero 默認集成

當一個請求進入 api 後,我們可以在 go-zero 源碼中查看到 https://github.com/zeromicro/go-zero/blob/master/rest/engine.go#L92。go-zero 已經在 api 的 middleware 中幫我們添加了第一層 trace,當進入 Login 方法內,我們調用了 rpc 的 GetUserByMobile 方法,通過 go-zero 的源碼 https://github.com/zeromicro/go-zero/blob/master/zrpc/internal/rpcserver.go#L55 可以看到在 rpc 的 interceptor 也默認幫我們添加好了,這兩層都是 go-zero 默認幫我們做好的。

2、本地方法

當調用完 rpc 的 GetUserByMobile 之後,api 調用了本地的 local,如果我們想在整個鏈路上體現出來調用了本地 local 方法,那默認的 go-zero 是沒有幫我們做的,需要我們手動來添加。

  tracer := otel.GetTracerProvider().Tracer(trace.TraceName)
  _ , span := tracer.Start(l.ctx, "local",  oteltrace.WithSpanKind(oteltrace.SpanKindInternal))
  defer span.End()
 
// 執行你的代碼 .....

我們通過上面代碼拿到 tracer,ctx 之後開啓一個 local 的 span,因爲 start 時候會從 ctx 獲取父 span 所以會將 local 方法與 Login 串聯起父子調用關係,這樣就將本次操作加入了這個鏈路

3、mq 的 producer 到 mq 的 consumer

我們在 mq 傳遞中如何串聯起來這個鏈路呢?也就是形成 api.Login->api.producer->mq.Consumer

想一下原理,雖然跨越了網絡,api 可以通過 header 傳遞,rpc 可以通過 metadata 傳遞,那麼 mq 是不是也可以通過 headerbody 傳遞就可以了,按照這個想法來看下我門的代碼。

  tracer := otel.GetTracerProvider().Tracer(trace.TraceName)
  spanCtx , span := tracer.Start(l.ctx, "send_msg_mq", oteltrace.WithSpanKind(oteltrace.SpanKindProducer))
  carrier := &propagation.HeaderCarrier{}
  otel.GetTextMapPropagator().Inject(spanCtx,carrier)

  producer := rabbit.NewRabbitmqPublisher(RabbitmqDNS)
  msg := &MsgBody{
    Carrier: carrier,
    Msg:     req.Mobile,
  }
  b , err := json.Marshal(msg)
  if err != nil{
    panic(err)
  }

  if err := producer.Publish(spanCtx, ExchangeName, RoutineKeys, b); err != nil {
    logx.Errorf("Publish Fail, msg :%s, err:%v", msg, err)
  }
  span.End()

首先獲取到了這個全局的 tracer,然後開啓一個 producerspan,跟 local 方法一樣,我們開啓 producerspan 時候也是通過 ctx 獲取到上一級父級 span,這樣就可以將 producerspanLogin 形成父子 span 調用關係,那我們想將 producerspan 與 mq 的 consumer 中的 span 形成調用父子關係怎麼做?我們將 api.producerspanCtx 注入到 carrier 中,這裏我們通過 mq 的 bodycarrier 發送給 consumer,發送完成我們 stop 我們的 producer,那麼 producer 的這層鏈路完成了。

隨後我們來看 mq-consumer 在接收到 body 消息之後怎麼做的。

type MsgBody struct {
  Carrier *propagation.HeaderCarrier
  Msg     string
}

func (c *consumer) Consumer(ctx context.Context, data []byte) error {
  var msg MsgBody
  if err := json.Unmarshal(data, &msg); err != nil {
    logx.Errorf(" consumer err : %v", err)
  } else {
    logx.Infof("consumerOne Consumer  , msg:%+v", msg)

    wireContext := otel.GetTextMapPropagator().Extract(ctx, msg.Carrier)
    tracer := otel.GetTracerProvider().Tracer(trace.TraceName)
    _, span := tracer.Start(wireContext, "mq_consumer_msg", oteltrace.WithSpanKind(oteltrace.SpanKindConsumer))

    defer span.End()
  }

  return nil
}

consumer 接收到消息後反序列化出來 Carrier *propagation.HeaderCarrier,然後通過 otel.GetTextMapPropagator().Extract 取出來 api.producer 注入的 wireContext,在通過 tracer.StartwireContext 創建 consumerspan,這樣 consumer 就是 api.producer 的子 span,就形成了調用鏈路關係,最終我們得到的關係就是

api.Login -> rpc.GetUserByMobile

讓我們來調用一下 Logic 方法,看下 jaeger 中的鏈路如果與我們預想的鏈路一致,so happy~

項目地址

go-zero 微服務框架:https://github.com/zeromicro/go-zero

go-zero 微服務最佳實踐項目:https://github.com/Mikaelemmmm/go-zero-looklook

歡迎使用 go-zerostar 支持我們!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xmSar4aG_HVuPvAi6auQOQ