Golang 上手GORM V2 + Opentracing链路追踪优化CRUD体验(源码阅读)
一、前言
系统环境(过几年我翻回来看或许会感慨我当初如此不堪)
go version go1.14.3 windows/amd64
gorm.io/gorm v0.2.31
不会吧不会吧,0202年了,还有人单纯依赖日志去排查CRUD问题?快了解一下链路追踪吧!
六月份前后,比较有名的GORM框架
更新了V2版本,尽管现在依旧在测试阶段,但是我们还是能体验一下框架的一部分新特性 Feature,其中最馋的还是支持Context
上下文传递的特性,结合分布式链路追踪技术,有助于我们服务在分布式部署的情况下精准排查问题。
还是要提及一下,ORM作为辅助工具能帮助我们快速构建项目,追求极致的响应速度应该手撸SQL,但是手撸SQL往往会遇到SQL注入、过程繁琐的问题,所以ORM是一把双刃剑,利用反射牺牲一定的性能以便我们更快上手项目。
文章按例依旧有部分源码分析,之前有做过同类型ORM框架XORM
的链路追踪教程,有兴趣的可以看一下
Golang XORM实现分布式链路追踪(源码分析,分布式CRUD必学)
二、Docker搭建Opentracing + jaeger all in one平台
注:Docker是最简单的,还有其他的方式,有兴趣的朋友可以去翻阅技术文档
使用的镜像:jaegertracing/all-in-one:1.18
Docker命令
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
浏览器访问localhost:16686
,可以看到JaegerUI
界面,如下所示:
三、创建项目
在项目目录下使用控制台输入,懂得都懂
go mod init
go get -u gorm.io/gorm
go get -u github.com/uber/jaeger-client-go
四、编写CallBacks插件
这里的CallBacks和模型的钩子不一样,CallBacks伴随GORM的DB对象整个生命周期,我们需要利用CallBacks对GORM框架进行侵入,以达到操作和访问GORM的DB对象的行为
1. 在每次SQL操作前从context上下文生成子span
通常我们服务(业务)在入口会分配一个根Span,然后在后续操作中会分裂出子Span,每个span都有自己的具体的标识,Finsh之后就会汇集在我们的链路追踪系统中
(OpenTracing的Span示意图,我初学也看不懂,其实也就那样)
文件:gormTracing.go
package gormTracing
// 包内静态变量
const gormSpanKey = "__gorm_span"
func before(db *gorm.DB) {
// 先从父级spans生成子span ---> 这里命名为gorm,但实际上可以自定义
// 自己喜欢的operationName
span, _ := opentracing.StartSpanFromContext(db.Statement.Context, "gorm")
// 利用db实例去传递span
db.InstanceSet(gormSpanKey, span)
return
}
就这么朴实无华的两行代码就能生成子span,按照惯例,我们需要抛弃掉StartSpanFromContext
第二个结果,因为我们不能把父span覆盖掉,同时利用db的Setting(sync.Map)
去寄存子Span
下文会对db.InstanceSet
方法进行源码说明,有兴趣的朋友可以看一下
2. 在每次SQL操作后从DB实例拿到Span并记录数据
文件: gormTracing.go
// 注意,这里重命名了log模块
import tracerLog "github.com/opentracing/opentracing-go/log"
func after(db *gorm.DB) {
// 从GORM的DB实例中取出span
_span, isExist := db.InstanceGet(gormSpanKey)
if !isExist {
// 不存在就直接抛弃掉
return
}
// 断言进行类型转换
span, ok := _span.(opentracing.Span)
if !ok {
return
}
// <---- 一定一定一定要Finsih掉!!!
defer span.Finish()
// Error
if db.Error != nil {
span.LogFields(tracerLog.Error(db.Error))
}
// sql --> 写法来源GORM V2的日志
span.LogFields(tracerLog.String("sql", db.Dialector.Explain(db.Statement.SQL.String(), db.Statement.Vars...)))
return
}
同样非常简单地就能从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"