3. 慢查询日志的开启与配置

慢查询日志是数据库性能诊断的“黑匣子”。在生产环境中,没有慢查询日志,性能复盘就如同盲人摸象。本章将分别讲解 MySQL、PostgreSQL 以及 Go 应用侧日志的开启与配置方法,帮助你建立完整的“数据库 + 应用”双端日志体系。

3.1 MySQL 慢查询日志

MySQL 的慢查询日志记录了执行时间超过指定阈值(long_query_time)的 SQL 语句。它是排查 SQL 性能瓶颈最直接的工具。

3.1.1 参数配置

MySQL 慢查询日志的核心参数如下表所示:

参数名 默认值 说明
slow_query_log OFF 是否开启慢查询日志,ON 表示开启
slow_query_log_file hostname-slow.log 慢查询日志文件路径
long_query_time 10.000000 慢查询阈值(秒),建议生产环境设为 0.1 ~ 1
log_queries_not_using_indexes OFF 是否记录未使用索引的查询,建议开启
log_slow_admin_statements OFF 是否记录慢管理语句(如 ALTER TABLE)
min_examined_row_limit 0 扫描行数超过此值才记录,可用于过滤小表全表扫描

3.1.2 动态开启与持久化

推荐使用 SET GLOBAL 动态开启,避免重启数据库:

-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;

-- 设置阈值 0.5 秒
SET GLOBAL long_query_time = 0.5;

-- 记录未使用索引的查询
SET GLOBAL log_queries_not_using_indexes = ON;

-- 确认配置生效
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
注意: 动态配置在 MySQL 重启后会失效。如需持久化,请将参数写入 my.cnf 或 my.ini 的 [mysqld] 段。

3.1.3 日志分析示例

一条典型的 MySQL 慢查询日志记录如下:

# Time: 2025-03-21T10:15:30.123456Z
# User@Host: go_app[go_app] @  [10.0.0.1]  Id: 12345
# Query_time: 2.345678  Lock_time: 0.000123  Rows_sent: 10  Rows_examined: 500000
SET timestamp=1742544930;
SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at DESC;

关键字段解读:

  • Query_time:SQL 执行总耗时(2.35 秒),是判断慢查询的核心指标。
  • Lock_time:锁等待时间(0.000123 秒),过高可能表示锁竞争。
  • Rows_examined:扫描行数(50 万行),远大于返回行数(10 行),说明缺少索引或查询条件不优。

3.2 PostgreSQL 慢查询日志

PostgreSQL 的慢查询日志通过 log_min_duration_statement 参数控制,记录执行时间超过指定毫秒数的所有语句。

3.2.1 核心配置参数

参数名 默认值 说明
log_min_duration_statement -1 慢查询阈值(毫秒),-1 表示关闭,0 表示记录所有语句
log_line_prefix %m [%p] 日志行前缀,建议包含时间、进程 ID、用户、数据库名
log_statement none 记录语句级别:none、ddl、mod、all
log_duration off 是否记录每条语句的持续时间,建议与 log_min_duration_statement 配合使用

3.2.2 配置示例

编辑 postgresql.conf:

# 记录执行时间超过 200ms 的语句
log_min_duration_statement = 200

# 日志前缀:时间 [进程ID] 用户@数据库
log_line_prefix = '%m [%p] %u@%d '

# 记录所有 DDL 语句
log_statement = 'ddl'

# 重启生效
pg_ctl reload

3.2.3 日志分析示例

PostgreSQL 慢查询日志格式:

2025-03-21 10:15:30.123 UTC [12345] go_app@mydb LOG:  duration: 2345.678 ms  statement: SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at DESC;
建议: 使用 pgBadger 或 pt-query-digest 对 PostgreSQL 慢查询日志进行聚合分析,快速定位 TOP N 慢 SQL。

3.3 Go 应用侧日志

数据库慢查询日志只能看到 SQL 执行层面的耗时,但无法感知 Go 应用内部的调用链路、连接池状态、业务逻辑耗时。因此,Go 应用侧必须记录“应用层慢查询日志”。

3.3.1 日志结构设计

一条完整的 Go 应用侧慢查询日志应包含以下字段:

字段 类型 说明
trace_id string 链路追踪 ID,用于关联上下游
sql string 执行的 SQL 语句(可脱敏)
duration_ms float64 应用层感知的耗时(毫秒)
db_duration_ms float64 数据库返回的耗时(毫秒)
conn_wait_ms float64 从连接池获取连接的等待时间
rows_affected int64 影响行数
error string 错误信息(如有)
caller string 调用位置(文件:行号)

3.3.2 基于数据库驱动的拦截实现

推荐使用 database/sql 的 DB.SetConnMaxLifetime 和 DB.SetMaxIdleConns 管理连接池,同时通过自定义 driver.Driver 包装层拦截 SQL 执行:

package sqllog

import (
    "context"
    "database/sql/driver"
    "fmt"
    "log"
    "time"
)

type SlowLogDriver struct {
    driver.Driver
    slowThreshold time.Duration
}

