← 返回AI教程
🌐 其他

高可用设计模式:冗余、故障转移与仲裁

来源:掘金 · 发布于 2026-08-10 23:11:57
Active-Passive/Active-Active, failover, split-brain fencing, VIP and DNS switch tradeoffs.

高可用设计模式:冗余、故障转移与仲裁

ltl 2026-08-10 10 阅读21分钟

原文发布于 quant67.com,转载请保留出处。

一个支付系统的数据库主节点在凌晨 3:17 发生磁盘故障,备节点检测到心跳超时并在 8 秒内完成切换,对外服务中断时间控制在 10 秒以内。另一个电商平台的数据库也发生了类似故障,但因为备节点和主节点同时认为自己是主——脑裂(Split-Brain)——两个节点各自接受写入,导致订单数据出现不可调和的冲突,恢复用了 6 小时。

两个系统都声称自己是"高可用架构"。差别在于:前者的高可用设计经过了故障检测、仲裁机制和数据一致性三个层面的完整考量;后者只做了冗余部署,缺少仲裁和 Fencing 机制。

高可用(High Availability,HA)不是一个功能,而是一组设计模式的组合。冗余提供了故障发生后的恢复能力,但冗余本身不等于高可用——如何检测故障、如何决定切换、切换过程中如何保证数据一致、如何避免脑裂,这些问题的答案决定了一个系统到底能做到几个 9。

本文从度量体系开始,逐层拆解冗余模型、故障检测、切换机制、脑裂防护、网络层切换、数据一致性,最后给出工程案例和模式选型对比。


一、停机的代价:为什么需要高可用

停机的代价可以用三个维度来衡量:直接经济损失、用户信任损失、合规风险。

直接经济损失。 2017 年 Amazon 的 S3 故障持续约 4 小时,影响了大量依赖 S3 的互联网服务,据估算造成超过 1.5 亿美元的直接经济损失。对于在线交易系统,每分钟停机可能意味着数万笔交易无法完成。一个日均交易额 1 亿元的支付系统,如果每分钟处理约 7 万元交易,停机 30 分钟的直接交易损失就超过 200 万元。

用户信任损失。 用户对服务可用性的容忍度在过去十年里持续降低。移动互联网时代,一个 App 打不开,用户 3 秒内就会切换到竞争对手。信任一旦失去,恢复的成本远高于技术修复本身的成本。

合规风险。 金融、医疗、电信行业有明确的可用性要求。中国人民银行对支付清算系统的可用性要求是 99.99%,欧盟的 DORA(Digital Operational Resilience Act)对金融机构的 ICT 系统可用性有法律层面的约束。

这些代价不是线性的。从 99.9% 提升到 99.99% 的工程成本可能是 10 倍,但从 99.9% 掉到 99% 的业务损失可能是 100 倍。高可用设计的目标不是"永不停机"——那在物理上不可能——而是把停机时间控制在业务可以承受的范围内。


二、可用性度量:MTBF、MTTR 与"几个 9"

基本概念

高可用的定量描述依赖三个核心指标:

MTBF(Mean Time Between Failures,平均故障间隔时间): 系统两次故障之间的平均正常运行时间。MTBF 越大,说明系统越稳定、故障频率越低。

MTTR(Mean Time To Repair,平均修复时间): 从故障发生到服务恢复的平均时间。MTTR 包含故障检测时间、诊断时间、修复时间和恢复验证时间。

MTTF(Mean Time To Failure,平均无故障时间): 对于不可修复的组件(如硬盘),使用 MTTF 而非 MTBF。对于可修复的系统,MTBF ≈ MTTF + MTTR。

可用性(Availability)的计算公式:

A = MTBF / (MTBF + MTTR)

这个公式揭示了一个关键洞察:提升可用性有两条路径——增大 MTBF(减少故障发生的频率)或者减小 MTTR(缩短故障恢复的时间)。高可用设计在两个方向上同时发力,但实践中,缩短 MTTR 的投入产出比往往更高。

