WEBKT

Docker Swarm 脑裂灾难恢复:利用 Ansible 与 Restic 快速重建 Raft 集群

26 0 0 0

在生产环境中,Docker Swarm 凭借其轻量化、易维护的特点被广泛部署。然而,由于 Swarm Manager 节点之间强依赖 Raft 共识协议,当遭遇网络分区、磁盘 I/O 严重抖动或节点异常宕机时,Manager 节点数量极易降到法定人数(Quorum)以下,进而引发集群脑裂(Split-Brain)共识丧失。此时,整个 Swarm 管理端将处于不可用状态(运行中的容器通常能继续运行,但无法进行任何调度或更新操作)。

本文将介绍一种高效、自动化的灾难恢复方案:通过 Ansible 编排调度,利用 Restic 安全恢复备份的 Raft 状态数据,并强行重建 Swarm 领导者,最终恢复集群活力。


1. 恢复策略的核心逻辑:避免“二次脑裂”

在动手编写自动化脚本前,必须理清 Swarm Raft 恢复的底层逻辑。

许多运维人员会犯一个致命错误:在所有旧的 Manager 节点上同时恢复 /var/lib/docker/swarm 的备份,然后直接启动 Docker。 这样做会导致每个节点都认为自己拥有最新的状态,并在无法建立共识的情况下各自尝试选举,从而引发二次脑裂。

正确的恢复路径如下:

                       [ 灾难发生:Swarm 共识崩溃 ]
                                   │
                     ┌─────────────┴─────────────┐
                     ▼                           ▼
            【Seed 种子节点】             【其他旧 Manager 节点】
                     │                           │
            1. 停止 Docker 服务           1. 停止 Docker 服务
                     │                           │
            2. 清理旧 Swarm 目录          2. 彻底清理 Swarm 目录
                     │                           │
     3. Restic 恢复健康的 Raft 备份               │
                     │                           │
 4. 强制初始化: --force-new-cluster               │
                     │                           │
            5. 获取新 Join Token                 │
                     │                           │
                     └─────────────┬─────────────┘
                                   ▼
                        【其他旧 Manager 节点】
                     3. 以 Manager 身份加入新集群
  1. 确立种子节点(Seed Node):选择一台性能最好、网络最稳定的原 Manager 节点作为恢复的种子。
  2. 状态清理:在所有 Manager 节点上停止 Docker 进程。
  3. 单点恢复仅在种子节点上通过 Restic 恢复 /var/lib/docker/swarm 目录的最后一次健康备份。
  4. 强制重建(--force-new-cluster):在种子节点上启动 Docker,并执行强制单节点集群初始化。这会强制废除旧的 Raft 成员列表,生成新的 Raft 元数据,并将当前节点设为唯一的 Leader。
  5. 重构群落:获取新的 Manager Join Token,清理其他 Manager 节点的 Swarm 目录,并让它们作为干净的节点重新加入新生成的 Swarm。

2. 环境与准备工作

本方案假设你的环境已满足以下条件:

  • Ansible 控制端:可以无密码 SSH 登录所有 Swarm 节点。
  • Restic 备份仓库:Swarm 的 /var/lib/docker/swarm 目录已经定期通过 Restic 备份至远端存储(如 MinIO、AWS S3 或自建的 SFTP 服务器)。
  • 免密认证环境:Restic 密码和仓库凭证已通过环境变量或 Ansible Vault 保护。

变量定义样例 (group_vars/all.yml)

# Restic 仓库配置
restic_repo: "s3:https://s3.example.com/swarm-backups"
restic_password: "SuperSecretResticPassword"
aws_access_key_id: "my-access-key"
aws_secret_access_key: "my-secret-key"

# Swarm 配置
swarm_seed_host: "manager-01" # 选定的种子节点(必须在 Ansible Inventory 中定义)
swarm_interface: "eth0"       # 用于 Swarm 内部通信的网卡

3. Ansible 灾难恢复 Playbook 编写

我们将恢复流程封装进一个名为 swarm_recovery.yml 的 Playbook 中。它将自动化执行停止服务、单点恢复、强制起群、获取 Token、动态重组的所有步骤。

