说实话,Redis 订阅者响应变慢,通常不是订阅逻辑本身的问题,而是 bgsave 触发时系统级资源争抢造成的——尤其是 fork 加 COW 再加磁盘 I/O 三重叠加引发的延迟毛刺。

先看一个核心现象:当 bgsave 运行时,订阅者客户端会感受到明显的卡顿。但这里的问题核心不在于发布/订阅本身,而在于持久化操作如何干扰了主线程调度和底层资源分配。
为什么 bgsave 会让 SUBSCRIBE 客户端卡顿
Redis 的发布/订阅虽然是纯内存操作,不走命令队列主流程,但它的响应依然依赖主线程调度和底层系统资源。当 bgsave 执行时,问题就来了:
bgsave会调用fork()创建子进程。在内存压力大或开启 THP(透明大页)的情况下,这个系统调用可能阻塞主线程数十毫秒——期间所有客户端(包括订阅者)都收不到新消息推送。- 子进程遍历内存做快照时,如果主线程正在高频修改 key(比如频繁执行
PUBLISH),COW 机制会大量复制脏页,进一步加剧内存带宽争抢和 TLB 压力。 - RDB 文件写入磁盘的过程会拉高 I/O 队列深度。用
iostat -x 1观察的话,能看到avgqu-sz突增,这会间接拖慢内核网络栈的处理速度,表现为订阅者客户端的READ系统调用返回延迟升高。
简单说,这不是订阅逻辑的问题,而是系统资源被持久化操作借走了。
bgsave 期间磁盘 I/O 高的典型表现与确认方式
别光盯着 redis-cli info | grep rdb 看,还得看系统层是不是真被卡住了:
- 执行
iostat -x 1,观察对应磁盘的%util是否持续超过 80%,并且await(平均 I/O 等待时间)超过 10ms。 - 检查
/proc/中的/io write_bytes,看它在bgsave启动后是否突然飙升。注意,数据是子进程写的,但父进程的io文件也会反映部分上下文切换开销。 - 用
perf record -e syscalls:sys_enter_write -p抓取写系统调用的耗时,常能发现某些write耗时超过 50ms——这正是订阅者消息推送卡顿的直接源头。
如何让 bgsave 对 SUBSCRIBE 影响最小化
核心思路是解耦持久化与主服务,而不是简单地关掉 bgsave:
- 把 RDB 持久化迁移到从节点。主节点用
save ""清空自动触发规则,从节点配置save 300 1等策略并开启bgsave。 - 禁用 THP:
echo never > /sys/kernel/mm/transparent_hugepage/enabled,否则fork()延迟可能从毫秒级跳到百毫秒级。 - 确保
vm.swappiness = 0,避免fork()过程中因内存不足触发 swap,进一步放大延迟。 - 如果必须在主节点运行
bgsave,改用redis-cli bgsave手动触发,并避开消息高峰时段,比如避开每小时整点的批量PUBLISH。
这里得提醒一句:清空自动触发、挪到从节点开、再配上系统参数调整,这套组合拳之后,主节点基本就能保持稳定了。
容易被忽略的细节:SUBSCRIBE 不是“无状态”,它依赖 TCP 接收窗口和内核缓冲区
当 bgsave 拉高系统负载时,即便 Redis 主线程没卡死,也可能会出现:
- TCP 接收缓冲区(
net.ipv4.tcp_rmem)来不及消费,导致客户端的recv()阻塞。 - epoll_wait() 返回频率下降,订阅者客户端感知到的是“消息断流”而非“延迟”,但实际上内核事件分发已经变慢了。
- 在云环境的 cgroup 内存限制下,
fork()失败日志可能被吞掉,只留下rdb_last_bgsave_status:err。这时候需要查dmesg -T | grep -i "out of memory"才能定位到真实原因。
所以说,解决 Redis 订阅者卡顿问题,关键不在于调整订阅逻辑本身,而是要在系统层面做好资源隔离和优化。