原因很简单:MTBF 的提升受限于硬件可靠性、软件缺陷率等难以根本消除的因素;而 MTTR 可以通过自动化故障检测、自动化切换、预配置备节点等工程手段大幅降低。

"几个 9"的含义

工业界用"几个 9"来描述可用性等级。下面的表格列出了不同等级的含义:

可用性等级百分比每年允许停机时间每月允许停机时间每周允许停机时间典型场景
2 个 999%3.65 天7.31 小时1.68 小时内部管理系统
3 个 999.9%8.77 小时43.83 分钟10.08 分钟一般业务系统
3.5 个 999.95%4.38 小时21.92 分钟5.04 分钟电商平台
4 个 999.99%52.60 分钟4.38 分钟1.01 分钟支付系统、核心交易
4.5 个 999.995%26.30 分钟2.19 分钟30.24 秒电信核心网
5 个 999.999%5.26 分钟26.30 秒6.05 秒航空管制、医疗设备

从这张表可以看出几个关键信息:

第一,每提升一个 9,允许的停机时间缩短约 10 倍。 从 3 个 9 到 4 个 9,允许停机时间从每年 8.77 小时降到 52.60 分钟。

第二,4 个 9 意味着每个月的停机窗口只有约 4 分钟。 这意味着故障检测加切换的总时间必须控制在分钟级别以内——人工介入几乎不可能,必须依赖自动化。

第三,5 个 9 意味着每年只允许 5 分钟停机。 这个级别的可用性通常需要全冗余硬件、无单点故障设计、自动化故障检测和切换、以及完善的灾备方案。

可用性的计算陷阱

可用性计算有几个常见的误区:

串联系统的可用性。 如果一个请求链路经过 A、B、C 三个服务,每个服务的可用性都是 99.9%,那么整条链路的可用性是:

A_total = 0.999 × 0.999 × 0.999 = 0.997 ≈ 99.7%

三个 3 个 9 的服务串联,整体只有不到 3 个 9。这就是为什么微服务架构下,单个服务的可用性标准往往需要高于系统整体的目标。

并联系统的可用性。 如果同一个服务部署了两个相互独立的副本,任一副本可用即服务可用,那么:

A_parallel = 1 - (1 - A)^2 = 1 - (0.001)^2 = 1 - 0.000001 = 99.9999%

两个 3 个 9 的节点并联,理论可用性达到 6 个 9。但这有一个前提——两个节点的故障必须是独立的。如果共用同一个电源、同一个机架、同一个交换机,那么一个硬件故障可能同时击倒两个节点,独立性假设就不成立了。

计划内停机算不算? 不同组织的口径不同。有些组织把计划内维护窗口排除在可用性计算之外,有些则全部计入。SLA(Service Level Agreement)里必须明确这一点,否则同样是"4 个 9",实际含义可能完全不同。


三、冗余模型:从 Active-Passive 到 N+M

冗余(Redundancy)是高可用的基础。没有冗余就没有接管资源。冗余模式的选择直接影响成本、复杂度和切换速度。

Active-Passive(主备模式)

Active-Passive 也叫主备模式(Master-Slave 或 Primary-Standby)。系统中有一个主节点(Active)处理所有请求,一个或多个备节点(Passive)处于等待状态,当主节点故障时,备节点接管服务。

graph TB
    Client[客户端] --> VIP[VIP 虚拟 IP]
    VIP --> Active[主节点 Active]
    Active -.->|数据同步| Standby[备节点 Passive]
    Active -.->|心跳检测| Standby
    
    subgraph 正常状态
        Active
        Standby
    end
    
    style Active fill:#4CAF50,color:#fff
    style Standby fill:#9E9E9E,color:#fff
    style VIP fill:#2196F3,color:#fff

工作机制:

  1. 主节点承担所有读写请求。
  2. 数据通过同步或异步复制传输到备节点。
  3. 心跳机制持续检测主节点状态。
  4. 主节点故障时,备节点提升为新的主节点,VIP 漂移到新主节点。

