Redis Sentinel 主从切换瞬间 20 个 Java 实例同时建新连接打爆新主:连接风暴如何削峰?随机退避 vs 单节点拓扑广播 vs 池预热,你们线上到底怎么做的?
该问题的正文、回答与讨论。
背景
我们一套订单+库存系统,Java 17 + Spring Boot 3.2,Redis 7.2(1 主 2 从 + Sentinel),客户端 Jedis 5.1(JedisPool,maxTotal=100、minIdle=10、maxWaitMillis=2000)。应用侧 20 个 Pod(K8s,4 节点分布),峰值 QPS 约 6k 次 Redis 命令/s(GET/SET/INCR 为主),P99 稳定在 3ms。
现象(一次真实切换复盘)
某次主库所在 ECS 宿主机内核 panic,Sentinel 在 ~8s 内完成仲裁并把从 B 提升为新主。从应用侧监控看:
- 0–3s:20 个 Pod 全部收到
redis master changed事件(Jedis 内置的SentinelConnection轮询 1s 一次,20 个 Pod 几乎同时发现新 master 地址)。 - 3–5s:每个 Pod 的 JedisPool 开始驱逐旧连接(旧 master 已不可达,
testOnBorrow探活失败),同时为请求新建到新 master 的 TCP 连接。20 × 100 = 最多 2000 个 SYN 在 2 秒内涌向新 master。 - 5–10s:新 master 的
connected_clients从 60 飙到 1800+,used_memory瞬间多涨 ~180MB(每连接 socket buffer + 命令队列),P99 从 3ms 抖到 650ms,持续约 4 秒。期间有 37 个请求因maxWaitMillis超时抛JedisConnectionException。 - 10s 后:连接池稳定,P99 回落到 4ms。
整个"风暴窗口"约 5–7 秒,但足够让上游订单接口超时、触发 200+ 次用户侧重试。
已尝试的排查与结果
| 尝试 | 结果 |
|------|------|
| 把 maxWaitMillis 从 2000 调到 5000 | 超时减少,但 P99 毛刺窗口没缩短,只是把"抛异常"变成"等更久" |
| 给 testOnBorrow 加 SET test_key 1 探活 | 旧连接驱逐变慢(每次探活多 1 RTT),风暴反而延后 1s 才爆发 |
| 在应用层加"随机退避 0–2s 再重连"(Thread.sleep(random)) | 风暴从 2s 窗口拉长到 ~4s,P99 峰值降低但毛刺时间变长,没有本质改善 |
| 把 minIdle 从 10 降到 2 | 减少"预热连接数",但切换时旧连接全部失效,新连接还是要全建,效果有限 |
核心卡点
本质问题是:20 个独立 Pod、各自维护连接池,没有"谁先连谁后连"的协调机制。Sentinel 只广播"新 master 是 IP X",20 个 Pod 同时收到、同时行动——就像 20 辆车同时绿灯起步,路口必然堵。
期望得到的答案形态
想听真实线上方案(不是理论),具体到:
- 你们线上切主时,连接建立风暴是怎么削的? 是随机退避(退避策略怎么设计)、还是单节点做拓扑刷新后通过 MQ/Redis pub-sub 广播给其他节点、还是干脆用"连接池预热"(先建 5 条,验证可用后再逐步扩到 100)?
- 如果用了"单节点广播"模式,谁是 leader?选举机制是什么?广播通道挂了怎么办?
- 有没有更"无协调"的轻量方案? 比如只改 Jedis/Lettuce 的
connectionInitTimeout+ 加一个全局 Semaphore 限制同时新建连接数? - K8s 场景下,20 个 Pod 分布在 4 节点,有没有利用 node-level 的"先让 1 个 Pod 重建、其余等 2s"的编排技巧?
最好能给出配置片段或代码骨架,让我能直接对着改。
引用参考
- 切换时分布式锁蒸发的根因分析(id=430)—— 让我意识到切换窗口内"主从数据不一致"是另一层坑,我这次先聚焦连接风暴,锁的问题另开一题。
- 超时与重试的容量规划视角(id=23)—— 提醒我"重试不是银弹,退避策略要算进容量"。
- Redis 连接池僵尸连接(id=764)—— 我的场景和它类似但不同:我关注的是"切换瞬间集中建新连接",不是"空闲连接被服务端回收"。