搭建Jaeger

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 搭建Jaeger

本篇是对 Golang 上手GORM V2 + Opentracing链路追踪优化CRUD体验(源码阅读) 阅读与实践


该篇相关代码




GORM V2版本开始支持Context上下文传递,支持插件Plugins(有了插件,callback和hook的代码就能更优雅一点)

ORM利用反射,以牺牲一定的性能为代价,快速构建项目


使用Docker搭建Opentracing + jaeger 平台


docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14268:14268 -p 14250:14250 -p 9411:9411 jaegertracing/all-in-one:1.18

微信截图_20230926105601.png

访问 http://localhost:16686/ 如下:

微信截图_20230926105612.png

编写CallBacks插件


CallBacks和Hook不同,前者将伴随GORM的DB对象的整个生命周期,利用CallBacks对GORM框架进行侵入,实现自定义的一些功能


1. 在每次SQL操作前,从context上下文生成子span


gormTracing.go:

package gormTracing
import (
  "github.com/opentracing/opentracing-go"
  "gorm.io/gorm"
)
const gormSpanKey = "__gorm_spqn"
func before(db *gorm.DB) {
  //生成子span。 名字可以自定义
  span, _ := opentracing.StartSpanFromContext(db.Statement.Context, "shuang_gorm_jaeger")
  // 利用db实例去传递span
  // gorm v1.x版本没有InstanceSet,有scope.Set
  db.InstanceSet(gormSpanKey, span)
}

2. 在每次SQL操作后 从DB实例拿到Span并记录数据


gormTracing.go:

func after(db *gorm.DB) {
  _span, isExist := db.InstanceGet(gormSpanKey)
  if !isExist {
    // 不存在则直接抛弃掉
    return
  }
  // 断言 进行类型转换
  span, ok := _span.(opentracing.Span)
  if !ok {
    return
  }
  // 一定要Finish掉
  defer span.Finish()
  // 记录error
  if db.Error != nil {
    span.LogFields(tracerLog.Error(db.Error))
  }
  span.LogFields(tracerLog.String("sql", db.Dialector.Explain(db.Statement.SQL.String(), db.Statement.Vars...)))
}

同样可以非常简单就可以从DB的Setting中,拿到用于处理GORM操作的子Span。

只需要调用span的LogFields方法就能记录下想要的信息


3. 创建结构体,实现gorm.Plugin接口


gormTracing.go:

const (
  callBackBeforeName = "opentracing:before"
  callBackAfterName  = "opentracing:after"
)
type OpentracingPlugin struct{}
func (op *OpentracingPlugin) Name() string {
  return "opentracingPlugin"
}
func (op *OpentracingPlugin) Initialize(db *gorm.DB) (err error) {
  // 开始前 - 并不是都用相同的方法,可自定义
  db.Callback().Create().Before("gorm:before_create").Register(callBackBeforeName, before)
  db.Callback().Query().Before("gorm:query").Register(callBackBeforeName, before)
  db.Callback().Delete().Before("gorm:before_delete").Register(callBackBeforeName, before)
  db.Callback().Update().Before("gorm:setup_reflect_value").Register(callBackBeforeName, before)
  db.Callback().Row().Before("gorm:row").Register(callBackBeforeName, before)
  db.Callback().Raw().Before("gorm:raw").Register(callBackBeforeName, before)
  // 结束后 - 并不是都用相同的方法,可自定义
  db.Callback().Create().After("gorm:after_create").Register(callBackAfterName, after)
  db.Callback().Query().After("gorm:after_query").Register(callBackAfterName, after)
  db.Callback().Delete().After("gorm:after_delete").Register(callBackAfterName, after)
  db.Callback().Update().After("gorm:after_update").Register(callBackAfterName, after)
  db.Callback().Row().After("gorm:row").Register(callBackAfterName, after)
  db.Callback().Raw().After("gorm:raw").Register(callBackAfterName, after)
  return
}
// 告诉编译器这个结构体实现了gorm.Plugin接口
var _ gorm.Plugin = &OpentracingPlugin{}

需要给GORM所有的最终操作(Create、Query、Delete、Update、Row、Raw等), 注册上刚刚编写的两个方法 beforeafter (即在sql执行前要做的操作,和sql执行后要做的操作)


GORM的Plugin接口源码如下:

// Plugin GORM plugin interface
type Plugin interface {
  Name() string
  Initialize(*DB) error
}