优点:

  • 架构简单,状态管理清晰——在任何时刻,只有一个节点接受写入,不存在写冲突。
  • 切换逻辑相对简单,只需要做一次角色转换。
  • 对应用层透明——应用只需要连接 VIP,不需要感知主备切换。

缺点:

  • 备节点资源利用率低——在正常情况下,备节点处于闲置状态(或只承担少量只读查询),硬件资源浪费。
  • 切换过程有中断——从检测到故障到完成切换,通常需要数秒到数十秒,期间服务不可用。
  • 数据同步延迟——如果使用异步复制,切换时可能丢失未同步的数据。

典型使用场景: 传统数据库主备(MySQL 主从、PostgreSQL 流复制)、Redis Sentinel 模式、单体应用的 HA 部署。

Active-Active(双活模式)

Active-Active 也叫双活或多活(Multi-Active)。所有节点同时处理请求,没有"备"的概念。

工作机制:

  1. 多个节点同时接受读写请求。
  2. 请求通过负载均衡器分发到各节点。
  3. 节点间需要实时数据同步机制来保证一致性。
  4. 任一节点故障时,负载均衡器将其从服务池中移除,流量自动分发到剩余节点。

优点:

  • 资源利用率高——所有节点都在工作,没有闲置资源。
  • 切换速度快——不需要角色转换,只需要从负载均衡池中摘除故障节点。
  • 天然支持横向扩展——增加节点即可提升处理能力。

缺点:

  • 数据一致性挑战大——多个节点同时写入,需要解决写冲突问题。
  • 架构复杂度高——需要分布式一致性协议(如 Paxos、Raft)或冲突解决策略(如 CRDT、Last-Write-Wins)。
  • 脑裂风险——网络分区时,不同节点可能接受冲突的写入。

典型使用场景: 无状态应用集群、DNS 轮询、分布式缓存集群、跨数据中心部署。

关于"真双活"的说明: 严格意义上的数据库双活(两个节点同时写入同一份数据且保证强一致性)在工程上极其复杂。很多声称"双活"的方案,实际上是"读双活、写单活",或者"按分片双活"——不同的数据分片有各自的主节点,分布在不同数据中心,整体看起来是双活,但对每一个分片来说仍然是 Active-Passive。

N+1 冗余

N+1 冗余的含义是:N 个工作节点加 1 个备用节点。正常情况下 N 个节点处理请求,备用节点待命。任一工作节点故障时,备用节点接管。

和 Active-Passive 的区别: Active-Passive 通常是 1+1(一主一备),N+1 是多主一备。N+1 的备用节点是"共享"的——它不是某个特定工作节点的备份,而是整个集群的备份。这意味着备用资源的比例更低,成本效率更高。

示例: 一个服务需要 5 个节点才能承载峰值流量。1+1 方案需要部署 10 个节点(5 个主,5 个备),资源利用率 50%。N+1 方案只需要 6 个节点(5 个工作节点,1 个备用),资源利用率 83%。

限制: N+1 假设同一时间最多只有一个节点故障。如果两个工作节点同时故障,备用节点只能接管其中一个。对于故障概率较高或对可用性要求极高的场景,需要 N+2 或更高的冗余度。

N+M 冗余

N+M 冗余是 N+1 的泛化:N 个工作节点加 M 个备用节点。M 的选择取决于对同时故障数量的容忍度。

M 值的确定方法:

  • 基于历史故障数据:统计过去一年里同时发生的最大故障节点数,取该值或略高于该值作为 M。
  • 基于可用性目标:如果要求在任意 K 个节点同时故障时仍能维持服务,那么 M ≥ K。
  • 基于成本约束:M 越大,冗余成本越高。通常在可用性目标和成本之间做权衡。

典型使用场景: 大规模集群部署(如 Kubernetes 中的 ReplicaSet,设置副本数 = 需要数 + 冗余数)、数据中心级别的服务器冗余、网络设备冗余。

冗余模型对比

