Go 中的可观测性:QueryTracer、Metrics 和 OpenTelemetry 之间如何选择
在将 PostgreSQL 集成到 Go 应用中时,开发者常常需要在可观测性工具之间做出选择。Metrics、QueryTracer 和 OpenTelemetry 解决不同的问题,但很容易混淆。让我们基于一个涉及装饰器和事务的真实案例,拆解何时使用哪一个,以及如何避免常见陷阱。
可观测性的三个层面:指标、追踪和上下文
指标是可观测性的基础层面。它们回答“发生了什么”的问题:例如,部署后错误率激增,或者 p99 端点响应时间从 50 毫秒跃升到 800 毫秒。收集指标资源消耗低,能够实现快速异常检测。然而,指标缺少上下文:它们显示查询变慢了,但无法解释原因。这时就需要追踪来解答。
QueryTracer 是 pgx 驱动提供的接口,用于在驱动层面拦截操作。它为事务的每个阶段生成独立事件:BEGIN、查询本身以及 COMMIT。它不是指标收集工具,而是一个钩子,你可以在其中插入创建 span、记录 SQL 或收集指标的逻辑。重要的是:QueryTracer 本身不发送数据——它仅提供生成数据的机制。
OpenTelemetry (OTel) 处理分布式追踪。在 HTTP 请求处理程序层面创建一个 span,并通过上下文 (context.Context) 传递到调用栈的下层:服务、仓库、驱动。每层添加一个子 span,形成执行树。在可视化系统(如 Jaeger)中,你能看到全貌:哪个具体的数据库查询变慢了,以及在什么上下文中。OTel 持续运行,但使用采样降低开销:例如,仅追踪 10% 的请求。如果请求被采样,则记录完整的 span 树。
装饰器示例:问题隐藏在哪里
在典型项目中,通常在仓库之上使用装饰器来收集指标。下面是一个实现示例:
func WithDBMetrics(operation string, fn func() (T any, error)) (T, error) {
start := time.Now()
result, err := fn()
seconds := time.Since(start).Seconds()
metrics.ObserveDatabaseQueryDuration(operation, seconds)
if err != nil {
metrics.IncDatabaseErrors(operation)
return result, CheckDBError(operation, err)
}
return result, err
}
对于执行单个 SQL 查询的方法,装饰器工作得很精确:指标与查询执行时间匹配。然而,在包含事务的方法中,情况就不同了。考虑这个示例:
func (r *BookingRepo) CreateBooking(ctx context.Context, b *models.Booking) (int64, error) {
const operation = "create_booking"
return WithDBMetrics(operation, func() (int64, error) {
if err := r.validateBooking(b); err != nil {
return 0, err
}
tx, err := r.db.BeginTxx(ctx, nil)
if err != nil {
return 0, err
}
defer func() { _ = tx.Rollback() }()
var id int64
err = tx.QueryRowContext(ctx, createBookingAtomicQuery, ...).Scan(&id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, models.ErrSlotOccupied
}
return 0, err
}
if err = tx.Commit(); err != nil {
return 0, err
}
return id, nil
})
}
装饰器测量整个代码块的时间,包括验证、事务启动、查询执行和提交。create_booking = 45ms 指标无法揭示哪个阶段占用了 40 毫秒。与此同时,QueryTracer 将事务分解为三个独立事件:
- BEGIN = 0.1ms
- INSERT = 40ms
- COMMIT = 2ms
这种粒度对于诊断性能瓶颈至关重要。
实现 QueryTracer:接口与陷阱
QueryTracer 由 pgx 中的多个接口组成,每个接口针对特定操作类型。例如,BatchTracer 用于 Batch 操作:
type BatchTracer interface {
TraceBatchStart(ctx context.Context, conn *Conn, data TraceBatchStartData) context.Context
TraceBatchQuery(ctx context.Context, conn *Conn, data TraceBatchQueryData)
TraceBatchEnd(ctx context.Context, conn *Conn, data TraceBatchEndData)
}
实现需要创建一个满足这些接口的结构体:
type MyTracer struct{}
func (t *MyTracer) TraceQueryStart(ctx context.Context, conn *Conn, data TraceQueryStartData) context.Context {
// Query start: create a span or log
return ctx
}
func (t *MyTracer) TraceQueryEnd(ctx context.Context, conn *Conn, data TraceQueryEndData) {
// Query end: record time, send metric
}
// Attach to config
config.ConnConfig.Tracer = &MyTracer{}
主要陷阱:
- 如果不实现 BatchTracer,Batch 操作将无人察觉。没有错误发生,但数据丢失。
- 对于 OTel 集成,需要在 QueryTracer 方法中手动创建 span 并正确传递上下文。
- QueryTracer 无法取代指标:需要额外逻辑进行聚合。
技术栈比较:sqlx、pgx 和自定义方案
工具选择取决于当前技术栈和细节需求。这里有三种选项:
- sqlx + otelsql
- 设置: 一行代码。
- 优点: 无需更改驱动,支持基本追踪。
- 缺点: 无 Batch 或 CopyFrom 支持,无自定义逻辑。
- 推荐: 适合没有复杂数据库操作的简单项目。
- pgx + otelpgx
- 设置: 一行代码。
- 优点: 完整支持 Batch 和 CopyFrom,自动创建 span。
- 缺点: 需要从 sqlx 切换到 pgx。
- 推荐: 最适合新项目或迁移。
- pgx + MyTracer
- 设置: 手动实现接口。
- 优点: 完全控制(自定义指标、高级日志)。
- 缺点: 复杂度高,需要维护。
- 推荐: 仅用于 otelpgx 未覆盖的特定需求。
重要提示:在所有情况下,都必须将上下文 (context.Context) 传递通过所有应用层——从处理程序到驱动。否则,span 将成为根级别,并丢失与原始用户请求的关联。
关键要点
- 指标显示问题但非原因。 用于监控,而非深度诊断。
- QueryTracer 是追踪钩子,非用于指标。 它将事务分解为阶段以进行详细分析。
- OpenTelemetry 需要正确的上下文传播。 确保 context.Context 流经所有应用层。
- 装饰器适用于简单指标,但不适合事务。 使用 QueryTracer 或 OTel 进行事务内诊断。
- 技术栈选择: 如果使用 sqlx 且无复杂操作,坚持 otelsql。切换到 pgx + otelpgx 以获得完整可见性。
— Editorial Team
暂无评论。