标签: Gossip协议

  • Redis 生产环境 P99 飙升:RDB COW 触发内存淘汰与 Cluster Gossip 故障转移雪崩排查实战

    生产环境 Redis Cluster (6.2.7) 突发 P99 延迟飙升至 2000ms,并伴随频繁的主从切换。核心原因是 BGSAVE 触发 Copy-On-Write 导致内存触碰 maxmemory,引发主线程大规模 LRU 淘汰阻塞。主线程卡顿导致 Gossip 协议心跳超时,误判节点下线并触发级联故障转移。解决方式:预留 30% 内存给 COW,开启 lazyfree-lazy-eviction,并调大 cluster-node-timeout

    故障现场与指标断崖式下跌

    近期某集群告警,监控面板上呈现出典型的“雪崩”特征:

    1. QPS 骤降:单节点 QPS 从 80k 瞬间跌至 5k 以下。

    2. P99 剧烈抖动:平时稳定在 2ms 以内的 P99 突增至 2000ms+。

    3. 连接风暴:客户端因超时大量重连,引发短连接风暴。

    立刻登机拉取 Redis 日志,发现大量内存淘汰警告,紧接着是集群节点的 FAIL 状态广播:

    29302:M 10:23:14.123 * 10000 keys evicted, 153MB freed.
    29302:M 10:23:14.891 * 10000 keys evicted, 142MB freed.
    ...
    29302:M 10:23:19.456 * Marking node 3a8b... as failing (quorum reached).
    29302:M 10:23:19.458 # Cluster state changed: fail
    

    查看当时的内存指标和核心状态:

    $ redis-cli -p 6379 info memory | grep -E "used_memory_human|maxmemory_human|latest_fork_usec"
    used_memory_human:24.1G
    maxmemory_human:24.0G
    latest_fork_usec:89450
    

    现象很明确:由于触发了内存淘汰,主线程被长时间占用,导致集群内的 Gossip 节点心跳无响应,最终引发整个集群拓扑结构的重新计算和主从切换。

    为什么一次 BGSAVE 会引发集群雪崩?

    很多人在配置 Redis 时,习惯把 maxmemory 设为物理内存的 90% 甚至更大,认为这样“不浪费”。这在没有高频写入和持久化的场景下勉强能跑,但一旦触发 BGSAVE (RDB 持久化),就是灾难的开始。

    Redis 执行 BGSAVE 时会 fork() 一个子进程。现代操作系统利用 Copy-On-Write (COW) 机制,父子进程初始共享物理内存页。然而,如果此时集群正处于高频写入状态(特别是大 Key 的更新),父进程在修改数据时,操作系统必须为这些被修改的内存页分配新的物理空间。

    排查过程中的现场数据显示,该节点在 BGSAVE 期间产生了高达 4GB 的 COW 内存:

    # cat /proc/$(pidof redis-server)/smaps | grep -i private_dirty | awk '{sum+=$2} END {print sum/1024 " MB"}'
    3952 MB
    

    雪崩的传导链条如下:

    1. 内存触顶:COW 导致 Redis 实际占用内存 + 自身分配的内存超过了 maxmemory(24GB)。

    2. 同步淘汰阻塞:Redis 触发 maxmemory-policy(当时配的是 allkeys-lru)。在 Redis 6.2 中,如果未开启异步淘汰,主线程必须同步寻找并释放内存。大规模的 Key 淘汰(且包含 Hash/Set 大 Key)死死卡住了主线程。

    3. Gossip 协议“假死”:Redis 集群的节点保活依赖 Gossip 协议,而处理 Gossip 消息的 clusterCron() 是在主线程的事件循环中执行的。主线程被 Eviction 阻塞了 5 秒,导致无法回复其他节点的 PING

    4. 脑裂与故障转移:其他节点超过 cluster-node-timeout(当时配的是激进的 5000ms)未收到 PONG,将其标记为 PFAIL,进而升级为 FAIL,触发 Replica 强制上位。

    5. 全量同步加剧雪崩:旧 Master 恢复后变为 Slave,向新 Master 发起 SYNC,再次触发新 Master 的 BGSAVE。死循环形成。

    防御性配置与底层调优实战

    为了彻底根除这种由于持久化抖动引发的集群雪崩,必须从内存预留、异步淘汰和集群容忍度三个维度进行改造。

    1. 严格的内存水位控制 (COW 预留)

    永远不要把 maxmemory 贴着物理内存上限配置。标准做法是预留 30% – 40% 的内存给 COW、主从复制的 repl-backlog 以及客户端缓冲区。

    # 假设实例物理内存 32GB
    maxmemory 20gb
    # 淘汰策略根据业务改为 volatile-lru 或 volatile-ttl,避免全盘扫描
    maxmemory-policy volatile-lru
    

    2. 开启 Lazyfree 机制

    Redis 4.0 引入了 lazyfree,6.0+ 版本进一步完善。针对内存淘汰引发的阻塞,必须开启惰性删除,将释放内存的动作交给后台线程 (bio 线程池) 执行,保命主线程。

    # 开启惰性内存淘汰
    lazyfree-lazy-eviction yes
    # 开启惰性键过期
    lazyfree-lazy-expire yes
    # 隐式 DEL 转化为 UNLINK
    lazyfree-lazy-user-del yes
    

    3. 调校 Cluster Gossip 参数

    cluster-node-timeout 决定了集群对网络抖动和主线程阻塞的容忍度。千万别为了追求极端的“故障恢复速度”将其设为 3-5 秒。主线程偶然卡顿是常态,误判导致的 Failover 成本极高。

    # 推荐值为 15000 (15秒),足够覆盖绝大多数 RDB fork 和淘汰耗时
    cluster-node-timeout 15000
    

    配合调大复制积压缓冲区,防止主从切换或短连后触发全量重传:

    repl-backlog-size 512mb
    

    4. 彻底接管内核 THP (Transparent Huge Pages)

    在排查中发现,操作系统的 THP 是开启的。THP 会将默认的 4KB 内存页放大为 2MB。在 COW 发生时,即使 Redis 只修改了 10 字节的数据,内核也必须拷贝完整的 2MB 内存页。这直接导致了 BGSAVE 期间内存飙升速度放大了数百倍。

    必须在所有 Redis 宿主机上硬性关闭 THP:

    echo never > /sys/kernel/mm/transparent_hugepage/enabled
    echo never > /sys/kernel/mm/transparent_hugepage/defrag
    # 固化到 rc.local 或 grub 中
    

    常见问题

    Q1:除了 BGSAVE,AOF 重写 (BGREWRITEAOF) 会引发完全一样的问题吗? 会。BGREWRITEAOF 同样依赖 fork() 子进程进行机制。只要有 Fork 操作,在海量写入时就会产生大量 COW 内存。防御策略完全一致。

    Q2:如何快速确认集群是否正因为主线程卡顿而处于 Gossip 瘫痪边缘? 观察 info stats 中的 latest_fork_usec 耗时,以及慢查询日志。如果 latest_fork_usec 超过 100ms,说明 fork 本身就极其耗时(通常由于系统页表太大引起)。同时可以监控 redis_cluster_messages_ping_sentpong_received 的差值斜率。

    Q3:开启了 lazyfree-lazy-eviction,内存就一定不会爆吗? 并非绝对。Lazyfree 只是把内存释放动作交给了后台线程。如果业务的写入速度远大于后台线程释放内存的速度,Redis 的总内存依然会持续上涨,最终触发操作系统的 OOM Killer 直接干掉 Redis 进程。因此,合理的 maxmemory 预留和限流依然是底线。

    Q4:Redis 7.0 的 Multi-part AOF 能解决这个问题吗? Redis 7.0 的 Multi-part AOF 优化了 AOF 重写期间的增量数据追加机制,大幅降低了重写带来的内存开销和 CPU 负担。但对于纯 RDB 的 BGSAVE COW 物理内存翻倍问题,底层机制并没有变,依然受限于内核的页表和 COW 行为。