我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [Redis 从 0 到 1(二十九):CPU 突然 100%------commandstats 定位法与 6 步排查链路](#Redis 从 0 到 1(二十九):CPU 突然 100%——commandstats 定位法与 6 步排查链路)
Redis 从 0 到 1(二十九):CPU 突然 100%------commandstats 定位法与 6 步排查链路
上一篇《Redis 从 0 到 1(二十八):Redis 抖一下,重试把它打挂了------超时/重试/熔断的 Go 封装》我们给客户端装上了保险丝,链接:https://blog.csdn.net/2501_92769340/article/details/167400257 。
本篇起进入故障排查弧线,第一案:凌晨告警 Redis CPU 100%,但 QPS 曲线平平------谁在偷 CPU?
一、现象:CPU 100%,QPS 没涨
凌晨 2:47 监控告警:Redis 实例 CPU 从 30% 飙到 100%,持续不降。看 QPS 曲线------和白天持平,没有流量洪峰。看慢查询日志------也是空的。
CPU 高但 QPS 不高,只剩两种可能:每条命令变贵了 (同样的 QPS 吃了更多 CPU),或者有非命令的 CPU 消耗(fork、AOF 重写、碎片整理)。本篇的 6 步链路就是按这个二分法展开的。
二、第一步分诊:CPU 花在哪(收藏点 1)
| 证据 | 指向 |
|---|---|
INFO commandstats 里某命令 usec_per_call 异常高 |
命令变贵(大概率大 key/复杂命令) |
INFO cpu 高但 commandstats 各项正常 |
系统级事件:fork、activedefrag、AOF fsync(第 30 篇) |
LATENCY DOCTOR 报告 fork 类事件 |
同上 |
| 从库 CPU 高主库正常 | 从库在消化积压或跑了自己的慢命令 |
INFO commandstats 每行格式:
cmdstat_hgetall:calls=10000,usec=8000000,usec_per_call=800.00,rejected_calls=0,failed_calls=0
三个数字各管一件事:calls 总量、usec 累计耗时、usec_per_call 平均每次。排序看 usec_per_call 的异常项(排除 get/set 这类微秒级基线后,谁高谁可疑);再用 usec(累计)判断它值不值得管------一条 800μs 的 HGETALL 一天只跑 10 次,不是凶手;跑 10 万次就是。
三、6 步排查链路(收藏点 2)
1. 分诊 INFO cpu + commandstats + LATENCY DOCTOR → 命令消耗 or 系统事件
2. 定位 commandstats 按 usec_per_call 排序,找异常命令
3. 实证 拿到命令参数(slowlog 没有就去客户端日志/monitor 采样),
MEMORY USAGE 确认 key 大小,测试库复现耗时
4. 归因 归类:大 key 全量读 / 热 key 高频读 / 命令滥用 / 系统事件
5. 治理 换命令、拆 key、加本地缓存(第 23 篇)、避开高峰做系统任务
6. 回归 压测复现路径验证,指标回落,写复盘
四、本案复盘:大 key 全量读(收藏点 3)
顺着链路走下来的真实结论:某服务的"配置刷新"每 5 秒对每个实例执行一次 HGETALL config:gateway------看着是配置读取,但这个 Hash 被运营塞了 12 万个 field(历史遗留),单次 HGETALL 平均 800μs。每 5 秒一次 × 20 个实例 = 每秒 4 次 × 800μs ≈ 3.2% CPU,加上其他命令的正常基线,凌晨低峰期 CPU 被它顶到 100%(白天 QPS 高反而被稀释)。
治理方案三步:
- 改读法 :应用只需要 3 个 field,改
HMGET config:gateway f1 f2 f3,耗时从 800μs 到 15μs; - 改频率:配置刷新从 5 秒一次改 60 秒 + 推送触发(第 23 篇本地缓存同款思路);
- 治根本:config Hash 拆出"运营数据"到独立 key(第 22 篇 bigkey 治理的配套动作)。
Go 验证脚本(复现 + 对比治理前后耗时):
go
package main
import (
"context"
"fmt"
"time"
"redis-0to1/redisx"
)
func main() {
rdb, _ := redisx.NewClient(redisx.Config{Addr: "127.0.0.1:6379", Password: "redis123", PoolSize: 10})
ctx := context.Background()
bench := func(name string, fn func()) {
start := time.Now()
for i := 0; i < 100; i++ {
fn()
}
fmt.Printf("%s: 平均 %v/次\n", name, time.Since(start)/100)
}
bench("HGETALL 12万field", func() { rdb.HGetAll(ctx, "config:gateway").Result() })
bench("HMGET 3个field", func() { rdb.HMGet(ctx, "config:gateway", "f1", "f2", "f3").Result() })
}
五、命令清单
| 命令 | 作用 |
|---|---|
INFO cpu |
used_cpu_sys/user,区分内核态与用户态 |
INFO commandstats |
每命令的 calls/usec/usec_per_call |
LATENCY DOCTOR |
系统级延迟事件诊断(fork、defrag) |
SLOWLOG GET |
抓到具体命令参数(配合第 21 章采集器) |
OBJECT ENCODING / MEMORY USAGE |
实证 key 的形态与大小 |
六、排查步骤:CPU 告警后的操作序列
INFO cpu确认是真 100%(不是容器配额问题);INFO commandstats排序找异常命令------2 分钟内出嫌疑名单;- 无嫌疑 →
LATENCY DOCTOR查系统事件,转第 30 篇(碎片/defrag); - 有嫌疑 → slowlog/客户端日志拿参数 → MEMORY USAGE 实证 → 测试库复现;
- 治理后回归压测,CPU 回落曲线写进复盘。
七、总结
- CPU 高 ≠ QPS 高:先分诊"命令消耗"还是"系统事件";
- commandstats 三字段(calls/usec/usec_per_call)组合排序是定位主力;
- 高频 × 中耗时的命令比低频 × 高耗时的更危险;
- 治理三板斧:换命令、降频率、拆 key。
下一篇预告:CPU 第一案破完,下一案是"内存虚高"------下一篇《删了一半数据 RSS 不降------内存碎片与 activedefrag 观测》,我们搞清楚 RSS 和 used_memory 为什么分家。