只需如上面代码,实现NameInitialize这两个方法,即实现了这个接口




单元测试


1. 初始化Jeager


gormTracing_test.go:

package gormTracing
import (
  "github.com/opentracing/opentracing-go"
  "github.com/uber/jaeger-client-go"
  "github.com/uber/jaeger-client-go/config"
  "io"
)
func initJaeger() (closer io.Closer, err error) {
  // 根据配置初始化Tracer, 返回Closer
  tracer, closer, err := (&config.Configuration{
    ServiceName: "gormTracing",
    Disabled:    false,
    Sampler: &config.SamplerConfig{
      Type: jaeger.SamplerTypeConst,
      // param的值在0到1之间,设置为1则将所有的Operation输出到Reporter
      Param: 1,
    },
    Reporter: &config.ReporterConfig{
      LogSpans:           true,
      LocalAgentHostPort: "localhost:6831",
    },
  }).NewTracer()
  if err != nil {
    return
  }
  // 设置全局Tracer - 如果不设置将会导致上下文无法生成正确的Span
  opentracing.SetGlobalTracer(tracer)
  return
}

2. 实现GORM官方范例


GORM V2文档

package gormTracing
import (
  "context"
  "github.com/opentracing/opentracing-go"
  "github.com/uber/jaeger-client-go"
  "github.com/uber/jaeger-client-go/config"
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
  "io"
  "testing"
)
func initJaeger() (closer io.Closer, err error) {
  // 根据配置初始化Tracer, 返回Closer
  tracer, closer, err := (&config.Configuration{
    ServiceName: "gormTracing",
    Disabled:    false,
    Sampler: &config.SamplerConfig{
      Type: jaeger.SamplerTypeConst,
      // param的值在0到1之间,设置为1则将所有的Operation输出到Reporter
      Param: 1,
    },
    Reporter: &config.ReporterConfig{
      LogSpans:           true,
      LocalAgentHostPort: "localhost:6831",
    },
  }).NewTracer()
  if err != nil {
    return
  }
  // 设置全局Tracer - 如果不设置将会导致上下文无法生成正确的Span
  opentracing.SetGlobalTracer(tracer)
  return
}
type Product struct {
  gorm.Model
  Code  string
  Price uint
}
type User struct {
  gorm.Model
  Id     int
  Name   string
  gender string
}
// V2需要利用Driver来连接MySQL数据库
func Test_GormTracing(t *testing.T) {
  // 1. 初始化Jaeger
  closer, err := initJaeger()
  if err != nil {
    t.Fatal(err)
  }
  defer closer.Close()
  // 2. 连接数据库
  // "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  dsn := "root:12345678@tcp(localhost:3306)/shuang?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
  if err != nil {
    t.Fatal(err)
  }
  // 3. 最重要的一步,使用之前自定义的插件
  _ = db.Use(&OpentracingPlugin{})
  // 迁移 schema ---> 生成对应的数据表
  //_ = db.AutoMigrate(&Product{})
  // 4. 生成新的Span - 注意将span结束掉,不然无法发送对应的结果
  span := opentracing.StartSpan("gormTracing unit test")
  defer span.Finish()
  // 5. 把生成的Root Span写入到Context上下文,获取一个子Context
  // 通常在Web项目中,Root Span由中间件生成
  ctx := opentracing.ContextWithSpan(context.Background(), span)
  // 6. 将上下文传入DB实例,生成Session会话
  // 这样子就能把这个会话的全部信息反馈给Jaeger
  session := db.WithContext(ctx)
  // ---> 下面是官方文档GORM的范例
  // Create
  //session.Create(&Product{Code: "D42", Price: 100})
  //
  //// Read
  //var product Product
  //session.First(&product, 1)                 // 根据整形主键查找
  //session.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录
  //
  //// Update - 将 product 的 price 更新为 200
  //session.Model(&product).Update("Price", 200)
  //// Update - 更新多个字段
  //session.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
  //session.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
  //
  //// Delete - 删除 product
  //session.Delete(&product, 1)
  var user User
  //db.Table("user").Where("id=?", 1).First(&user)
  session.Table("user").Where("id=?", 1).First(&user)
}

3. 执行并查看结果

微信截图_20230926105908.png

微信截图_20230926105917.png

访问Jaeger控制台(localhost:16686),可发现有一条新的记录:

微信截图_20230926105934.png

