我不会起名字322· 后端 / 算法 / 数据库 技术栈 力扣 Hot100 Go 项目 Redis MySQL文章目录Redis 为什么会卡住fork、大 key、慢命令、AOF fsync 4 个阻塞源逐个排除一、先建观测能力3 条命令二、4 个阻塞源逐个排除2.1 fork 子进程耗时和数据量成正比2.2 大 key两个独立的伤害2.3 慢命令几条必须拉黑的 O(N) 操作2.4 AOF fsync取决于磁盘不取决于 Redis三、还有 3 类影响较小但别忽略的四、排查顺序先看哪 3 个指标五、结论分三档写六、小结Redis 为什么会卡住fork、大 key、慢命令、AOF fsync 4 个阻塞源逐个排除“Redis 是单线程的所以它很快”——这句话只说了前半段。后半段是正因为是单线程任何一条耗时 50ms 的命令都会把后面所有请求一起堵 50ms。Redis 不会变慢它只会在某几个瞬间完全停住然后在监控图上留下一根根尖刺。这篇文章不谈理论只解决一个问题当 P99 突然出现几十毫秒的尖刺怎么在 5 分钟内定位到是哪个阻塞源。先把观测能力建起来再逐个排除 4 类高频原因最后给一份排查顺序表。一、先建观测能力3 条命令排查阻塞靠猜是没用的。落这三条命令就够了。第一条慢查询日志。默认阈值是 10000 微秒10ms线上建议直接调到 5ms因为尖刺往往就是从 5~10ms 这段堆出来的# 查看当前配置127.0.0.1:6379CONFIG GET slowlog-*1)slowlog-log-slower-than# 阈值单位微秒-1 表示关闭2)100003)slowlog-max-len# 最多保留多少条默认 1284)128# 运行时调整不用重启127.0.0.1:6379CONFIG SET slowlog-log-slower-than5000127.0.0.1:6379CONFIG SET slowlog-max-len1024# 看最近 10 条慢日志127.0.0.1:6379SLOWLOG GET101)1)(integer)14# 日志 id2)(integer)1759999999# 发生时间戳3)(integer)48231# 耗时微秒 → 48.2ms4)1)HGETALL# 命令2)user:profile:8812# key5)10.0.3.51:412206)app-cache-3关键看第 4 项如果慢日志里反复出现同一个 key 前缀那就是大 key如果是一条KEYS/FLUSHALL那是慢命令。这一步就能区分掉两大类。第二条延迟监控。慢查询只能记录命令执行这一段fork、AOF fsync 这类不在命令执行路径上的耗时它抓不到——那要用LATENCY127.0.0.1:6379CONFIG SET latency-monitor-threshold50# 超过 50ms 才记录127.0.0.1:6379LATENCY LATEST1)1)fork2)(integer)17599998703)(integer)213# 最近一次 213ms4)(integer)431# 历史最大 431ms2)1)aof-fsync-always# 或 aof-write 相关事件2)(integer)17599998603)(integer)874)(integer)156LATENCY LATEST列出的事件名本身就是一份原因清单fork、aof-fsync-always、command、expire-cycle、eviction-del。第三条INFO的几个关键字段。一次抓齐省得来回切127.0.0.1:6379INFO stats;INFO persistence;INFO clients# stats 里关注# latest_fork_usec 最近一次 fork 耗时微秒# rejected_connections 因超 maxclients 被拒的连接数# persistence 里关注# aof_delayed_fsync 因硬盘压力被延迟的 fsync 次数 ⚠️ 非 0 就是问题# rdb_last_bgsave_status 上次 RDB 是否成功# clients 里关注# blocked_clients 当前阻塞的客户端数# client_recent_max_input_buffer 最大输入缓冲区二、4 个阻塞源逐个排除阻塞源典型现象观测命令耗时量级主要改法fork 子进程周期性尖刺与 RDB/AOF 重写时间吻合INFO stats看latest_fork_usec20ms/GB量级单实例控制在 10GB 内大 key单个 key 访问偶发变慢集群内存不均redis-cli --bigkeys、MEMORY USAGE随元素数线性增长拆分 UNLINK删除慢命令慢日志里能看到完整命令SLOWLOG GET几十 ms 到秒级禁用 改走 SCANAOF fsync与 AOF 落盘节奏吻合aof_delayed_fsync上涨INFO persistence取决于磁盘换盘 /no-appendfsync-on-rewrite2.1 fork 子进程耗时和数据量成正比RDB 生成和 AOF 重写都会fork一个子进程。fork本身要做两件事复制父进程的页表以及建立写时复制COW的映射。页表大小与实例内存量成正比经验值是20ms/GB——一个 32GB 的实例光 fork 就要 600ms 以上。这期间主线程是被阻塞的复制页表这个动作在父进程里同步完成所以监控上会看到一根明显的尖刺而且周期性与save配置或 AOF 重写触发点严丝合缝。# 确认是不是 fork 造成的127.0.0.1:6379INFO stats|greplatest_fork_usec latest_fork_usec:213478# 213ms一个 10GB 左右的实例偏慢三条改法按优先级控制单实例内存RDB 场景下建议不超过 10GB超过就拆实例或改用 Cluster降低 fork 频率把save从900 秒内 1 个 key 变化改成更宽松的多档组合AOF 重写用auto-aof-rewrite-percentage调高关闭自动重写期间的 fsyncno-appendfsync-on-rewrite yes用重写期间可能丢一点数据换重写期间不抖这是权衡不是免费午餐。2.2 大 key两个独立的伤害大 key 的麻烦是双份的内存不均在 Cluster 里一个 500MB 的 key 会让它所在的节点内存明显高于其他节点槽位迁移也会因此变慢超时阻塞单线程执行HGETALL一个 100 万字段的 hash耗时是几十毫秒量级期间所有请求排队。发现大key最省事的办法是 Redis 自带的扫描工具走SCAN实现不会阻塞redis-cli-h10.0.3.51-p6379--bigkeys# 输出片段# -------- summary -------# Sampled 1284312 keys in the keyspace!# Total key length in bytes is 32014588 (avg len 24.93)## Biggest string found user:session:bulk:20261009 has 5242880 bytes# Biggest hash found cart:items:8812 has 128403 fields# Biggest zset found rank:feed:global has 902144 members逐个 key 精确测量用MEMORY USAGE127.0.0.1:6379MEMORY USAGE cart:items:8812(integer)12583168# 12MB一个 key处理上有三条硬纪律删除必须用UNLINK而不是DEL。DEL是同步释放删一个几百万元素的 zset 会直接卡住主线程UNLINK把释放动作丢给后台线程主线程只摘除引用。不要对大 key 做全量读。HGETALL→HSCANSMEMBERS→SSCANLRANGE 0 -1→ 分页LRANGE 0 99。从设计上拆。按业务维度切分cart:items:8812拆成cart:items:8812:page:{0..N}每一个控制在 1000 个字段以内或者把大 hash 的冷字段下沉到数据库。2.3 慢命令几条必须拉黑的 O(N) 操作这一类最好查——慢日志里会直接写着命令名。要拉黑的是这几条命令为什么危险替代方案KEYS pattern遍历整个 keyspaceO(N)SCAN cursor MATCH pattern COUNT 100FLUSHALL/FLUSHDB一次性清空元素越多越慢FLUSHALL ASYNCHGETALL/SMEMBERS大 key 上等同于全量读HSCAN/SSCANSORT默认 O(N log N)还会占用额外内存排序放业务层ZRANGE key 0 -1大 zset 全量返回网络也是负担分页取或ZRANGE ... LIMITDEL大 key同步释放阻塞主线程UNLINK这些命令不一定一定是坏的但都需要在知道 key 规模的前提下才敢用。生产环境的通用做法是在中间件层直接禁用KEYS和FLUSHALL# 禁用危险命令写进 redis.conf重启生效rename-command KEYSrename-command FLUSHALLrename-command FLUSHDB2.4 AOF fsync取决于磁盘不取决于 RedisAOF 的appendfsync有三档生产上一般选everysec配置行为数据安全性能always每条写命令都 fsync最强最差直接受磁盘 IOPS 限制everysec每秒 fsync 一次默认最多丢 1 秒好但有阻塞分支no交给操作系统最多丢 30 秒最好everysec听起来很安全但它有一个隐藏的阻塞路径后台线程正在 fsync 时如果主线程也要写 AOF就会等这个 fsync 完成。磁盘压力大时比如和 RDB 重写、其他 IO 密集服务抢盘这个等待会累积。判定指标只有一个aof_delayed_fsync127.0.0.1:6379INFO persistence|grep-Eaof_delayed_fsync|aof_last_write_statusaof_delayed_fsync:37# ⚠️ 只要不是 0就说明 fsync 被延迟过aof_last_write_status:okaof_delayed_fsync只要不为 0就说明已经发生过主线程等 fsync的情况。处理顺序先看磁盘是不是到瓶颈了iostat -x 1看%util再考虑开no-appendfsync-on-rewrite yes最后才是重新评估是否必须用always。三、还有 3 类影响较小但别忽略的类别观测点说明swap/proc/pid/smaps里Swap字段一旦发生内存交换性能会断崖式下降。这是唯一必须 0 容忍的指标输入缓冲区CLIENT LIST的qbuf/qbuf-free单客户端输入缓冲区上限 1GB超了会被强断大量命令堆积时会出现输出缓冲区CLIENT LIST的omem大 key 全量返回时omem会飙升挤占内存网络INFO stats的rejected_connections超maxclients后的拒绝ulimit -n默认 1024连接多的实例必须调大输入缓冲区有个容易误解的点它不受maxmemory限制。所以设置了 maxmemory 就安全了是错的——一个客户端把 1GB 的命令堆在输入缓冲区里该 OOM 还是 OOM。四、排查顺序先看哪 3 个指标盯的时间越短越好建议按这个顺序INFO stats的latest_fork_usec—— 一个数字排除掉 fork。超过 200ms 就重点看 RDB/AOF 触发时间是否与尖刺重合。SLOWLOG GET 10—— 一眼看出有没有大 key 或慢命令。有重复 key 前缀 → 大 key有KEYS/HGETALL→ 慢命令。INFO persistence的aof_delayed_fsync—— 非 0 就是 fsync 阻塞接着去查磁盘。三条都干净再往 swapsmaps和客户端缓冲区CLIENT LIST上找。五、结论分三档写现象可复现的数字每天 02:00 出现 200~400ms 尖刺latest_fork_usec峰值 431msaof_delayed_fsync稳定为 0慢日志同期无记录。已排除的解释已排除慢命令——慢日志阈值 5ms在该时段没有新增条目已排除 AOF fsync——aof_delayed_fsync为 0已排除并发压力——同时段 QPS 只有均值的 12%。尚未证实的猜测实例内存 28GB按 20ms/GB 估算 fork 约 560ms与观测值量级吻合但还需要在同一实例上分三天记录latest_fork_usec与内存量的对应关系才能定量确认也还不能排除同机其他进程抢 CPU 的影响。六、小结Redis 性能优化里卡从来不是一个模糊的感觉它一定能对应到四件事之一fork—— 看latest_fork_usec和数据量成正比20ms/GB 是基准线大 key—— 看慢日志里重复的 key 前缀删除一律用UNLINK慢命令—— 看慢日志里的命令名KEYS和FLUSHALL直接禁用AOF fsync—— 看aof_delayed_fsync不为 0 就去查磁盘。再配上latency-monitor把非命令路径的耗时也抓进来你会发现绝大多数尖刺都能在 5 分钟内归因。这套观测思路和单线程模型本身是配套的想弄清为什么单线程还能跑 10 万 QPS可以看《Redis 进阶一单线程的 Redis 为什么这么快》而想处理主从切换时读到旧数据则是《Redis 进阶二主从复制与哨兵》那条线和本文的阻塞排查互不重叠。