维度Active-PassiveActive-ActiveN+1N+M
资源利用率50%(1+1 场景)接近 100%N/(N+1)N/(N+M)
切换速度秒级到十秒级毫秒到秒级秒级到十秒级秒级到十秒级
数据一致性较好(单写入点)复杂(多写入点)较好较好
架构复杂度低高中中
容忍同时故障数1取决于剩余容量1M
典型成本比2x1.2x-1.5x1.2x1+(M/N)x

四、故障检测与切换机制

冗余只是高可用的前提。有了备用节点,还需要回答:主节点是否故障了?确认故障后谁来接管?接管过程怎么执行?

心跳检测(Heartbeat Detection)

心跳检测是最基础的故障检测方式。备节点(或监控节点)定期向主节点发送探测包,如果连续 N 次未收到响应,则判定主节点故障。

心跳检测的配置通常涉及三个参数:

  • 心跳间隔(Heartbeat Interval): 两次心跳之间的时间间隔。通常设置为 1-5 秒。
  • 超时阈值(Timeout): 未收到心跳响应的最大等待时间。通常设置为心跳间隔的 2-3 倍。
  • 重试次数(Retry Count): 连续多少次心跳失败后判定故障。通常设置为 3-5 次。

以 Keepalived 为例,心跳检测的配置如下:

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1          # 心跳间隔:1 秒

    authentication {
        auth_type PASS
        auth_pass mypassword
    }

    virtual_ipaddress {
        192.168.1.100/24   # VIP 地址
    }

    track_script {
        chk_service        # 关联服务检查脚本
    }
}

vrrp_script chk_service {
    script "/etc/keepalived/check_service.sh"
    interval 2             # 每 2 秒执行一次检查
    weight -20             # 检查失败时降低优先级 20
    fall 3                 # 连续 3 次失败判定为故障
    rise 2                 # 连续 2 次成功判定为恢复
}

对应的服务检查脚本:

#!/bin/bash
# /etc/keepalived/check_service.sh
# 检查 MySQL 是否可用

MYSQL_HOST="127.0.0.1"
MYSQL_PORT=3306
MYSQL_USER="monitor"
MYSQL_PASS="monitor_pass"

# 尝试执行简单查询
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" \
    -e "SELECT 1" > /dev/null 2>&1

if [ $? -ne 0 ]; then
    # MySQL 不可用,返回非零退出码
    exit 1
fi