点击进入查看详情,可以非常清楚看到整个单元测试从开始到结束的SQL执行情况:

总共执行了2条SQL命令,整个过程耗时3.76ms(因为连接的本地库,所以比较快)

微信截图_20230926110027.png

点开对应的Span,可以看到每次GORM操作所执行的SQL命令:

微信截图_20230926110040.png

至此使用OpenTracing对GORM执行过程进行链路追踪已成功实现,从此摆脱需要检索庞大日志查找慢查询、异常和错误的情况,直接一目了然


4. 并发情况下链路追踪的效果

func Test_GormTracing2(t *testing.T) {
  closer, err := initJaeger()
  if err != nil {
    t.Fatal(err)
  }
  defer closer.Close()
  db, err := gorm.Open(mysql.Open("root:12345678@tcp(localhost:3306)/shuang?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{})
  if err != nil {
    t.Fatal(err)
  }
  _ = db.Use(&OpentracingPlugin{})
  rand.Seed(time.Now().UnixNano())
  num, wg := 1<<10, &sync.WaitGroup{}
  wg.Add(num)
  for i := 0; i < num; i++ {
    go func(t int) {
      span := opentracing.StartSpan(fmt.Sprintf("gormTracing unit test %d", t))
      defer span.Finish()
      ctx := opentracing.ContextWithSpan(context.Background(), span)
      session := db.WithContext(ctx)
      p := &Product{Code: strconv.Itoa(t), Price: uint(rand.Intn(1 << 10))}
      session.Create(p)
      session.First(p, p.ID)
      session.Delete(p, p.ID)
      wg.Done()
    }(i)
  }
  wg.Wait()
}

微信截图_20230926110127.png

微信截图_20230926110136.png

番外:GORM V2 部分源码阅读


GORM V2 部分源码阅读




更多参考:

Jaeger V1.18文档

分布式链路追踪:OpenTracing SDK 与 Jaeger 的对接方法

gRPC与分布式链路追踪

全链路监控Jaeger搭建实战

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
中间件
链路追踪学习四:gin集成jaeger
链路追踪学习四:gin集成jaeger
599 0
|
1月前
|
存储 监控 Java
Zipkin/Pinpoint/SkyWalking全面对比
【11月更文挑战第1天】这里重点从探针的性能、Collector的可扩展性、调用链路分析、完整的应用拓扑、对于科技人员使用友好程度(部署安装、埋点接入、使用管理)几个方面来进行对比。
|
7月前
|
存储 Prometheus 监控
当 OpenTelemetry 遇上阿里云 Prometheus
本文以构建系统可观测(重点为指标监控体系)为切入点,对比 OpenTelemetry 与 Prometheus 的相同与差异,后重点介绍如何将应用的 OpenTelemetry 指标接入 Prometheus 及背后原理,最后介绍阿里云可观测监控 Prometheus 版拥抱 OpenTelemetry 及相关落地实践案例,希望能更好的帮助读者更好的理解 OpenTelemetry 及与 Prometheus 的生态融合。
587 0
|
存储 监控 Cloud Native
opentracing(开放分布式追踪) + jaeger初探
以下是小马整理总结的入门理解笔记,助于入门和理解分布式链路追踪,opentracing(开放分布式追踪) + jaeger。
208 0
opentracing(开放分布式追踪) + jaeger初探
|
Java Windows
skywalking03 - skywalking入门使用
skywalking03 - skywalking入门使用
145 0
|
Java 数据库
skywalking05 - skywalking探针插件开发
skywalking05 - skywalking探针插件开发
224 0
|
关系型数据库 MySQL 数据库
skywalking02 - skywalking安装
skywalking02 - skywalking安装
158 0
|
监控 前端开发 数据可视化
Skywalking的安装与使用
Skywalking的安装与使用
680 0
Skywalking的安装与使用
|
存储 数据采集 Ubuntu
Skywalking实战
Skywalking实战
655 0
|
运维 前端开发 程序员
基于Dapper的分布式链路追踪入门——Opencensus+Zipkin+Jaeger
最近做了一些分布式链路追踪有关的东西,写篇文章来梳理一下思路,或许可以帮到想入门的同学。下面我将从原理到demo为大家一一进行讲解,欢迎评论区交流~。
306 0
基于Dapper的分布式链路追踪入门——Opencensus+Zipkin+Jaeger
下一篇
DataWorks