---
- name: Docker Swarm Raft Cluster Disaster Recovery
  hosts: swarm_managers
  become: yes
  vars_files:
    - group_vars/all.yml

  tasks:
    # -----------------------------------------------------------------
    # 步骤 1:全网停服,规避状态冲突
    # -----------------------------------------------------------------
    - name: Stop Docker daemon on all managers
      systemd:
        name: docker
        state: stopped

    - name: Ensure Docker socket is stopped
      systemd:
        name: docker.socket
        state: stopped

    # -----------------------------------------------------------------
    # 步骤 2:彻底清理非种子节点的 Swarm 残留,防止后续干扰
    # -----------------------------------------------------------------
    - name: Clear Swarm state directory on non-seed managers
      file:
        path: /var/lib/docker/swarm
        state: absent
      when: inventory_hostname != swarm_seed_host

    # -----------------------------------------------------------------
    # 步骤 3:在种子节点上执行 Restic 恢复
    # -----------------------------------------------------------------
    - name: Recover Raft backup on Seed Node
      block:
        - name: Clean existing Swarm directory on seed node
          file:
            path: /var/lib/docker/swarm
            state: absent

        - name: Recreate Swarm directory container
          file:
            path: /var/lib/docker/swarm
            state: directory
            mode: '0700'

        - name: Pull latest healthy snapshot from Restic
          shell: |
            restic restore latest \
              --target / \
              --include /var/lib/docker/swarm
          environment:
            RESTIC_REPOSITORY: "{{ restic_repo }}"
            RESTIC_PASSWORD: "{{ restic_password }}"
            AWS_ACCESS_KEY_ID: "{{ aws_access_key_id }}"
            AWS_SECRET_ACCESS_KEY: "{{ aws_secret_access_key }}"
      when: inventory_hostname == swarm_seed_host

    # -----------------------------------------------------------------
    # 步骤 4:激活种子节点,强行生成单节点 Raft 领导权
    # -----------------------------------------------------------------
    - name: Start Docker on Seed Node
      systemd:
        name: docker
        state: started
      when: inventory_hostname == swarm_seed_host

    - name: Force new Swarm cluster initialization on Seed Node
      shell: |
        docker swarm init \
          --force-new-cluster \
          --advertise-addr {{ hostvars[inventory_hostname]['ansible_' + swarm_interface]['ipv4']['address'] }}
      when: inventory_hostname == swarm_seed_host
      register: swarm_init_result

    - name: Retrieve new Manager Join Token from Seed Node
      shell: "docker swarm join-token manager -q"
      when: inventory_hostname == swarm_seed_host
      register: manager_token_raw

    - name: Set Join Token and Seed IP as global fact
      set_fact:
        new_manager_token: "{{ hostvars[swarm_seed_host]['manager_token_raw']['stdout'] }}"
        seed_private_ip: "{{ hostvars[swarm_seed_host]['ansible_' + swarm_interface]['ipv4']['address'] }}"
      run_once: true

    # -----------------------------------------------------------------
    # 步骤 5:启动其他 Manager 的 Docker,并重组集群
    # -----------------------------------------------------------------
    - name: Start Docker on remaining managers
      systemd:
        name: docker
        state: started
      when: inventory_hostname != swarm_seed_host

    - name: Join remaining managers to the new Swarm cluster
      shell: |
        docker swarm join \
          --token {{ new_manager_token }} \
          {{ seed_private_ip }}:2377
      when: inventory_hostname != swarm_seed_host
      retries: 3
      delay: 5
      register: join_result
      until: join_result is succeeded

    # -----------------------------------------------------------------
    # 步骤 6:健康状态验证
    # -----------------------------------------------------------------
    - name: Check Swarm node status from Seed
      shell: "docker node ls"
      when: inventory_hostname == swarm_seed_host
      register: swarm_nodes_status

    - name: Show Swarm Cluster Status
      debug:
        var: swarm_nodes_status.stdout_lines
      when: inventory_hostname == swarm_seed_host

4. 关键步骤原理解析与高可用避坑

① 为什么要执行 docker swarm init --force-new-cluster

当 Raft 数据库中的旧成员无法通信时,直接启动 Docker,Swarm 将永远卡在“寻找共识成员”的自锁阶段。--force-new-cluster 告诉守护进程:“丢弃当前 Raft 记录中的所有其他 Manager 成员身份,以我自己恢复出的数据为准,重新生成单成员共识组,并转为 Active 状态。”

这是灾难恢复中至关重要的一步,切不可直接在多台机器上并发执行此命令。

② Swarm Autolock(自动锁定)的处理

如果在灾难发生前,你的 Swarm 集群启用了 Autolock(即在重启 Docker 时需要手动输入解锁密钥以解密 Raft 密钥对),那么在种子节点启动 Docker 并执行恢复后,你需要先运行 docker swarm unlock 并输入原解锁密钥,然后再进行后续的操作。

如果你的集群开启了该功能,建议在 Playbook 的启动任务后增加交互式提示或利用 Ansible 变量自动填入 unlock_key

- name: Unlock Swarm if locked
  shell: "docker swarm unlock <<EOF\n{{ swarm_unlock_key }}\nEOF"
  when: inventory_hostname == swarm_seed_host and swarm_is_locked

③ Restic 恢复时的增量更新

由于 /var/lib/docker/swarm 中可能包含数万个小型的 Raft 状态日志段文件(wal 和 snap),Restic 的快速检索和解密传输机制能够将恢复时间控制在数秒内。
在执行 restic restore 前,执行 file: path=/var/lib/docker/swarm state=absent 是推荐的做法,这能确保不会有残留的脏数据干扰恢复后的 Raft 状态,防止出现数据合并冲突。


5. 灾后健康度验证

Playbook 执行完成后,你可以在 swarm_seed_host 上运行以下命令,验证集群是否已恢复元气:

# 查看节点状态,确认所有 Manager 节点均显示 Ready 且 Status 为 Active
docker node ls

# 验证服务(Services)是否自动重新拉起
docker service ls

注意: 重新组建集群后,由于原有的 Overlay 网络配置是从 Raft 库中一同恢复出来的,原运行在 Worker 节点上的容器通常可以无缝重连至新的 Manager,无需重启所有业务容器。

6. 总结与日常预案建议

利用 Ansible + Restic 构建的 Swarm 恢复方案,可以在集群遭遇不可逆脑裂后的 2~5 分钟内一键重建控制平面。为了确保该机制在真实灾难场景中切实有效,建议在日常运维中坚持以下规范:

  1. 高频备份计划:建议利用 Systemd Timer 或 Cron,每隔 1 小时或至少每天一次执行 restic backup /var/lib/docker/swarm
  2. 异地灾备:务必将 Restic 仓库托管在外部高可用对象存储或异地机房,防止整个宿主机集群物理损坏。
  3. 定期进行脑裂演练:在非生产环境,可以通过人为对 Manager 节点注入 iptables -A INPUT -p tcp --dport 2377 -j DROP 制造脑裂,以此演练上述 Playbook,确保在突发故障时运维团队能够临危不乱。
DevOps匠人 AnsibleRestic

评论点评