玩转 Go 链路追踪

本文涉及的产品
可观测链路 OpenTelemetry 版,每月50GB免费额度
简介: 玩转 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

相关实践学习
基于OpenTelemetry构建全链路追踪与监控
本实验将带领您快速上手可观测链路OpenTelemetry版,包括部署并接入多语言应用、体验TraceId自动注入至日志以实现调用链与日志的关联查询、以及切换调用链透传协议以满足全链路打通的需求。
分布式链路追踪Skywalking
Skywalking是一个基于分布式跟踪的应用程序性能监控系统,用于从服务和云原生等基础设施中收集、分析、聚合以及可视化数据,提供了一种简便的方式来清晰地观测分布式系统,具有分布式追踪、性能指标分析、应用和服务依赖分析等功能。 分布式追踪系统发展很快,种类繁多,给我们带来很大的方便。但在数据采集过程中,有时需要侵入用户代码,并且不同系统的 API 并不兼容,这就导致了如果希望切换追踪系统,往往会带来较大改动。OpenTracing为了解决不同的分布式追踪系统 API 不兼容的问题,诞生了 OpenTracing 规范。OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。Skywalking基于OpenTracing规范开发,具有性能好,支持多语言探针,无侵入性等优势,可以帮助我们准确快速的定位到线上故障和性能瓶颈。 在本套课程中,我们将全面的讲解Skywalking相关的知识。从APM系统、分布式调用链等基础概念的学习加深对Skywalking的理解,从0开始搭建一套完整的Skywalking环境,学会对各类应用进行监控,学习Skywalking常用插件。Skywalking原理章节中,将会对Skywalking使用的agent探针技术进行深度剖析,除此之外还会对OpenTracing规范作整体上的介绍。通过对本套课程的学习,不止能学会如何使用Skywalking,还将对其底层原理和分布式架构有更深的理解。本课程由黑马程序员提供。
相关文章
|
21天前
|
存储 监控 Go
带你十天轻松搞定 Go 微服务系列(九、链路追踪)
带你十天轻松搞定 Go 微服务系列(九、链路追踪)
|
2月前
|
监控 Kubernetes Go
全链路追踪 & 性能监控,GO 应用可观测全面升级
当前,大多数面向 Golang 应用的监控能力主要是通过 SDK 方式接入,需要开放人员手动进行埋点,会存在一定问题。对此,可观测 Go Agent 应运而生。本文介绍的阿里云可观测 Go Agent 方案,能通过无侵入的方式实现应用监控能力。
108397 23
|
1月前
|
SQL NoSQL Go
Go - 实现项目内链路追踪(二)
Go - 实现项目内链路追踪(二)
25 1
|
1月前
|
SQL JSON NoSQL
Go - 实现项目内链路追踪
Go - 实现项目内链路追踪
24 0
|
11月前
|
中间件 Go API
Go语言微服务框架 - 9.分布式链路追踪-OpenTracing的初步引入
我们从API层到数据库层的链路已经打通,简单的CRUD功能已经可以快速实现。 随着模块的增加,我们会越发感受到系统的复杂性,开始关注系统的可维护性。这时,有个名词会进入我们的视野:**分布式链路追踪**
130 0
|
Prometheus 监控 Cloud Native
Go微服务架构实战 下篇:1. gRPC + Opentracing + Zipkin实现分布式链路追踪系统
Go微服务架构实战 下篇:1. gRPC + Opentracing + Zipkin实现分布式链路追踪系统
|
存储 Prometheus 监控
(16)go-micro微服务jaeger链路追踪
(16)go-micro微服务jaeger链路追踪
219 0
|
消息中间件 监控 NoSQL
工作用Go: 异步任务怎么写4 | Trace: 异步任务还能进行链路追踪么?
工作用Go: 异步任务怎么写4 | Trace: 异步任务还能进行链路追踪么?
536 0
工作用Go: 异步任务怎么写4 | Trace: 异步任务还能进行链路追踪么?
|
存储 消息中间件 运维
Go 分布式链路追踪实现原理
在分布式、微服务架构下,应用一个请求往往贯穿多个分布式服务,这给应用的故障排查、性能优化带来新的挑战。分布式链路追踪作为解决分布式应用可观测问题的重要技术,愈发成为分布式应用不可缺少的基础设施。本文将详细介绍分布式链路的核心概念、架构原理和相关开源标准协议,并分享我们在实现无侵入 Go 采集 Sdk 方面的一些实践。
569 0
Go 分布式链路追踪实现原理
|
存储 消息中间件 运维
一文详解|Go 分布式链路追踪实现原理
在分布式、微服务架构下,应用一个请求往往贯穿多个分布式服务,这给应用的故障排查、性能优化带来新的挑战。分布式链路追踪作为解决分布式应用可观测问题的重要技术,愈发成为分布式应用不可缺少的基础设施。本文将详细介绍分布式链路的核心概念、架构原理和相关开源标准协议,并分享我们在实现无侵入 Go 采集 Sdk 方面的一些实践。
1385 0
一文详解|Go 分布式链路追踪实现原理