WEBKT

Docker Swarm 脑裂双活灾难:用 Keepalived + 状态自愈脚本实现分区节点秒级自动切断

3 0 0 0

在生产环境中,最让人头疼的不是整个集群彻底宕机,而是节点处于**“半死不活”**的状态。

在基于 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 进程彻底挂掉时触发切换。但在网络分区发生时:

  1. dockerd 进程依然活着,甚至容器也在运行。
  2. 少数派节点的 Swarm 状态已经变为 Error: rpc error: code = Unknown desc = The node is not part of a swarm 或者是失去了 Leader 投票权的孤立状态。
  3. 此时该节点上的容器无法进行正确的服务发现与路由,网络互通性破坏。

因此,我们的健康检查脚本必须深入到 Swarm 引擎的控制平面状态


二、 核心自愈机制设计

我们要让 Keepalived 实时感知以下两个核心指标:

  1. LocalNodeState(本地节点状态): 必须是 active
  2. 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)的初始 priority100,节点 B(Backup)的初始 priority90
  • 正常状态下,由于 100 > 90,VIP 留在节点 A 上。
  • 当网络分区发生,节点 A 与集群失联:
    1. keepalived_check_swarm.sh 在节点 A 上执行失败。
    2. 触发 weight -30 惩罚,节点 A 的有效优先级实时降低为 100 - 30 = 70
    3. 此时节点 B 的优先级 90 已经大于节点 A 的 70
    4. 节点 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 秒内,你会看到:

  1. keepalived_check_swarm.sh 报错。
  2. Keepalived 日志显示 Entering BACKUP state 并释放 VIP。
  3. 另一个正常的 Manager 节点顺利接管 VIP,整个切换过程对客户端无感知。

通过这套机制,我们可以将 Docker Swarm 网络分区的自愈时间缩短到十秒以内,极大地提升了容器集群的弹性与稳定性。

SRE运维极客 Keepalived脑裂保护

评论点评