func (d *SlowLogDriver) Open(name string) (driver.Conn, error) {
    conn, err := d.Driver.Open(name)
    if err != nil {
        return nil, err
    }
    return &slowLogConn{Conn: conn, slowThreshold: d.slowThreshold}, nil
}

type slowLogConn struct {
    driver.Conn
    slowThreshold time.Duration
}

func (c *slowLogConn) Prepare(query string) (driver.Stmt, error) {
    stmt, err := c.Conn.Prepare(query)
    if err != nil {
        return nil, err
    }
    return &slowLogStmt{Stmt: stmt, query: query, slowThreshold: c.slowThreshold}, nil
}

type slowLogStmt struct {
    driver.Stmt
    query         string
    slowThreshold time.Duration
}

func (s *slowLogStmt) Exec(args []driver.Value) (driver.Result, error) {
    start := time.Now()
    result, err := s.Stmt.Exec(args)
    elapsed := time.Since(start)
    if elapsed >= s.slowThreshold {
        log.Printf("[SLOW] query=%s args=%v duration=%v", s.query, args, elapsed)
    }
    return result, err
}

func (s *slowLogStmt) Query(args []driver.Value) (driver.Rows, error) {
    start := time.Now()
    rows, err := s.Stmt.Query(args)
    elapsed := time.Since(start)
    if elapsed >= s.slowThreshold {
        log.Printf("[SLOW] query=%s args=%v duration=%v", s.query, args, elapsed)
    }
    return rows, err
}

3.3.3 结构化日志输出

使用 log/slog 输出 JSON 格式的结构化日志,便于日志中心采集和分析:

import (
    "context"
    "log/slog"
    "time"
)

type SlowQueryEvent struct {
    TraceID      string        `json:"trace_id"`
    SQL          string        `json:"sql"`
    DurationMs   float64       `json:"duration_ms"`
    ConnWaitMs   float64       `json:"conn_wait_ms"`
    RowsAffected int64         `json:"rows_affected"`
    Caller       string        `json:"caller"`
}

func LogSlowQuery(ctx context.Context, event SlowQueryEvent) {
    slog.LogAttrs(ctx, slog.LevelWarn, "slow_query",
        slog.String("trace_id", event.TraceID),
        slog.String("sql", event.SQL),
        slog.Float64("duration_ms", event.DurationMs),
        slog.Float64("conn_wait_ms", event.ConnWaitMs),
        slog.Int64("rows_affected", event.RowsAffected),
        slog.String("caller", event.Caller),
    )
}
最佳实践: 应用侧慢查询阈值建议设置为数据库侧阈值的 1.5~2 倍。例如数据库阈值 200ms,应用侧设为 300ms,这样可以捕获到“数据库执行快但应用层因连接池等待、网络延迟等原因整体变慢”的场景。

3.4 三端日志体系总览

下图展示了 MySQL、PostgreSQL 和 Go 应用侧慢查询日志的协同工作关系:

三端慢查询日志体系 Go 应用 应用侧慢查询日志 MySQL 慢查询日志 PostgreSQL 慢查询日志 日志收集层(Filebeat / Fluentd / Vector) 存储与分析层(Elasticsearch / ClickHouse + Grafana) 告警与复盘(AlertManager + 慢查询看板) 三端日志统一时间戳

3.5 配置清单与避坑指南

3.5.1 快速配置清单

  1. MySQL:SET GLOBAL slow_query_log = ON; long_query_time = 0.5; log_queries_not_using_indexes = ON;
  2. PostgreSQL:log_min_duration_statement = 200ms; log_line_prefix = '%m [%p] %u@%d ';
  3. Go 应用:实现 database/sql/driver 拦截层,输出 JSON 结构化日志,阈值设为数据库的 1.5 倍。
  4. 日志收集:使用 Filebeat 或 Vector 采集三端日志,统一写入 Elasticsearch 或 ClickHouse。
  5. 看板建设:在 Grafana 中创建“慢查询趋势”、“TOP 10 慢 SQL”、“连接池等待时间”等面板。

3.5.2 常见避坑点

场景 问题 解决方案
MySQL 日志文件过大 慢查询日志未轮转,撑爆磁盘 配置 logrotate 或使用 SET GLOBAL slow_query_log = OFF; 配合定时任务切割
PostgreSQL 日志重复 同时开启了 log_statement = 'all' 和 log_min_duration_statement,导致重复记录 只使用 log_min_duration_statement,关闭 log_statement
Go 应用日志丢失 应用侧日志未使用缓冲写入,高并发下丢失 使用 slog 的 HandlerOptions 开启异步写入,或对接 zerolog 的 Write 接口
时间戳不一致 数据库日志使用 UTC,Go 应用使用本地时间,导致关联分析困难 统一使用 UTC 时间戳,Go 应用日志中显式标注时区
复盘提示: 开启慢查询日志后,建议观察 24 小时再调整阈值。如果日志量过大,说明阈值设置过松;如果日志量极少,说明阈值可能过紧,需要适当放宽。