不能直接用 database/sql 原生接口做耗时监控,因其抽象层不暴露执行前后的钩子;需通过代理 driver.driver,在 open 返回的 monitoredconn 中实现 driver.conn 及扩展接口,并在 querycontext/execcontext 等方法中打点计时。

为什么不能直接用 database/sql 的原生接口做耗时监控
因为 database/sql 提供的是抽象层,所有执行入口(如 Query、Exec)最终都走 driver.Conn 接口,但这个接口本身不暴露调用前后的钩子。你无法在不侵入驱动源码的前提下,在每次 SQL 执行前后自动打点——除非你控制连接的创建和使用路径。
如何包装 driver.Driver 实现透明耗时采集
核心思路是实现一个代理 driver.Driver,在 Open 返回的 driver.Conn 上再套一层带计时逻辑的 wrapper。关键点在于:必须让 wrapper 同时满足 driver.Conn 和 driver.Execer/driver.Queryer 等可选接口,否则某些驱动(如 pgx 的 stdlib 适配层)会跳过你的包装直接调用底层。
- 代理
Driver.Open方法,返回自定义的monitoredConn -
monitoredConn需实现driver.Conn+ 所有它底层实际支持的扩展接口(比如driver.QueryerContext) - 在
QueryContext、ExecContext等方法里用time.Now()打点,记录 SQL 文本、参数、耗时、错误,并透传调用到底层Conn - 注意:不要在
Close里埋点,连接复用下Close不代表 SQL 执行结束
sql.Register 注册包装驱动时的常见陷阱
注册新驱动名(如 "mysql_monitored")后,必须确保应用代码显式使用该名字打开 DB,否则监控无效。很多人误以为能“全局劫持” "mysql" 或 "postgres",但 Go 的 sql.Register 是按名称查表,不会覆盖已注册驱动。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 不能重名注册,否则 panic:
sql: Register called twice for driver "mysql" - 如果用
github.com/go-sql-driver/mysql,需先import _ "github.com/go-sql-driver/mysql",再注册自己的包装驱动 - SQL 日志中看到
context canceled或context deadline exceeded时,耗时应以 context 结束时间为准,而非底层驱动返回时间 - 参数打印要限制长度(如
fmt.Sprintf("%v", args)[:200]),避免大 blob 或 slice 拖慢监控逻辑
性能开销与生产环境取舍
每次 SQL 调用额外增加一次函数调用、一次 time.Now()、一次 map 写入(如果上报到内存 buffer),实测单次开销约 200–500ns,对 QPS > 1k 的服务影响明显。真正上线前必须关掉 debug 级日志、禁用 SQL 文本全量采集、聚合后异步上报。
- 避免在 hot path 做 JSON 序列化或网络写入;建议用 ring buffer + 单独 goroutine 批量 flush
- 区分采样率:开发环境 100%,预发 10%,线上 0.1% —— 通过
rand.Float64() 控制 - 注意
context.Context可能被 cancel,务必用ctx.Err()判断是否超时,而不是只看 error == nil
最易被忽略的是驱动接口兼容性:不同数据库驱动实现的上下文接口不一致(比如 QueryerContext vs Queryer),漏实现某个接口会导致降级调用原始方法,监控就断了。必须对照底层驱动源码,把它的所有 Conn 接口都补全。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










