Redis 阻塞与延迟定位
Redis 执行命令始终是单线程的(Redis 6 起只是把网络读写改成多线程),这意味着一条耗时 500ms 的命令会让后面所有请求跟着排队 500ms。排查延迟问题要回答的第一个问题是:慢是命令本身慢,还是 Redis 被别的东西挡住了。
两类“慢”必须先分清
| 类型 | 特征 | 典型来源 | 判断方法 |
|---|---|---|---|
| 命令本身慢 | 只有特定接口或特定命令慢 | KEYS、大对象 HGETALL、长 Lua 脚本、大 ZSET 的 ZRANGE | slowlog get 里能看到该命令 |
| 被阻塞 | 所有命令一起慢,表现为尖刺式抖动 | RDB fork、AOF 重写、swap、透明大页、慢客户端 | slowlog 干净但延迟毛刺明显 |
分的意义在于处置完全不同:前者改代码,后者改配置与部署。判断错了会一直在错误的方向上优化。
第一步:slowlog 确认有没有慢命令
# 单位是微秒,默认 10000(即 10ms);设为 -1 表示关闭
redis-cli config get slowlog-log-slower-than
redis-cli config set slowlog-log-slower-than 10000
# 默认 128 条,只记录不落盘,重启丢失
redis-cli config get slowlog-max-len
redis-cli slowlog len
redis-cli slowlog get 10
redis-cli slowlog reset
SLOWLOG GET 每条记录有六个字段:
| 序号 | 字段 | 判读要点 |
|---|---|---|
| 1 | 日志 ID | 唯一递增,用于确认是否持续新增 |
| 2 | 发生时间戳 | 换算成时间后与业务告警时间对齐 |
| 3 | 执行耗时(微秒) | 真正需要看的数字,10ms 与 500ms 是两个量级的问题 |
| 4 | 命令与参数 | 参数超长会被截断,但足够看出是哪个 key |
| 5 | 客户端地址 | 定位是哪个应用实例、哪台机器发来的 |
| 6 | 客户端名称 | 前提是客户端设置了 CLIENT SETNAME,建议强制规范 |
关键限制:slowlog 只统计命令执行时间,不包含排队等待、网络收发与输出缓冲时间。所以“慢客户端把内存撑爆”这类问题在 slowlog 里完全看不出来,必须看 CLIENT LIST。
第二步:LATENCY 抓毫秒级毛刺
# 阈值单位是毫秒,0 表示关闭;建议 100
redis-cli config set latency-monitor-threshold 100
redis-cli latency latest # 每类事件最近一次与最大耗时
redis-cli latency history fork # 某类事件的时间序列,便于对齐业务告警时间
redis-cli latency reset
常见事件名:command、fork、expire-cycle、eviction-cycle、eviction-del、rdb-unlink-fsync、aof-fsync-always、aof-write。如果毛刺集中在 fork,问题在持久化而不在业务命令。
第三步:区分是机器慢还是 Redis 慢
# 连续跑 100 秒,只测本机内核调度与虚拟化的最大延迟,不连 Redis
redis-cli --intrinsic-latency 100
# 观察一段时间内的延迟分布(-i 是采样间隔秒数)
redis-cli --latency-history -i 5
判读表:
| intrinsic max latency | 结论 | 下一步 |
|---|---|---|
| 小于 1ms | 机器与虚拟化环境正常 | 问题在 Redis 自身,回到 slowlog 与 CLIENT LIST |
| 1ms ~ 10ms | 宿主有轻微抖动 | 关注 CPU steal、NUMA、其他进程干扰 |
| 大于 10ms | 机器本身就不满足低延迟要求 | 换宿主机或专用机型,调 Redis 参数没有意义 |
--latency 与 --latency-history 是阻塞式前台命令,跑完要按 Ctrl+C 退出,别在交互式会话里忘了它。
常见阻塞源与特征
| 阻塞源 | 特征 | 缓解手段 |
|---|---|---|
大 key 的 DEL | 耗时与元素数量成正比,O(N) 且同步 | 改用 UNLINK 异步删除 |
KEYS / FLUSHALL | 全库扫描,几十万 key 就是几百毫秒 | SCAN 分批;FLUSHALL ASYNC |
HGETALL / SMEMBERS 大对象 | 单命令耗时高、网络包巨大 | 改 HMGET 按需取、分页取 |
| RDB fork | 毫秒到几百毫秒的毛刺,与实例内存成正比 | 关闭透明大页、把持久化放到从库 |
| AOF 重写 | 后台线程与 fsync 竞争磁盘 | no-appendfsync-on-rewrite yes,用 SSD |
| swap | rss 小于 used_memory 时的典型信号 | 保证物理内存充足,vm.swappiness 调到 0 或 1 |
| 透明大页 THP | fork 时页表拷贝放大数百倍 | 内核参数设为 never |
| 慢客户端与大 value | 输出缓冲膨胀,内存暴涨 | 见下一节的 omem 判读与限制 |
| 长 Lua 脚本 | 超过 lua-time-limit(默认 100ms)后其他客户端收到 BUSY | 拆短脚本,避免循环内大量命令 |
| 大批量 pipeline | qbuf 暴涨,一次性涌入几十万命令 | 限制单批数量,客户端侧分批提交 |
CLIENT LIST 的 omem 与 qbuf
redis-cli client list | head -20
# omem 最大的客户端(单位字节)
redis-cli client list | sed 's/ /\n/g' | grep '^omem=' | sort -t= -k2 -nr | head -10
# 查询缓冲非零的客户端
redis-cli client list | sed 's/ /\n/g' | grep -E '^qbuf(-free)?=' | head -20
redis-cli config get 'client-output-buffer-limit'
| 字段 | 含义 | 看到什么数值判断什么 |
|---|---|---|
omem | 该客户端输出缓冲占用字节数 | 单个客户端持续大于 10MB:慢消费者(订阅客户端或副本),会被 client-output-buffer-limit 强制断开 |
qbuf | 查询缓冲已用字节数 | 接近 qbuf + qbuf-free 说明客户端一次性发来巨量数据,典型是超大 pipeline |
qbuf-free | 查询缓冲剩余空间 | 长期接近 0 表示该连接一直在堆积未读取的请求 |
multi | 当前事务中已入队命令数 | 大于 -1 且长期不变,说明有事务忘记 EXEC 或 DISCARD |
cmd | 最近一次执行的命令 | 结合 omem 判断是哪类命令在撑大缓冲 |
生产建议:client-output-buffer-limit normal 0 0 0(普通客户端不限制但靠客户端自身规范)、replica 256mb 64mb 60、pubsub 32mb 8mb 60,后两者的硬限制一旦触发会直接断开连接。
处置动作
- 定位到具体命令:换成异步删除(
UNLINK)、分批遍历(SCAN/HSCAN)、按需取值(HMGET),必要时把大 key 拆成多个小 key。 - fork 毛刺:关闭 THP,调整
save策略(例如把 RDB 交给从库做,主库只做 AOF),并保证maxmemory留出足够余量。 - swap 型变慢:这是物理内存问题,先扩容内存并调整 swappiness,参数调优无效。
- 慢客户端:
redis-cli client kill type normal addr 10.0.0.21:53422精确断开,同时收紧client-output-buffer-limit。 - 大批量写入:客户端分批 + 每批之间短暂间隔,避免单个 pipeline 阻塞整个实例。
预防与巡检
- 用
rename-command在生产禁用KEYS、FLUSHALL、MONITOR:rename-command KEYS ""。 - slowlog 阈值设为 10ms 并采集条数与内容,新增即告警;
latency latest纳入监控。 - 内核侧每次上线前确认:THP 为
never、vm.swappiness为 0 或 1、磁盘是 SSD 且 IO 不与其他服务争抢。 - 客户端强制要求:设置
CLIENT SETNAME、配置读写超时、控制单次 pipeline 大小、失败重试带退避。
小结:先分清“命令慢”与“被阻塞”两类问题,用 slowlog 抓命令、用 LATENCY 抓毛刺、用 --intrinsic-latency 判断机器是否合格;大 key 用 UNLINK 与分批遍历替代同步大命令,fork 毛刺靠关闭 THP 与调整持久化策略缓解,慢客户端用 CLIENT LIST 的 omem 与 qbuf 定位并限制。