笔者曾经分享过两篇文章,分别是基于 GORM V2 和 XORM 在分布式链路追踪上的建设,此后偶尔有网友联系笔者进行交流,主要关于项目使用 GORM V1 或者原生 SQL情况下,尽可能少侵入业务代码去做数据库操作的日志输出、错误监控和链路追踪。
本系列文章通过四点内容为所有 Go 业务上的 SQL 操作日志输出、监控和链路追踪问题提供解决思路。
- 利用 SQLHooks 在 sql.Driver 上挂载钩子函数。
- ORM 、SQL 以及 SQLX 的实践。
- Prometheus 采集 DB 操作指标。
- Opentracing 链路追踪。
本文内容主要为第一步和第二步,后续 Prometheus 和 Opentracing 相关内容日后有机会更新,文章所用到的代码均已开源,有问题可以自行查阅,Github - sqlhooks-example 。
自定义驱动
众所周知 database/sql
原生库提供的是 interface{}
接口定义,在进行数据库操作时通常都是借助 driver.Driver
和 driver.Conn
进行的,关于这部分内容可以阅读 Go 语言设计与实现 - 数据库 内容进行了解。
既然如此,我们只需要在 Diver 和 Conn 上面封装一层就能实现全量 SQL 日志打印和监控。
在尽量避免造轮子的前提下,笔者借助 Github 开源项目 SQLHooks 进行实践。值得注意的是,如果您需要应用到生产环境,可参考开源项目自行封装。
SQLHooks 的原理非常简单,封装了一个 Driver 实现原生库 driver.Driver
,在调用 Exec、Query 以及 Prepare 等操作函数时调用开发者传入的钩子函数。
1 | go复制代码// Driver implements a database/sql/driver.Driver |
此时,有开发经验的 Gopher 已经注意到 Hooks
接口的方法都带有 context.Context
参数,大概率已经猜到下文的操作。
以打印完整 SQL 和 Args 参数为例,我们可以定义一个包括日志打印对象的结构体实现 Hooks
接口。
为了能更好呈现效果,本文的实践中加入了 SQL 耗时,通常该功能是在数据库中实现并呈现给 DBA 人员查询,但我们开发人员一般也需要该指标用于确定 SQL 质量。
首先我们定义 zapHook
结构体,该结构体包括一个 zap logger
对象和用于启用 SQL 耗时计算的布尔值。
1 | go复制代码// make sure zapHook implement all sqlhooks interface. |
接下来我们需要定义 Before 函数需要做的两件事。
- 输出实际执行 SQL 的 Query 命令和参数日志。
- 将执行 SQL 的开始时间对象注入到上下文。
1 | go复制代码func buildQueryArgsFields(query string, args ...interface{}) []zap.Field { |
按照相同的流程,我们需要定义 After 函数需要做的流程。
- 尝试从上下文获取执行 SQL 的开始时间对象。
- 输出执行 SQL 完毕的 Query 和参数日志(通常仅在 Before 函数输出一次,但本文实践为了效果进行了二次输出)。
1 | go复制代码func (z *zapHook) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) { |
我们还需要完善 OnError
发生错误时的钩子函数,这个函数通常是必要的,我们希望在 SQL 执行失败后进行日志输出或上报指标和原因。
1 | go复制代码func (z *zapHook) OnError(_ context.Context, err error, query string, args ...interface{}) error { |
至此我们已经完成了 SQLHooks 所有接口的实现,最后一步是使用 SQLHooks 库提供的 Wrap 方法创建新的 Driver 注册到全局驱动上。
1 | go复制代码// 大部分 MySQL 操作都使用 go-sql-driver 作为驱动. |
接下来我们就可以借助该钩子函数输出全部 DB 框架生成的 SQL 的日志。
业务实践
在进行本文实践过程前,您需要了解本文基于以下环境进行。
- Go 1.17
- go-sql-driver v1.6.0
- SQLX 1.3.4
- GORM 1.22.3
- TiDB 5.2.2
接下来将会分成四个环节进行实践。
- 定义测试表结构和 Go 结构体。
- 查看 Go 原生 SQL 执行和查询效果。
- 查看 SQLX 框架执行和查询效果。
- 查看 GORM 框架执行和查询效果。
定义测试表结构和 Go 结构体
首先笔者手动定义了一个简单的数据库表结构体并创建。
1 | sql复制代码CREATE TABLE `test_table` |
接下来需要在 Go 代码中编写对应的结构体,此步骤可以通过各类 SQL 转 struct 工具简化。
1 | go复制代码import "gorm.io/gorm/schema" |
在编写业务代码之前,我们需要进行必要的初始化流程。
1 | go复制代码var ctx = context.Background() |
至此准备工作已经完成,接下来我们需要依照对应的框架编写业务代码,统一的流程是插入后查询。
Go 原生库 SQL
原生库 SQL 是 Go 开发常用的数据库操作库。
因为基于原生库的批量插入需要借助字符串拼接,为了简化流程,原生库的实践仅展示单个插入的过程。
1 | go复制代码// database/sql |
编写完业务代码之后我们可以运行单元测试检验效果。
1 | shell复制代码=== RUN Test_zapHook |
从日志输出来看,确实实现了我们想要的效果 —— 日志输出和 SQL 执行耗时。
SQLX 框架
SQLX 是一款 Go 业务开发过程中比较常见的数据库操作框架,目前 Github 上有 11.1k 的 star。
得益于 SQLX 支持命名参数,且命名参数支持切片,笔者可以演示批量插入的场景。
1 | go复制代码// jmoiron/sqlx |
运行单元测试我们获得对应的输出效果。
1 | shell复制代码=== RUN Test_zapHook |
SQLX 框架和原生库 SQL 的输出效果是一样的,同样可以得到执行的 SQL 和耗时。
GORM 框架
高度封装的 GORM 框架获取对应的 SQL 和执行时间难度高(特别是 V1 版本),而基于驱动的方式能磨平原生库与框架的差异,在数据库操作入口和出口捕捉我们所需要的 SQL 信息。
注意观察以下代码,我们只需要在 DB 对象创建时调整参数即能调用想要的 Hooks 钩子函数。
1 | go复制代码// GORM V2, need to focus driver name is zapDriverName. |
运行单元测试,我们可以得到输出效果。
1 | shell复制代码=== RUN Test_zapHook |
日志输出的效果和预期相同,都能正常输出实际执行的 SQL 和耗时。
值得注意的是,尽管我们没有编写额外的内容,但 GORM 框架依然在初始化过程执行了 SELECT VERSION()
命令。
注意事项
笔者认为本文的内容比较基础,能给部分有需求的同学提供解决问题的思路,但仍有需要注意的事项。
- 本文基于 SQLHooks 开源库进行编写,值得注意的是,在生产环境下,基建工程尽量自行封装或者提供配置开关。
- 输出还是采集都是开销较大的工作,可提供参数关闭或者调整采集概率。
- 在条件允许的情况下,核心业务减少或避免使用 ORM,降低人为误操的风险。
非常感谢您的阅读,如果您有更好的想法或问题,欢迎私信笔者。
参考资料
本文转载自: 掘金