我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [Redis 从 0 到 1(二十一):P99 涨到 80ms,慢查询日志 5 步定位法 + Go 采集器](#Redis 从 0 到 1(二十一):P99 涨到 80ms,慢查询日志 5 步定位法 + Go 采集器)
Redis 从 0 到 1(二十一):P99 涨到 80ms,慢查询日志 5 步定位法 + Go 采集器
上一篇《Redis 从 0 到 1(二十):1 亿用户签到一年只占 44MB------Bitmap 的 Go 实现》我们收官了实战弧线,链接:https://blog.csdn.net/2501_92769340/article/details/167399826 。
本篇进入性能优化弧线,第一件事:先学会发现"慢"。
一、现象:P99 悄悄涨了 40 倍,没人知道为什么
监控曲线:Redis 平均延迟 3ms,很健康;但 P99 从 2ms 一路爬到 80ms,已有半个月。平均值被大量快命令稀释,只有尾部分位暴露了真相------有人在单线程里塞了慢命令(第 5 篇:单线程一条慢命令,全局排队)。
要抓它,不需要猜:Redis 自带慢查询日志(slowlog),把超过阈值的命令连参数记下来。问题只在于没人去看它。本篇给出"5 步定位法" + 一个 Go 定时采集器,把 slowlog 从"需要人肉记得查"变成"自动告警"。
二、配置与查看(收藏点 1)
conf
# redis.conf
slowlog-log-slower-than 10000 # 阈值:10000 微秒 = 10ms;设 0 记录所有命令(排查期用)
slowlog-max-len 1024 # 最多记 1024 条,环形覆盖;生产别用默认 128,抓两次突刺就没了
bash
SLOWLOG GET 10 # 看最近 10 条
SLOWLOG LEN # 当前积压条数
SLOWLOG RESET # 清空(采集器消费后可选)
# 每条格式:
# 1) id 2) 时间戳 3) 耗时(微秒) 4) [命令, 参数...] 5) 客户端IP
怎么读数据:耗时 除以 1000 得毫秒;命令+参数 是元凶本身------尤其注意参数:同样 HGETALL,小 Hash 0.1ms,百万 field 的 Hash 能到 500ms(第 22 篇主角)。
三、5 步定位法(收藏点 2)
| 步骤 | 动作 | 产出 |
|---|---|---|
| 1. 采集 | 定时拉 SLOWLOG(见下一节采集器),阈值告警触发即拉 | 原始慢命令列表 |
| 2. 排序 | 按出现次数倒序,看"频率 × 耗时"双高项 | 嫌疑名单(TOP3~5) |
| 3. 归类 | 按命令类型归类:HGETALL 类全量读?KEYS 类扫描?DEL 大 key? | 成因类别(决定治理方向) |
| 4. 复现 | 在测试库执行同参数命令,MEMORY USAGE 看 key 大小 | 实锤:该 key 多大、多慢 |
| 5. 治理 | 换命令(HGET→HMGET 指定字段)、拆 key、加索引结构、限流 | 改动清单 + 回归验证 |
最常见的三类元凶与治理:
| 元凶 | 特征 | 治理 |
|---|---|---|
| 大 Hash/Set 全量读(HGETALL/SMEMBERS) | 命令固定、耗时随数据量涨 | 改 HSCAN 渐进读或只取需要的字段 |
| KEYS * 类扫描(或 SMEMBERS 大集合) | 偶发、耗时极长 | 禁命令(rename-command)+ 改 SCAN |
| DEL 大 key | 与运维操作时间吻合 | 改 UNLINK 或分批渐进删(第 22 篇) |
四、Go 实操:slowlog 定时采集器(收藏点 3)
go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
"redis-0to1/redisx"
)
type SlowEntry struct {
ID int64
Time time.Time
Duration time.Duration
Cmd []string
Client string
}
// FetchSlowLog 拉取慢日志并解析(go-redis 返回 []interface{} 嵌套,手动解包)
func FetchSlowLog(ctx context.Context, rdb *redis.Client, n int64) ([]SlowEntry, error) {
raw, err := rdb.SlowLogGet(ctx, n).Result()
if err != nil {
return nil, err
}
entries := make([]SlowEntry, 0, len(raw))
for _, e := range raw {
// e 结构:[id, timestamp, duration_us, [cmd args], client, name]
id, _ := e[0].(int64)
ts, _ := e[1].(int64)
dur, _ := e[2].(int64)
cmdArgs := e[3].([]interface{})
cmd := make([]string, len(cmdArgs))
for i, a := range cmdArgs {
cmd[i] = fmt.Sprint(a)
}
entries = append(entries, SlowEntry{
ID: id, Time: time.Unix(ts, 0),
Duration: time.Duration(dur) * time.Microsecond, Cmd: cmd,
})
}
return entries, nil
}
func main() {
rdb, err := redisx.NewClient(redisx.Config{Addr: "127.0.0.1:6379", Password: "redis123", PoolSize: 5})
if err != nil {
panic(err)
}
ctx := context.Background()
// 每 60 秒拉一次,超过 50ms 的慢命令打告警日志(接告警系统就换成 webhook)
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for range ticker.C {
entries, err := FetchSlowLog(ctx, rdb, 20)
if err != nil {
log.Printf("拉取 slowlog 失败: %v", err)
continue
}
for _, e := range entries {
if e.Duration > 50*time.Millisecond {
log.Printf("⚠️ 慢命令: %v 耗时=%v", e.Cmd, e.Duration)
// 这里接告警:钉钉/企业微信 webhook,或写入时序库做趋势
}
}
rdb.SlowLogReset(ctx) // 已消费的清掉,防 LEN 顶满丢新数据
}
}
要点:采集器本身要用独立的、池很小的连接(避免和业务抢连接);消费后 RESET 防止环形缓冲覆盖新数据;按分钟聚合告警而不是按条(同一条慢命令刷 100 次只报一次)。
五、命令清单
| 命令 | 作用 |
|---|---|
SLOWLOG GET/LEN/RESET |
查 / 数 / 清 |
CONFIG SET slowlog-log-slower-than 5000 |
在线调阈值(不用重启) |
LATENCY DOCTOR |
服务端延迟事件诊断(比 slowlog 更底层,看系统级卡顿) |
INFO commandstats |
各命令累计耗时(第 29 篇 CPU 排查的主角) |
六、排查步骤:P99 升高时的检查顺序
SLOWLOG GET 10直接看有没有慢命令(5 秒出结果);- 没有慢命令但 P99 高 → 看
LATENCY DOCTOR,可能是 fork、AOF fsync 等系统级事件(第 29/30 篇); - 有慢命令 → 5 步定位法走一遍,归类治理;
- 治理后回归:压测同参数命令,确认 P99 回落。
七、总结
- 平均值会骗你,P99 才暴露慢命令;slowlog 是现成的证据库;
- 5 步定位法:采集 → 排序 → 归类 → 复现 → 治理;
- 慢命令三类元凶:大 key 全量读、扫描类命令、DEL 大 key;
- 采集器自动化:定时拉取 + 阈值告警 + 消费后 RESET。
下一篇预告:慢命令里最常见的元凶是 bigkey------下一篇《一个 500MB 的 Hash 拖垮实例------bigkey 的 Go 渐进删除》,我们讲怎么安全地找到它、删掉它、并且永远不再养出它。