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

相关推荐
刘大辉在路上4 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
追逐时光者5 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~6 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581366 小时前
InnoDB 的页分裂和页合并
数据库·后端
有一个好名字6 小时前
zookeeper分布式锁模拟12306买票
分布式·zookeeper·云原生
小_太_阳6 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾6 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
星就前端叭7 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
小林coding8 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云