# 检查复制延迟(可选)
SLAVE_LAG=$(mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" \
    -e "SHOW SLAVE STATUS\G" 2>/dev/null | grep "Seconds_Behind_Master" | awk '{print $2}')

if [ -n "$SLAVE_LAG" ] && [ "$SLAVE_LAG" -gt 30 ]; then
    # 复制延迟超过 30 秒,判定为不健康
    exit 1
fi

exit 0

心跳检测的局限性: 心跳检测只能检测"节点是否可达",不能检测"服务是否正常"。一个节点可能网络可达但数据库进程已挂,也可能数据库进程在运行但不能正常响应查询。因此生产环境使用应用级别的健康检查——如上面的脚本实际执行 SQL 查询来验证。

仲裁投票(Quorum Voting)

心跳检测有一个根本问题:如果主节点和备节点之间的网络中断了,但主节点本身是正常的怎么办?备节点会认为主节点故障并尝试接管,结果两个节点同时提供服务——脑裂。

仲裁投票(Quorum)机制的核心思想是:不依赖单一节点的判断,而是让多个节点共同投票决定故障。只有获得多数票(Quorum)的节点组才能继续提供服务。

Quorum 的计算: 对于一个由 N 个节点组成的集群,Quorum = floor(N/2) + 1。例如,3 节点集群的 Quorum 是 2,5 节点集群的 Quorum 是 3。

工作机制:

  1. 每个节点持续与其他所有节点保持心跳。
  2. 当某个节点检测到其他节点不可达时,它会统计自己能联系到的节点数。
  3. 如果能联系到的节点数(含自己)≥ Quorum,则认为自己在"多数派"一侧,继续提供服务。
  4. 如果能联系到的节点数 < Quorum,则认为自己在"少数派"一侧,主动停止服务。
class QuorumChecker:
    """仲裁检查器:判断当前节点是否在多数派一侧"""

    def __init__(self, cluster_nodes: list[str], self_node: str):
        self.cluster_nodes = cluster_nodes
        self.self_node = self_node
        self.quorum_size = len(cluster_nodes) // 2 + 1

    def check_quorum(self, reachable_nodes: set[str]) -> bool:
        """检查当前可达节点数是否满足 Quorum 要求"""
        # 加上自己
        reachable_count = len(reachable_nodes) + 1
        has_quorum = reachable_count >= self.quorum_size

        if not has_quorum:
            self._step_down()
        return has_quorum

    def _step_down(self):
        """不满足 Quorum,主动降级"""
        print(f"节点 {self.self_node} 不在多数派一侧,"
              f"可达节点数不足 {self.quorum_size},主动停止服务")
        # 停止接受写请求
        # 释放 VIP
        # 记录日志


# 示例:3 节点集群
checker = QuorumChecker(
    cluster_nodes=["node-1", "node-2", "node-3"],
    self_node="node-1"
)

# 场景 1:node-1 能联系到 node-2,不能联系到 node-3
# 可达节点数 = 2(node-1 + node-2),Quorum = 2,满足
result = checker.check_quorum(reachable_nodes={"node-2"})
print(f"Quorum 检查结果: {result}")  # True

# 场景 2:node-1 不能联系到 node-2 和 node-3
# 可达节点数 = 1(仅 node-1),Quorum = 2,不满足
result = checker.check_quorum(reachable_nodes=set())
print(f"Quorum 检查结果: {result}")  # False

Quorum 与节点数的关系: 3 节点容忍 1 个故障(Quorum=2),5 节点容忍 2 个故障(Quorum=3),7 节点容忍 3 个故障(Quorum=4)。偶数节点没有额外优势——4 节点的 Quorum 是 3,只容忍 1 个故障,和 3 节点相同。因此 Quorum 集群通常使用奇数个节点。

Witness 节点(见证节点)

当集群只有 2 个节点时(最常见的 Active-Passive 部署),无法使用 Quorum 投票——两个节点各持一票,网络分区时无法分出多数派。解决方案是引入第三方 Witness(见证)节点。

Witness 节点不承载业务数据,只参与投票。它的作用是在两个业务节点无法互相通信时,充当仲裁者:哪个业务节点能联系到 Witness,哪个就获得继续服务的权利。

# Corosync 集群配置示例(含 Witness 节点)
totem {
    version: 2
    secauth: on
    cluster_name: payment-db-cluster
    transport: udpu
}

nodelist {
    node {
        ring0_addr: 10.0.1.10    # 主节点
        name: db-primary
        nodeid: 1
    }
    node {
        ring0_addr: 10.0.1.11    # 备节点
        name: db-standby
        nodeid: 2
    }
    node {
        ring0_addr: 10.0.2.10    # Witness 节点(不同子网)
        name: witness
        nodeid: 3
    }
}

quorum {
    provider: corosync_votequorum
    expected_votes: 3
    two_node: 0                   # 不使用 two_node 模式
}

Witness 节点的部署原则:

  • Witness 必须部署在与两个业务节点不同的故障域。如果主备节点在同一个机房,Witness 应该在另一个机房或云可用区。
  • Witness 的网络连接应该独立于主备节点之间的网络。否则一次网络故障可能同时切断主备通信和 Witness 通信。
  • Witness 本身的可用性要求不低于业务节点——如果 Witness 故障了,集群退化为 2 节点集群,又回到了无法仲裁的状态。

故障检测延迟分析

从故障发生到完成切换的总时间,决定了系统的实际 MTTR。这个过程可以分解为四个阶段:

阶段典型耗时影响因素
故障发生到心跳超时3-15 秒心跳间隔 × 重试次数
仲裁投票完成1-5 秒集群规模、网络延迟
角色切换(VIP 漂移、服务启动)1-10 秒服务启动时间、ARP 广播
客户端连接恢复1-30 秒连接池超时、重试策略
总计6-60 秒

以一个典型的 Keepalived + MySQL 主备架构为例:

  • 心跳间隔 1 秒,3 次超时判定故障:最长 3 秒。
  • VIP 漂移(免费 ARP 广播):通常 1 秒以内。
  • MySQL 备节点提升为主(如果使用半同步复制):1-2 秒。
  • 应用连接池检测到连接断开并重建:取决于连接池配置,通常 5-30 秒。

总切换时间:约 10-36 秒。这个时间对于 4 个 9 的可用性来说足够了(每月允许 4.38 分钟停机),但对于需要秒级切换的场景,需要更积极的配置。


五、脑裂问题与 Fencing

什么是脑裂

脑裂(Split-Brain)是分布式系统中最危险的故障之一。它发生在以下场景:

集群中的节点之间的通信中断(网络分区),但各节点本身运行正常。每一侧的节点都认为对方已经故障,于是各自提升为主节点,独立接受写请求。当网络恢复后,两侧的数据已经分叉,无法自动合并。

sequenceDiagram
    participant C1 as 客户端 A
    participant N1 as 节点 1(原主)
    participant N2 as 节点 2(备)
    participant C2 as 客户端 B

    Note over N1,N2: 正常状态:节点 1 为主,节点 2 为备
    C1->>N1: 写入订单 #1001
    N1->>N2: 同步数据

    Note over N1,N2: 网络分区发生
    N1--xN2: 心跳超时
    N2--xN1: 心跳超时

    Note over N2: 节点 2 判定节点 1 故障,自我提升为主
    Note over N1: 节点 1 仍在正常运行

    C1->>N1: 写入订单 #1002(金额 100)
    C2->>N2: 写入订单 #1002(金额 200)

    Note over N1,N2: 网络恢复
    Note over N1,N2: 订单 #1002 出现冲突:<br/>节点 1 记录金额 100<br/>节点 2 记录金额 200

脑裂的危害:

  • 数据不一致: 两个主节点各自接受写入,产生冲突数据。对于支付系统,这可能意味着同一笔交易被处理两次。
  • 数据损坏: 某些存储引擎在同时被两个实例写入时可能导致文件损坏(例如共享存储场景)。
  • 恢复困难: 脑裂后的数据合并没有通用方案。人工介入排查和修复可能需要数小时甚至数天。

脑裂的根因

脑裂的根因可以归结为一句话:无法区分"节点故障"和"网络分区"。

从观察者的视角看,"对方没有响应心跳"有两种可能的解释:

  1. 对方确实挂了(节点故障),应该切换。
  2. 对方没挂,只是网络断了(网络分区),不应该切换。

如果不能区分这两种情况就贸然切换,就可能产生脑裂。

STONITH:确保故障节点不再运行

STONITH(Shoot The Other Node In The Head)的策略很直接:在切换之前,先确保原来的主节点已经被彻底关闭。即使不确定它是真的故障了还是只是网络不通,直接把它关机(或者重启),然后再执行切换。

STONITH 的实现方式:

方式机制适用环境
IPMI/BMC通过带外管理接口发送关机指令物理服务器
PDU(电源分配单元)远程切断目标服务器的电源数据中心
虚拟化 API调用 vSphere/KVM API 关闭虚拟机虚拟化环境
云 API调用 AWS/Azure/GCP API 停止实例云环境
SBD(Storage-Based Death)通过共享存储上的"毒药丸"机制触发自杀共享存储集群

以 Pacemaker 集群的 STONITH 配置为例:

# 配置 IPMI STONITH 设备
pcs stonith create ipmi-fencing fence_ipmilan \
    ipaddr="10.0.1.200" \
    login="admin" \
    passwd="admin_pass" \
    pcmk_host_list="db-primary" \
    pcmk_host_check="static-list" \
    power_wait="5"

# 配置 STONITH 策略
pcs property set stonith-enabled=true
pcs property set stonith-action=reboot
pcs property set stonith-timeout=60s

# 验证 STONITH 配置
pcs stonith show
pcs stonith fence db-primary --off

在云环境中,STONITH 通过云 API 实现——调用 ec2.stop_instances(Force=True) 或等价的 API 强制停止目标实例,然后通过 describe_instances 验证实例确实已停止。

STONITH 的争议:

STONITH 是一个"宁可误杀,不可放过"的策略。它的好处是彻底消除脑裂风险——原主节点被物理关机了,不可能再接受写入。但它的代价也很明显:

  • 如果原主节点其实是健康的(只是网络抖动导致心跳超时),STONITH 会把一个健康的节点强制关机,然后再做切换。这意味着整个集群从 2 个节点变成 1 个节点,直到被关机的节点重新启动并加入集群。
  • STONITH 依赖带外管理通道(IPMI、云 API)的可用性。如果该通道不可用,STONITH 无法执行,集群会陷入僵局。

Quorum-Based 方案

Quorum 方案在前一节已经讨论过。它的核心是"少数派自杀":网络分区发生后,不在多数派一侧的节点主动停止服务。这样可以保证在任何时刻,最多只有一组节点在提供服务。

Quorum 方案和 STONITH 并不互斥。生产环境通常同时使用:Quorum 负责"谁有权成为主"的决策,STONITH 负责"确保旧主已经停止"的执行。

# Pacemaker 集群同时启用 Quorum 和 STONITH
pcs property set no-quorum-policy=stop      # 失去 Quorum 时停止所有资源
pcs property set stonith-enabled=true        # 启用 STONITH

# 配置 Quorum 策略
# last_man_standing: 集群缩小时自动调整 expected_votes
# wait_for_all: 启动时等待所有节点加入
cat > /etc/corosync/corosync.conf.d/quorum.conf << 'EOF'
quorum {
    provider: corosync_votequorum
    expected_votes: 3
    wait_for_all: 1
    last_man_standing: 1
    last_man_standing_window: 10000
}
EOF

Fencing 策略的选择

方案防脑裂效果实施复杂度适用场景
STONITH(IPMI)强中物理服务器、数据中心
STONITH(云 API)强低云环境
SBD(存储毒药丸)强高共享存储集群
Quorum + 少数派自杀中强中3 节点及以上集群
Witness 节点中低2 节点集群
不做 Fencing无无不推荐用于生产环境

六、VIP 漂移 vs DNS 切换

故障检测完成、仲裁投票通过之后,最后一步是让客户端流量切换到新的主节点。两种主流方式是 VIP 漂移和 DNS 切换。

VIP 漂移(VRRP/Keepalived)

VIP(Virtual IP,虚拟 IP)漂移的原理是:一个虚拟 IP 地址不绑定到任何物理网卡,而是由集群管理软件动态绑定到当前的主节点。当主节点故障时,VIP 被迁移到新的主节点,客户端继续使用同一个 IP 地址访问服务。

VRRP(Virtual Router Redundancy Protocol,虚拟路由冗余协议)是 VIP 漂移的标准协议。Keepalived 是 VRRP 的主流开源实现。

VIP 漂移的过程:

  1. 主节点持有 VIP,并通过 VRRP 协议定期向备节点发送通告。
  2. 备节点监听 VRRP 通告。如果超过设定时间未收到通告,备节点判定主节点故障。
  3. 备节点接管 VIP:将 VIP 绑定到自己的网卡上。
  4. 备节点发送免费 ARP(Gratuitous ARP)广播,通知局域网内所有设备更新 ARP 缓存——"这个 IP 地址现在对应的 MAC 地址是我的"。
  5. 后续发往 VIP 的数据包被路由到新的主节点。

Keepalived 完整配置示例:

# /etc/keepalived/keepalived.conf(主节点)
global_defs {
    router_id db_primary
    vrrp_skip_check_adv_addr
    vrrp_garp_interval 0
    vrrp_gna_interval 0
}

vrrp_script chk_mysql {
    script "/etc/keepalived/check_mysql.sh"
    interval 2
    weight -30
    fall 3
    rise 2
}

vrrp_instance VI_DB {
    state MASTER
    interface eth0
    virtual_router_id 100
    priority 100
    advert_int 1
    nopreempt                    # 不抢占:故障恢复后不自动切回

    unicast_src_ip 10.0.1.10    # 使用单播替代组播
    unicast_peer {
        10.0.1.11
    }

    authentication {
        auth_type PASS
        auth_pass ha_secret_2024
    }

    virtual_ipaddress {
        10.0.1.100/24 dev eth0   # VIP 地址
    }

    track_script {
        chk_mysql
    }

    notify_master "/etc/keepalived/notify.sh master"
    notify_backup "/etc/keepalived/notify.sh backup"
    notify_fault  "/etc/keepalived/notify.sh fault"
}

备节点的配置与主节点基本相同,关键差异是 state BACKUP 和 priority 90(低于主节点的 100)。当主节点心跳超时,备节点自动接管 VIP。nopreempt 参数确保原主节点恢复后不会自动抢回 VIP,避免不必要的二次切换。

VIP 漂移的优缺点:

优点缺点
切换速度快(通常 1-3 秒)仅限同一二层网络(同一子网)
对客户端完全透明跨数据中心不可用
不依赖 DNS 缓存云环境支持有限(需要 Elastic IP)
实现成熟,经过大量生产验证免费 ARP 可能被网络设备限速

DNS 切换

DNS 切换的原理是:客户端通过域名访问服务,域名解析到主节点的 IP 地址。当主节点故障时,修改 DNS 记录,将域名指向新主节点的 IP。

DNS 切换的过程:

  1. 正常状态下,DNS A 记录指向主节点 IP。
  2. 故障检测系统发现主节点不可用。
  3. 通过 DNS API 修改 A 记录,指向备节点 IP。
  4. 等待 DNS 缓存过期(TTL),客户端逐步切换到新节点。
import boto3


def dns_failover(
    hosted_zone_id: str,
    record_name: str,
    new_ip: str,
    ttl: int = 30
):
    """通过 Route53 API 执行 DNS 切换"""
    client = boto3.client("route53")

    response = client.change_resource_record_sets(
        HostedZoneId=hosted_zone_id,
        ChangeBatch={
            "Comment": f"Failover to {new_ip}",
            "Changes": [
                {
                    "Action": "UPSERT",
                    "ResourceRecordSet": {
                        "Name": record_name,
                        "Type": "A",
                        "TTL": ttl,
                        "ResourceRecords": [
                            {"Value": new_ip}
                        ]
                    }
                }
            ]
        }
    )
    return response["ChangeInfo"]["Status"]

云厂商(如 AWS Route53、阿里云 DNS)还支持基于健康检查的自动 DNS 故障转移:配置两条 Failover 类型的记录(PRIMARY 和 SECONDARY),当 PRIMARY 健康检查失败时,DNS 自动返回 SECONDARY 的 IP。

TTL 与切换延迟:

DNS 切换的最大问题是 TTL(Time To Live)。DNS 记录的 TTL 决定了客户端和中间 DNS 服务器缓存该记录的时间。即使你在 1 秒内修改了 DNS 记录,客户端可能在 TTL 过期之前仍然使用旧的 IP 地址。

TTL 设置切换延迟DNS 查询负载适用场景
300 秒(5 分钟)最长 5 分钟低对切换时间不敏感的场景
60 秒最长 1 分钟中一般业务系统
30 秒最长 30 秒中高需要较快切换的场景
10 秒最长 10 秒高对切换时间敏感的场景

注意:TTL 只是建议值,某些客户端和中间 DNS 服务器可能不遵守 TTL,缓存时间更长。Java 的默认 DNS 缓存在某些 JVM 版