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 应用侧慢查询日志的协同工作关系:
3.5 配置清单与避坑指南
3.5.1 快速配置清单
- MySQL:
SET GLOBAL slow_query_log = ON; long_query_time = 0.5; log_queries_not_using_indexes = ON; - PostgreSQL:
log_min_duration_statement = 200ms; log_line_prefix = '%m [%p] %u@%d '; - Go 应用:实现
database/sql/driver拦截层,输出 JSON 结构化日志,阈值设为数据库的 1.5 倍。 - 日志收集:使用 Filebeat 或 Vector 采集三端日志,统一写入 Elasticsearch 或 ClickHouse。
- 看板建设:在 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 小时再调整阈值。如果日志量过大,说明阈值设置过松;如果日志量极少,说明阈值可能过紧,需要适当放宽。