根本原因是bgsave触发的fork阻塞、cow内存争抢和磁盘io压力三重叠加,导致主线程调度延迟及内核网络栈响应变慢,进而使subscribe消息推送卡顿。

Redis 在执行 bgsave 时影响发布订阅(SUBSCRIBE)的实时延迟,根本原因不是订阅逻辑本身变慢,而是 fork() 阻塞、COW 内存争抢和磁盘 I/O 压力三者叠加,导致主线程调度延迟和内核网络栈响应变慢。
为什么 fork() 会卡住 SUBSCRIBE 消息推送
fork() 是系统调用,不是 Redis 自己实现的。当主进程 RSS 内存大(比如 >8GB)、内存碎片率高或启用了 THP(透明大页)时,fork() 可能阻塞主线程几十甚至上百毫秒。这段时间里,Redis 主线程无法处理任何事件,包括:
-
epoll_wait()返回后对新到达的 PUBLISH 消息做分发 - 向已订阅客户端的 socket 缓冲区写入消息
- 甚至 TCP ACK 包的及时发出(受内核调度影响)
此时客户端 recv() 会等不到数据,表现为“消息延迟几秒才收到”,但 redis-cli pubsub numsub 显示订阅正常——说明连接没断,只是推送卡住了。
为什么 COW 会让延迟毛刺更严重
子进程调用 fork() 后,所有内存页被标记为 Copy-on-Write。一旦主线程在 bgsave 过程中修改任意 key(比如高频 PUBLISH),内核就要为该页分配新物理内存并复制内容,触发 minor page fault:
- CPU 频繁被中断处理缺页异常,吞吐下降
- TLB(页表缓存)频繁刷新,访存效率降低
- 内存带宽被父子进程争抢,
used_memory_rss_human和used_memory_human差值越大(>2GB),问题越明显
这种毛刺不是持续延迟,而是偶发的几十毫秒到几百毫秒抖动,刚好落在消息链路的关键路径上。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
如何确认是 bgsave 导致的 SUBSCRIBE 卡顿
别只查 INFO replication 或 redis-cli pubsub,重点看三层指标:
- 执行
INFO stats,观察latest_fork_usec:若周期性出现 >500000(即 500ms),基本锁定 - 运行
iostat -x 1,关注对应磁盘的await(平均 I/O 等待时间)是否突增至 >10ms,avgqu-sz是否持续 >1 - 在卡顿时执行
cat /proc/$(pgrep redis-server)/stack,若看到大量wait_on_page_bit或ext4_writepages,说明卡在文件系统层
注意:redis-cli --latency 测出的延迟高峰,如果和 rdb_bgsave_in_progress 时间段严格对齐,就不是巧合。
真正有效的缓解方式不是关掉 bgsave
关掉 bgsave 能止血,但放弃 RDB 就等于放弃最可靠的故障恢复手段。生产环境应解耦职责:
- 主节点配置
save ""清空自动触发规则,专注读写与复制 - 选一台专用从节点开启
save 300 1并运行bgsave,确保它不承担线上读流量 - 禁用 THP:
echo never > /sys/kernel/mm/transparent_hugepage/enabled - 设
vm.swappiness = 0,避免 fork 过程中因内存压力触发 swap
最容易被忽略的一点:SUBSCRIBE 不是“无状态推送”,它依赖 TCP 接收窗口和内核 net.ipv4.tcp_rmem 缓冲区。当 bgsave 拉高系统负载时,即使主线程没卡死,内核也可能来不及把数据从 socket 缓冲区拷贝到用户态,客户端就只能干等。










