Docker Swarm 脑裂双活灾难:用 Keepalived + 状态自愈脚本实现分区节点秒级自动切断
在生产环境中,最让人头疼的不是整个集群彻底宕机,而是节点处于**“半死不活”**的状态。
在基于 Docker Swarm 搭建的高可用集群中,我们通常会在多个 Manager 节点上部署 Keepalived,通过虚拟 IP(VIP)对外部提供统一的入口。当发生物理网络分区(Network Partition)时,Raft 共识算法能保证占多数(Quorum)的 Manager 节点继续正常工作,而处于少数派分区的 Manager 节点会自动降级、失去写入权限。
然而,痛点就在这里:
虽然少数派 Manager 的 Swarm 状态已经不可用,但它的物理网络可能依然通畅(比如依然能 Ping 通网关,只是与另外几个 Manager 节点失联)。此时,运行在该节点上的 Keepalived 并不知道 Swarm 内部已经发生分裂,依然强行霸占着 VIP。
结果就是:外部流量通过 VIP 源源不断地打到这个已经失去集群控制权的孤立节点上,导致部分或全部业务请求直接被黑洞吞噬。
要解决这个问题,我们需要让 Keepalived 能够“感知” Swarm 的内部共识状态。本文将分享一套在线上生产环境平稳运行的自动切断与自愈方案。
一、 为什么传统的进程检测(pgrep/killall)行不通?
很多技术文档在配置 Keepalived 的 vrrp_script 时,往往只写了简单的进程存活检测:
# 这种检测在网络分区时完全失效!
vrrp_script chk_docker {
script "killall -0 dockerd"
interval 2
}
这种检测只能在 dockerd 进程彻底挂掉时触发切换。但在网络分区发生时:
dockerd进程依然活着,甚至容器也在运行。- 少数派节点的 Swarm 状态已经变为
Error: rpc error: code = Unknown desc = The node is not part of a swarm或者是失去了 Leader 投票权的孤立状态。 - 此时该节点上的容器无法进行正确的服务发现与路由,网络互通性破坏。
因此,我们的健康检查脚本必须深入到 Swarm 引擎的控制平面状态。
二、 核心自愈机制设计
我们要让 Keepalived 实时感知以下两个核心指标:
- LocalNodeState(本地节点状态): 必须是
active。 - ControlAvailable(控制面可用性): 必须是
true。这意味着当前节点不仅是 Manager,而且还处于 Raft 共识网络中,有资格参与决策。如果该值变为false,说明该节点已经掉线,失去了集群管理能力。
预防“死锁”:防范 Docker Daemon 自身卡死
在极端网络分区或 I/O 跑满的情况下,执行 docker info 命令行可能会直接无限期挂起(Hangs)。如果我们的脚本傻傻地等待输出,会导致 Keepalived 的检查超时,无法及时做出降级响应。
因此,脚本中所有的 Docker CLI 命令必须全部加上强制超时控制(timeout)。
三、 健壮的 Swarm 状态检测脚本设计
在所有运行 Keepalived 的 Manager 节点上创建该检测脚本。
创建脚本文件 /usr/local/bin/keepalived_check_swarm.sh:
#!/bin/bash
# ==============================================================================
# 脚本名称: keepalived_check_swarm.sh
# 适用场景: 防止 Docker Swarm 网络分区导致的脑裂与流量黑洞
# ==============================================================================
# 配置项:单次 Docker 命令执行的最大超时时间(秒)
CLI_TIMEOUT=3
# 1. 基础校验:Docker 守护进程是否存活
if ! timeout ${CLI_TIMEOUT} docker info >/dev/null 2>&1; then
# 说明 Docker 守护进程无响应或已挂起
echo "[ERROR] Docker daemon response timeout or dead." >&2
exit 1
fi
# 2. 获取 Swarm 本地节点状态 (active / pending / error)
SWARM_STATE=$(timeout ${CLI_TIMEOUT} docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null)
if [ "${SWARM_STATE}" != "active" ]; then
echo "[ERROR] Swarm LocalNodeState is ${SWARM_STATE}, not active." >&2
exit 1
fi
# 3. 获取控制面可用性 (ControlAvailable)
# 只有当该 Manager 仍处于 Raft 共识圈内(未被分区隔离)时,此值才为 true
CONTROL_AVAILABLE=$(timeout ${CLI_TIMEOUT} docker info --format '{{.Swarm.ControlAvailable}}' 2>/dev/null)
if [ "${CONTROL_AVAILABLE}" != "true" ]; then
echo "[ERROR] Lost Swarm Control plane quorum (ControlAvailable is false)." >&2
exit 1
fi
# 4. 深度校验:尝试进行一次极轻量的集群数据读取
# 防止 API 能响应但内部状态机已死锁的情况
if ! timeout ${CLI_TIMEOUT} docker node ls --filter "id=$(docker info --format '{{.Swarm.NodeID}}')" >/dev/null 2>&1; then
echo "[ERROR] Swarm API is unresponsive to cluster queries." >&2
exit 1
fi
# 全部校验通过,返回 0 代表健康
exit 0
赋予脚本执行权限:
chmod +x /usr/local/bin/keepalived_check_swarm.sh
四、 Keepalived 联动配置
在各 Manager 节点配置 Keepalived。这里以 vrrp_script 结合权重衰减机制来实现 VIP 的平滑漂移。
修改 /etc/keepalived/keepalived.conf:
global_defs {
router_id swarm_node_01 # 各节点唯一
enable_script_security # 启用安全模式,防止非 root 脚本执行越权
script_user root # 执行脚本的用户
}
# 定义 Swarm 健康检查脚本
vrrp_script chk_swarm_health {
script "/usr/local/bin/keepalived_check_swarm.sh"
interval 3 # 每 3 秒执行一次
timeout 4 # 脚本超时时间略大于脚本内部的命令超时
weight -30 # 如果脚本返回非 0(失败),该节点的优先级(priority)直接减少 30
fall 2 # 连续失败 2 次才确认失败(防止偶发性抖动导致 VIP 误漂移)
rise 2 # 连续成功 2 次才恢复
}
vrrp_instance VI_1 {
state BACKUP # 生产环境建议全部设为 BACKUP,由 priority 决定谁是 MASTER
interface eth0 # 绑定你的物理网卡
virtual_router_id 51 # 局域网内唯一
priority 100 # 初始优先级(主节点设 100,备节点设 90、80 等)
# 禁用抢占(避免网络瞬时抖动导致 VIP 频繁来回漂移)
nopreempt
advert_int 1
authentication {
auth_type PASS
auth_pass swarm_secure_pass
}
virtual_ipaddress {
192.168.10.100/24 dev eth0 label eth0:vip # 你的虚拟 VIP
}
# 追踪健康检查脚本
track_script {
chk_swarm_health
}
}
权重衰减逻辑说明:
- 假设节点 A(Master)的初始
priority为100,节点 B(Backup)的初始priority为90。 - 正常状态下,由于
100 > 90,VIP 留在节点 A 上。 - 当网络分区发生,节点 A 与集群失联:
keepalived_check_swarm.sh在节点 A 上执行失败。- 触发
weight -30惩罚,节点 A 的有效优先级实时降低为100 - 30 = 70。 - 此时节点 B 的优先级
90已经大于节点 A 的70。 - 节点 B 立即接管 VIP,对外提供服务,受影响时间仅为
(interval * fall) = 6秒以内。
五、 防御性围栏(Fencing):彻底切断隔离节点的孤立流量
在某些极端复杂的混合云环境中,即便 VIP 漂移走了,本地内网的某些交换机可能还残留着 ARP 缓存,导致部分局域网内部流量依然固执地往被隔离的节点上送。
为了做到绝对安全,我们可以利用 Keepalived 的状态转换钩子(Notify Scripts)。一旦检测到节点进入 FAULT 状态或降级为 BACKUP,我们可以通过脚本自动拉下该节点上的某些边界组件(如反向代理、Ingress Controller,或者直接封锁特定端口的 iptables)。
在 Keepalived 的 vrrp_instance 配置段最后增加以下钩子:
# 当节点状态转换为 BACKUP 或 FAULT 时执行的脚本
notify_backup "/usr/local/bin/keepalived_fencing.sh BACKUP"
notify_fault "/usr/local/bin/keepalived_fencing.sh FAULT"
notify_master "/usr/local/bin/keepalived_fencing.sh MASTER"
编写防灾切断脚本 /usr/local/bin/keepalived_fencing.sh:
#!/bin/bash
ACTION=$1
case "$ACTION" in
"BACKUP"|"FAULT")
echo "[FENCING] Node transitions to ${ACTION}. Taking protective measures..." >> /var/log/keepalived_fence.log
# 示例:如果是依靠外部 Nginx/HAProxy 负载,这里可以直接停止本地的 Ingress 容器,彻底断流
# docker service scale global_ingress_nginx=0 (谨慎操作,因被隔离节点可能无法执行集群级命令)
# 替代方案:通过 iptables 屏蔽外部进入业务端口的流量(比如 80/443),迫使上游完全断开物理连接
iptables -A INPUT -p tcp --dport 80 -j DROP
iptables -A INPUT -p tcp --dport 443 -j DROP
;;
"MASTER")
echo "[FENCING] Node transitions to MASTER. Restoring traffic paths..." >> /var/log/keepalived_fence.log
# 清除屏蔽规则,恢复流量接收
iptables -D INPUT -p tcp --dport 80 -j DROP >/dev/null 2>&1
iptables -D INPUT -p tcp --dport 443 -j DROP >/dev/null 2>&1
;;
esac
这种“自我了断”的机制也叫 STONITH(Shoot The Other Node In The Head),虽然这里属于温和的局部网络阻断,但对保证业务零脏数据、零无响应连接至关重要。
六、 总结与日常演练
这套方案通过 Keepalived 的健康检查脚本,打通了**“底层高可用网络”与“上层容器集群共识平面”**之间的信息孤岛。
日常演练建议:
在非业务高峰期,可以通过在其中一个 Manager 节点上手动执行命令来模拟脑裂:
# 模拟网络分区:直接屏蔽 Raft 协议端口 (2377 是 Swarm 的控制面通信端口)
iptables -A INPUT -p tcp --dport 2377 -j DROP
iptables -A INPUT -p udp --dport 2377 -j DROP
观察该节点的日志,通常在 5~10 秒内,你会看到:
keepalived_check_swarm.sh报错。- Keepalived 日志显示
Entering BACKUP state并释放 VIP。 - 另一个正常的 Manager 节点顺利接管 VIP,整个切换过程对客户端无感知。
通过这套机制,我们可以将 Docker Swarm 网络分区的自愈时间缩短到十秒以内,极大地提升了容器集群的弹性与稳定性。