Redis 阻塞与延迟定位

Redis 为什么"卡住":阻塞源与对应排查命令

Redis 执行命令始终是单线程的(Redis 6 起只是把网络读写改成多线程),这意味着一条耗时 500ms 的命令会让后面所有请求跟着排队 500ms。排查延迟问题要回答的第一个问题是:慢是命令本身慢,还是 Redis 被别的东西挡住了。

两类“慢”必须先分清

类型特征典型来源判断方法
命令本身慢只有特定接口或特定命令慢KEYS、大对象 HGETALL、长 Lua 脚本、大 ZSET 的 ZRANGEslowlog 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

常见事件名:commandforkexpire-cycleeviction-cycleeviction-delrdb-unlink-fsyncaof-fsync-alwaysaof-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
swaprss 小于 used_memory 时的典型信号保证物理内存充足,vm.swappiness 调到 0 或 1
透明大页 THPfork 时页表拷贝放大数百倍内核参数设为 never
慢客户端与大 value输出缓冲膨胀,内存暴涨见下一节的 omem 判读与限制
长 Lua 脚本超过 lua-time-limit(默认 100ms)后其他客户端收到 BUSY拆短脚本,避免循环内大量命令
大批量 pipelineqbuf 暴涨,一次性涌入几十万命令限制单批数量,客户端侧分批提交

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 且长期不变,说明有事务忘记 EXECDISCARD
cmd最近一次执行的命令结合 omem 判断是哪类命令在撑大缓冲

生产建议:client-output-buffer-limit normal 0 0 0(普通客户端不限制但靠客户端自身规范)、replica 256mb 64mb 60pubsub 32mb 8mb 60,后两者的硬限制一旦触发会直接断开连接。

处置动作

  1. 定位到具体命令:换成异步删除(UNLINK)、分批遍历(SCAN/HSCAN)、按需取值(HMGET),必要时把大 key 拆成多个小 key。
  2. fork 毛刺:关闭 THP,调整 save 策略(例如把 RDB 交给从库做,主库只做 AOF),并保证 maxmemory 留出足够余量。
  3. swap 型变慢:这是物理内存问题,先扩容内存并调整 swappiness,参数调优无效。
  4. 慢客户端:redis-cli client kill type normal addr 10.0.0.21:53422 精确断开,同时收紧 client-output-buffer-limit
  5. 大批量写入:客户端分批 + 每批之间短暂间隔,避免单个 pipeline 阻塞整个实例。

预防与巡检

  • rename-command 在生产禁用 KEYSFLUSHALLMONITORrename-command KEYS ""
  • slowlog 阈值设为 10ms 并采集条数与内容,新增即告警;latency latest 纳入监控。
  • 内核侧每次上线前确认:THP 为 nevervm.swappiness 为 0 或 1、磁盘是 SSD 且 IO 不与其他服务争抢。
  • 客户端强制要求:设置 CLIENT SETNAME、配置读写超时、控制单次 pipeline 大小、失败重试带退避。

小结:先分清“命令慢”与“被阻塞”两类问题,用 slowlog 抓命令、用 LATENCY 抓毛刺、用 --intrinsic-latency 判断机器是否合格;大 key 用 UNLINK 与分批遍历替代同步大命令,fork 毛刺靠关闭 THP 与调整持久化策略缓解,慢客户端用 CLIENT LIST 的 omem 与 qbuf 定位并限制。

笔记加载中…