搭建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

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


编写CallBacks插件

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

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

gormTracing.go:

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:

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:

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接口源码如下:

go 复制代码
// Plugin GORM plugin interface
type Plugin interface {
	Name() string
	Initialize(*DB) error
}

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


单元测试

1. 初始化Jeager

gormTracing_test.go:

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文档

go 复制代码
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. 执行并查看结果

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

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

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

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

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

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

go 复制代码
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()
}

番外:GORM V2 部分源码阅读

GORM V2 部分源码阅读


更多参考:

Jaeger V1.18文档

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

gRPC与分布式链路追踪

全链路监控Jaeger搭建实战

相关推荐
FLGB1 小时前
Flink 与 Kubernetes (K8s)、YARN 和 Mesos集成对比
大数据·flink·kubernetes
Adolf_19931 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼1 小时前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺2 小时前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
新知图书2 小时前
Rust编程的作用域与所有权
开发语言·后端·rust
petaexpress3 小时前
常用的k8s容器网络模式有哪些?
网络·容器·kubernetes
wn5313 小时前
【Go - 类型断言】
服务器·开发语言·后端·golang
希冀1233 小时前
【操作系统】1.2操作系统的发展与分类
后端
华为云开源4 小时前
openGemini 社区人才培养计划:助力成长,培养新一代云原生数据库人才
数据库·云原生·开源