一个支付系统的数据库主节点在凌晨 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 个 9 | 99% | 3.65 天 | 7.31 小时 | 1.68 小时 | 内部管理系统 |
| 3 个 9 | 99.9% | 8.77 小时 | 43.83 分钟 | 10.08 分钟 | 一般业务系统 |
| 3.5 个 9 | 99.95% | 4.38 小时 | 21.92 分钟 | 5.04 分钟 | 电商平台 |
| 4 个 9 | 99.99% | 52.60 分钟 | 4.38 分钟 | 1.01 分钟 | 支付系统、核心交易 |
| 4.5 个 9 | 99.995% | 26.30 分钟 | 2.19 分钟 | 30.24 秒 | 电信核心网 |
| 5 个 9 | 99.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
工作机制:
- 主节点承担所有读写请求。
- 数据通过同步或异步复制传输到备节点。
- 心跳机制持续检测主节点状态。
- 主节点故障时,备节点提升为新的主节点,VIP 漂移到新主节点。
优点:
- 架构简单,状态管理清晰——在任何时刻,只有一个节点接受写入,不存在写冲突。
- 切换逻辑相对简单,只需要做一次角色转换。
- 对应用层透明——应用只需要连接 VIP,不需要感知主备切换。
缺点:
- 备节点资源利用率低——在正常情况下,备节点处于闲置状态(或只承担少量只读查询),硬件资源浪费。
- 切换过程有中断——从检测到故障到完成切换,通常需要数秒到数十秒,期间服务不可用。
- 数据同步延迟——如果使用异步复制,切换时可能丢失未同步的数据。
典型使用场景: 传统数据库主备(MySQL 主从、PostgreSQL 流复制)、Redis Sentinel 模式、单体应用的 HA 部署。
Active-Active(双活模式)
Active-Active 也叫双活或多活(Multi-Active)。所有节点同时处理请求,没有”备”的概念。
工作机制:
- 多个节点同时接受读写请求。
- 请求通过负载均衡器分发到各节点。
- 节点间需要实时数据同步机制来保证一致性。
- 任一节点故障时,负载均衡器将其从服务池中移除,流量自动分发到剩余节点。
优点:
- 资源利用率高——所有节点都在工作,没有闲置资源。
- 切换速度快——不需要角色转换,只需要从负载均衡池中摘除故障节点。
- 天然支持横向扩展——增加节点即可提升处理能力。
缺点:
- 数据一致性挑战大——多个节点同时写入,需要解决写冲突问题。
- 架构复杂度高——需要分布式一致性协议(如 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-Passive | Active-Active | N+1 | N+M |
|---|---|---|---|---|
| 资源利用率 | 50%(1+1 场景) | 接近 100% | N/(N+1) | N/(N+M) |
| 切换速度 | 秒级到十秒级 | 毫秒到秒级 | 秒级到十秒级 | 秒级到十秒级 |
| 数据一致性 | 较好(单写入点) | 复杂(多写入点) | 较好 | 较好 |
| 架构复杂度 | 低 | 高 | 中 | 中 |
| 容忍同时故障数 | 1 | 取决于剩余容量 | 1 | M |
| 典型成本比 | 2x | 1.2x-1.5x | 1.2x | 1+(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。
工作机制:
- 每个节点持续与其他所有节点保持心跳。
- 当某个节点检测到其他节点不可达时,它会统计自己能联系到的节点数。
- 如果能联系到的节点数(含自己)≥ Quorum,则认为自己在”多数派”一侧,继续提供服务。
- 如果能联系到的节点数 < 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}") # FalseQuorum 与节点数的关系: 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
脑裂的危害:
- 数据不一致: 两个主节点各自接受写入,产生冲突数据。对于支付系统,这可能意味着同一笔交易被处理两次。
- 数据损坏: 某些存储引擎在同时被两个实例写入时可能导致文件损坏(例如共享存储场景)。
- 恢复困难: 脑裂后的数据合并没有通用方案。人工介入排查和修复可能需要数小时甚至数天。
脑裂的根因
脑裂的根因可以归结为一句话:无法区分”节点故障”和”网络分区”。
从观察者的视角看,“对方没有响应心跳”有两种可能的解释:
- 对方确实挂了(节点故障),应该切换。
- 对方没挂,只是网络断了(网络分区),不应该切换。
如果不能区分这两种情况就贸然切换,就可能产生脑裂。
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
}
EOFFencing 策略的选择
| 方案 | 防脑裂效果 | 实施复杂度 | 适用场景 |
|---|---|---|---|
| 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 漂移的过程:
- 主节点持有 VIP,并通过 VRRP 协议定期向备节点发送通告。
- 备节点监听 VRRP 通告。如果超过设定时间未收到通告,备节点判定主节点故障。
- 备节点接管 VIP:将 VIP 绑定到自己的网卡上。
- 备节点发送免费 ARP(Gratuitous ARP)广播,通知局域网内所有设备更新 ARP 缓存——“这个 IP 地址现在对应的 MAC 地址是我的”。
- 后续发往 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 切换的过程:
- 正常状态下,DNS A 记录指向主节点 IP。
- 故障检测系统发现主节点不可用。
- 通过 DNS API 修改 A 记录,指向备节点 IP。
- 等待 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 版本中是永久的,需要通过 JVM 参数
-Dsun.net.inetaddr.ttl=30 或
java.security.Security.setProperty("networkaddress.cache.ttl", "30")
显式配置。
VIP 漂移 vs DNS 切换对比
| 维度 | VIP 漂移(VRRP/Keepalived) | DNS 切换 |
|---|---|---|
| 切换速度 | 1-3 秒 | 30 秒到数分钟(取决于 TTL) |
| 网络范围 | 同一二层网络 | 跨任意网络 |
| 跨数据中心 | 不支持 | 支持 |
| 客户端改造 | 无需 | 无需(但需注意 DNS 缓存) |
| 云环境兼容 | 部分支持(需要 Elastic IP 或等价物) | 完全支持 |
| 实现复杂度 | 低 | 中 |
| 可靠性 | 高(不依赖缓存) | 中(受 TTL 和 DNS 缓存行为影响) |
选择建议: 同一数据中心内优先使用 VIP 漂移;跨数据中心使用 DNS 切换或 Anycast;也可两者结合——数据中心内部用 VIP,数据中心之间用 DNS。
七、主备切换中的数据一致性
故障切换不仅是”谁来接管服务”的问题,还有”切换后数据是否完整”的问题。核心在于数据复制模式的选择。
同步复制 vs 异步复制
同步复制(Synchronous Replication): 主节点收到写请求后,必须等待备节点确认收到并持久化了数据,才向客户端返回成功。
客户端 → 主节点(写入本地) → 备节点(写入并确认) → 主节点(返回成功给客户端)
优点:切换时不丢数据——备节点拥有和主节点完全一致的数据。
缺点:每次写入都要等待备节点的确认,写入延迟增加(增加一个网络往返时间)。如果备节点不可用,主节点的写入也会被阻塞(或超时失败),可用性反而降低。
异步复制(Asynchronous Replication): 主节点收到写请求后,写入本地即向客户端返回成功,然后异步将数据发送给备节点。
客户端 → 主节点(写入本地,返回成功给客户端) → 备节点(异步接收数据)
优点:写入延迟低,主节点不依赖备节点的可用性。
缺点:切换时可能丢数据——主节点已经确认但尚未同步到备节点的数据会丢失。
半同步复制(Semi-Synchronous Replication): MySQL 的半同步复制是一种折中方案。主节点等待至少一个备节点确认收到了 binlog 事件(写入 relay log),但不需要等待备节点执行(apply)该事件。
-- MySQL 半同步复制配置
-- 主节点
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 5000; -- 5 秒超时
SET GLOBAL rpl_semi_sync_master_wait_for_slave_count = 1;
SET GLOBAL rpl_semi_sync_master_wait_point = 'AFTER_SYNC';
-- 备节点
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
STOP SLAVE IO_THREAD;
START SLAVE IO_THREAD;半同步复制的超时机制:如果在
rpl_semi_sync_master_timeout
时间内没有收到备节点确认,主节点自动降级为异步复制。这意味着在备节点延迟或网络抖动时,系统不会因为等待备节点而阻塞,但数据丢失的风险也回来了。
RPO 与 RTO
RPO(Recovery Point Objective,恢复点目标): 可以容忍丢失多少数据。RPO = 0 意味着不能丢失任何数据,需要同步复制。RPO = 5 分钟意味着可以容忍最多丢失 5 分钟的数据。
RTO(Recovery Time Objective,恢复时间目标): 系统从故障到恢复服务的最长可接受时间。RTO = 30 秒意味着必须在 30 秒内恢复服务。
RPO 和 RTO 之间存在权衡:
| 配置 | RPO | RTO | 写入性能影响 |
|---|---|---|---|
| 同步复制 + 自动切换 | 0(零数据丢失) | 秒级 | 高(延迟增加) |
| 半同步复制 + 自动切换 | 接近 0 | 秒级 | 中 |
| 异步复制 + 自动切换 | 秒级到分钟级 | 秒级 | 低 |
| 异步复制 + 人工切换 | 秒级到分钟级 | 分钟到小时级 | 低 |
| 定期备份 + 人工恢复 | 小时到天级 | 小时级 | 无 |
切换过程中的数据安全
即使使用了同步复制,切换过程本身也可能引入数据一致性问题。以下是几个需要注意的场景:
场景一:正在执行的事务。 主节点故障时,正在执行但尚未提交的事务会被丢弃。这是正确的行为——未提交的事务本来就不应该被持久化。但客户端可能已经发送了写请求并在等待响应,需要在应用层处理超时和重试。
场景二:已提交但未同步的事务。 在异步复制模式下,主节点崩溃时可能有已经提交的事务尚未同步到备节点。这些事务对应的数据会丢失。应用层需要能处理这种情况——例如,通过幂等设计确保重试不会产生副作用。
场景三:切换后的数据不一致窗口。 即使使用半同步复制,备节点在被提升为主时,可能还有已经收到但尚未 apply 的 relay log 事件。新主节点需要先 apply 完所有 relay log,才能开始接受新的写入。这个 apply 过程可能需要几秒到几十秒。
# MySQL 备节点提升为主时的数据一致性检查脚本
#!/bin/bash
# 1. 停止 IO 线程(停止从旧主接收新数据)
mysql -e "STOP SLAVE IO_THREAD;"
# 2. 等待 SQL 线程 apply 完所有 relay log
echo "等待 relay log apply 完成..."
while true; do
EXEC_POS=$(mysql -e "SHOW SLAVE STATUS\G" | grep "Exec_Master_Log_Pos" | awk '{print $2}')
READ_POS=$(mysql -e "SHOW SLAVE STATUS\G" | grep "Read_Master_Log_Pos" | awk '{print $2}')
if [ "$EXEC_POS" = "$READ_POS" ]; then
echo "Relay log 已全部 apply 完毕"
break
fi
echo "等待中... Exec_Pos=$EXEC_POS, Read_Pos=$READ_POS"
sleep 1
done
# 3. 停止复制,清除复制配置
mysql -e "STOP SLAVE; RESET SLAVE ALL;"
# 4. 设置为可写
mysql -e "SET GLOBAL read_only = OFF; SET GLOBAL super_read_only = OFF;"
# 5. 记录当前 binlog 位置(供其他备节点 change master)
mysql -e "SHOW MASTER STATUS\G"
echo "提升完成,当前节点已成为新主节点"复制拓扑与一致性保证
不同的复制拓扑对数据一致性有不同的影响:
链式复制(A → B → C): 主节点 A 同步到 B,B 同步到 C。如果 A 故障,B 提升为主,C 的数据可能落后于 B。需要 C 先追赶到 B 的位置才能继续复制。
星型复制(A → B, A → C): 主节点 A 同时同步到 B 和 C。如果 A 故障,B 和 C 的数据位置可能不同。需要选择数据最新的节点作为新主。
组复制(Group Replication): MySQL Group Replication 和 Galera Cluster 使用基于 Paxos 的协议,所有节点在同一个一致性组内。写入需要获得多数节点的确认,任何节点故障都不影响数据一致性。
八、工程案例:某支付系统数据库高可用架构
以下是一个真实的支付系统数据库高可用架构案例。该系统日均处理约 5000 万笔交易,峰值 TPS 约 2 万,可用性要求 99.99%。
技术选型
| 组件 | 技术 | 说明 |
|---|---|---|
| 数据库 | PostgreSQL 15 | 支持流复制、逻辑复制 |
| HA 管理 | Patroni 3.x | 基于 DCS 的 PostgreSQL HA 管理 |
| 分布式协调 | etcd 3.5 | 存储集群状态,提供 Leader 选举 |
| 连接代理 | PgBouncer | 连接池,减少数据库连接开销 |
| VIP 管理 | 自定义脚本 + 云 API | 通过回调脚本管理 Elastic IP |
| 监控 | Prometheus + Grafana | 集群状态监控和告警 |
架构概览
graph TB
App[应用服务集群] --> PgB_W[PgBouncer 写入]
App --> PgB_R[PgBouncer 只读]
PgB_W --> VIP_W[写入 VIP]
PgB_R --> LB_R[只读负载均衡]
VIP_W --> Primary[PostgreSQL Primary]
LB_R --> Sync[PostgreSQL Sync Standby]
LB_R --> Async[PostgreSQL Async Standby]
Primary -->|同步复制| Sync
Primary -->|异步复制| Async
Patroni_1[Patroni Agent] --> Primary
Patroni_2[Patroni Agent] --> Sync
Patroni_3[Patroni Agent] --> Async
Patroni_1 --> etcd[etcd 集群 3 节点]
Patroni_2 --> etcd
Patroni_3 --> etcd
style Primary fill:#4CAF50,color:#fff
style Sync fill:#2196F3,color:#fff
style Async fill:#FF9800,color:#fff
style etcd fill:#9C27B0,color:#fff
Patroni 配置
# /etc/patroni/patroni.yml
scope: payment-db-cluster
name: pg-node-1
restapi:
listen: 0.0.0.0:8008
connect_address: 10.0.1.10:8008
etcd3:
hosts:
- 10.0.2.10:2379
- 10.0.2.11:2379
- 10.0.2.12:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576 # 1MB:超过此复制延迟的备节点不参与选举
master_start_timeout: 300
synchronous_mode: true # 启用同步复制
synchronous_mode_strict: false # 没有同步备节点时允许降级为异步
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
max_connections: 500
shared_buffers: 8GB
wal_level: replica
max_wal_senders: 10
max_replication_slots: 10
hot_standby: "on"
wal_log_hints: "on"
synchronous_commit: "on" # 同步提交
synchronous_standby_names: "ANY 1 (*)" # 任意一个备节点确认即可
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- host replication replicator 10.0.1.0/24 md5
- host all all 10.0.1.0/24 md5
postgresql:
listen: 0.0.0.0:5432
connect_address: 10.0.1.10:5432
data_dir: /var/lib/postgresql/15/main
bin_dir: /usr/lib/postgresql/15/bin
authentication:
superuser:
username: postgres
password: "pg_super_pass"
replication:
username: replicator
password: "repl_pass"
callbacks:
on_start: /etc/patroni/callbacks/on_start.sh
on_stop: /etc/patroni/callbacks/on_stop.sh
on_role_change: /etc/patroni/callbacks/on_role_change.sh
tags:
nofailover: false
noloadbalancer: false
clonefrom: false回调脚本:角色变更时更新 VIP
#!/bin/bash
# /etc/patroni/callbacks/on_role_change.sh
ACTION=$1
ROLE=$2
CLUSTER=$3
LOG_FILE="/var/log/patroni/callbacks.log"
ELASTIC_IP="eipalloc-0123456789abcdef0"
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$ACTION] [$ROLE] $1" >> $LOG_FILE
}
case $ROLE in
master|primary)
log "本节点提升为 Primary,开始绑定 Elastic IP"
# 绑定 Elastic IP 到当前实例
aws ec2 associate-address \
--instance-id "$INSTANCE_ID" \
--allocation-id "$ELASTIC_IP" \
--allow-reassociation
if [ $? -eq 0 ]; then
log "Elastic IP 绑定成功"
else
log "Elastic IP 绑定失败"
exit 1
fi
# 通知监控系统
curl -s -X POST "https://alert.internal/api/events" \
-H "Content-Type: application/json" \
-d "{
\"event\": \"db_failover\",
\"cluster\": \"$CLUSTER\",
\"new_primary\": \"$INSTANCE_ID\",
\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
}"
;;
replica|standby)
log "本节点降级为 Replica"
;;
esacPatroni 集群管理操作
# 查看集群状态
patronictl -c /etc/patroni/patroni.yml list
# 输出示例:
# + Cluster: payment-db-cluster -----+---------+---------+----+-----------+
# | Member | Host | Role | State | TL | Lag in MB |
# +-----------+------------+---------+---------+----+-----------+
# | pg-node-1 | 10.0.1.10 | Leader | running | 5 | |
# | pg-node-2 | 10.0.1.11 | Sync | running | 5 | 0 |
# | pg-node-3 | 10.0.1.12 | Replica | running | 5 | 0.1 |
# +-----------+------------+---------+---------+----+-----------+
# 手动切换(Switchover,计划内)
patronictl -c /etc/patroni/patroni.yml switchover --master pg-node-1 --candidate pg-node-2 --force
# 紧急切换(Failover)
patronictl -c /etc/patroni/patroni.yml failover --candidate pg-node-2 --force
# 维护窗口暂停/恢复自动故障转移
patronictl -c /etc/patroni/patroni.yml pause
patronictl -c /etc/patroni/patroni.yml resume关键设计决策说明
为什么选择 Patroni 而不是手动搭建 Keepalived + PostgreSQL?
Patroni 把故障检测、Leader 选举、复制管理、VIP 切换整合在一起,避免了手动编排多个组件时的一致性问题。手动方案中,Keepalived 可能已经切换了 VIP,但 PostgreSQL 的 promote 还没完成——客户端连接到新的 VIP,但数据库还不能写入。Patroni 通过 DCS(分布式协调服务,如 etcd)统一管理状态,确保角色切换和 VIP 切换的顺序正确。
为什么使用 synchronous_mode 但不使用 synchronous_mode_strict?
synchronous_mode: true
确保写入获得同步备节点确认(RPO =
0)。synchronous_mode_strict: false
允许同步备节点不可用时降级为异步复制,在数据安全和可用性之间做权衡。
为什么 maximum_lag_on_failover 设置为 1MB? 该参数控制复制延迟超过多少的备节点不参与选举。1MB 确保只有数据足够新的节点能成为 Leader,在数据安全和可用性之间取得平衡。
实测切换指标
在该支付系统的实际测试中,Patroni 自动故障转移的各阶段耗时如下:
| 阶段 | 耗时 |
|---|---|
| 故障发生到 Patroni 检测 | 约 10 秒(loop_wait 配置) |
| etcd Leader 选举 | 1-2 秒 |
| PostgreSQL promote | 1-3 秒 |
| Elastic IP 重新绑定 | 2-5 秒 |
| PgBouncer 连接恢复 | 3-5 秒 |
| 端到端总切换时间 | 约 17-25 秒 |
这个切换时间意味着每次故障转移造成约 20 秒的写入不可用。按每月一次故障转移计算,月度停机约 20 秒,远低于 4 个 9(每月 4.38 分钟)的要求。
九、高可用模式选型对比
以下是对三种主要高可用模式的综合对比。选型是”哪个最适合当前场景”的问题。
全维度对比表
| 维度 | Active-Passive(主备) | Active-Active(双活) | N+1 冗余 |
|---|---|---|---|
| 架构复杂度 | 低 | 高 | 中 |
| 实施成本 | 中(需要备用资源) | 高(需要一致性机制) | 中低 |
| 资源利用率 | 50%(备节点闲置) | 80%-100% | N/(N+1),通常 >80% |
| 故障切换时间 | 5-30 秒 | <1 秒(摘除节点) | 5-30 秒 |
| 数据一致性保证 | 好(单写入点) | 复杂(需要冲突解决) | 好(单写入点切换) |
| 脑裂风险 | 有(需要 Fencing) | 有(需要分区容错) | 低(N 选 1 切换) |
| 写入性能影响 | 无(仅主节点写入) | 无(多节点写入) | 无 |
| 读性能 | 可扩展(备节点分担读) | 天然扩展 | 可扩展 |
| 运维复杂度 | 低 | 高 | 中 |
| 适用数据类型 | 强一致性数据(交易) | 最终一致性数据(会话) | 无状态或弱状态服务 |
| 典型技术实现 | MySQL 主从 + Keepalived | Redis Cluster、CockroachDB | K8s ReplicaSet、Nginx upstream |
| 推荐场景 | 数据库、核心交易 | 缓存、无状态服务、CDN | 应用服务器集群 |
选型决策流
选择高可用模式时,可以按以下顺序考虑:
第一步:确定可用性目标。 不同的可用性等级对应不同的架构投入。如果目标是 3 个 9(每年 8.77 小时停机),简单的 Active-Passive 加人工切换可能就够了。如果目标是 4 个 9 以上,必须自动化故障检测和切换。
第二步:确定数据一致性要求。 金融交易要求 RPO = 0,必须使用同步复制,Active-Active 实现难度大。用户会话或缓存允许最终一致性,Active-Active 更合适。
第三步:评估资源和成本约束。 Active-Passive 运维简单但资源利用率低;Active-Active 利用率高但人力成本高;N+1 在两者之间提供较好平衡。
第四步:考虑技术栈和团队能力。 熟悉 Pacemaker/Keepalived 的团队适合 Active-Passive;已使用 Kubernetes 的团队可以自然采用 N+1 模式。
混合模式
实际生产环境中,很少有系统只使用一种高可用模式。更常见的做法是按组件特性选择不同的模式:
- 数据库层: Active-Passive(Patroni/MySQL Group Replication),保证数据强一致性。
- 应用层: N+M 冗余(Kubernetes Deployment),无状态服务天然适合多副本。
- 缓存层: Active-Active(Redis Cluster),允许最终一致性,追求高性能。
- 消息队列: N+M 冗余(Kafka 多 Broker + 副本因子),数据通过分区副本保证持久性。
- 负载均衡层: Active-Passive(Keepalived + LVS/Nginx),VIP 漂移保证入口可用。
# Kubernetes Deployment 示例:应用层 N+M 冗余
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
namespace: production
spec:
replicas: 6 # N=5(承载峰值)+ M=1(冗余)
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
selector:
matchLabels:
app: payment-api
template:
spec:
affinity:
podAntiAffinity: # 反亲和:副本分散到不同节点
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: ["payment-api"]
topologyKey: "kubernetes.io/hostname"
containers:
- name: payment-api
image: registry.example.com/payment-api:v2.3.1
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 10
failureThreshold: 5配合
PodDisruptionBudget(minAvailable: 4)可以确保在节点维护或滚动更新期间,始终有足够的副本提供服务。
十、总结
高可用不是一个开关。它是一组设计模式的组合,每个模式解决一个特定问题:
冗余解决”故障后谁来接管”的问题。 Active-Passive 简单可靠,适合需要强一致性的场景;Active-Active 资源利用率高,适合允许最终一致性的场景;N+M 提供了灵活的成本和可用性平衡。
故障检测解决”什么时候切换”的问题。 心跳检测是基础,但仅靠心跳无法区分节点故障和网络分区。Quorum 投票和 Witness 节点提供了更可靠的故障判定机制。
Fencing 解决”切换时如何避免脑裂”的问题。 STONITH 通过物理隔离确保旧主不再运行;Quorum 通过少数派自杀确保只有一组节点提供服务。两者通常配合使用。
数据复制模式解决”切换后数据是否完整”的问题。 同步复制保证零数据丢失但增加延迟;异步复制性能好但有丢数据风险;半同步复制是常用的折中方案。
网络切换解决”客户端如何找到新主”的问题。 VIP 漂移速度快但限于同一子网;DNS 切换支持跨数据中心但受 TTL 影响。
回到本文开头的两个系统:它们的差别不在于是否做了冗余部署,而在于是否完整地考虑了从故障检测到仲裁到 Fencing 到数据一致性到网络切换的整条链路。高可用设计的难点从来不是”多部署一个节点”,而是”在故障发生的那几秒内,系统的每一个组件都知道自己应该做什么”。
下一篇将讨论弹性设计模式(Resilience Patterns)。高可用解决的是”节点挂了怎么办”,弹性设计解决的是”节点还没挂但已经扛不住了怎么办”:熔断、限流、降级、重试、舱壁隔离——这些模式如何在过载和部分故障情况下保护系统。
上一篇:【系统架构设计百科】弹性伸缩:自动扩缩容的策略与实现
下一篇:【系统架构设计百科】弹性设计模式:熔断、限流、降级与舱壁隔离
参考资料
书籍
- Michael T. Nygard, Release It! Design and Deploy Production-Ready Software, 2nd Edition, Pragmatic Bookshelf, 2018. 稳定性模式和反模式的权威参考,涵盖了断路器、舱壁、超时等模式,以及大量的真实生产事故案例分析。
- Brendan Burns, Designing Distributed Systems: Patterns and Paradigms for Scalable, Reliable Services, O’Reilly, 2018. 分布式系统设计模式,包含冗余、复制、Leader 选举等主题。
- Martin Kleppmann, Designing Data-Intensive Applications, O’Reilly, 2017. 分布式系统中数据复制、一致性、共识协议的深入讨论,第五章(Replication)和第八章(Trouble with Distributed Systems)与本文主题直接相关。
- Betsy Beyer, Chris Jones, Jennifer Petoff, Niall Richard Murphy, Site Reliability Engineering: How Google Runs Production Systems, O’Reilly, 2016. Google 的 SRE 实践,包含可用性目标设定、故障管理、容量规划等内容。
论文与标准
- Leslie Lamport, “The Part-Time Parliament”, ACM Transactions on Computer Systems, 16(2):133-169, May 1998. Paxos 协议的原始论文,是理解分布式共识和 Quorum 机制的基础。
- Diego Ongaro, John Ousterhout, “In Search of an Understandable Consensus Algorithm”, USENIX ATC, 2014. Raft 协议论文,比 Paxos 更易理解的共识算法,被 etcd、Consul 等广泛采用。
- RFC 5798, “Virtual Router Redundancy Protocol (VRRP) Version 3 for IPv4 and IPv6”, IETF, 2010. VRRP 协议的正式规范,VIP 漂移的协议基础。
博客与在线资源
- Patroni 官方文档, github.com/patroni/patroni. PostgreSQL 高可用管理工具的完整文档,包含配置参考、运维手册和故障处理指南。
- Keepalived 官方文档, keepalived.readthedocs.io. Keepalived 的配置参考和使用指南。
- Percona, “MySQL High Availability”, percona.com/blog. Percona 关于 MySQL 高可用方案的系列博客,覆盖了 Group Replication、Galera Cluster、Orchestrator 等方案的对比和实践。
- AWS, “Reliability Pillar - AWS Well-Architected Framework”, docs.aws.amazon.com. AWS 关于可靠性设计的最佳实践指南,包含故障隔离、自动恢复、多可用区部署等内容。
- Jepsen, “Analyses”, jepsen.io/analyses. Kyle Kingsbury 对多种分布式数据库的一致性和高可用性的严格测试报告,包括 PostgreSQL、MySQL Group Replication、etcd 等。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】容灾架构:多活与灾备设计
同城双活、异地多活、两地三中心——名词背后是完全不同的 RPO/RTO 和成本曲线。本文从容灾基础概念出发,拆解数据同步的五种拓扑、流量调度与 DNS 切换的工程细节,深入分析蚂蚁金服 LDC(逻辑数据中心)的多活架构,最后给出不同容灾等级的成本对比与选型建议。
【系统架构设计百科】架构质量属性:不只是"高可用高性能"
需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。
【系统架构设计百科】告警策略:如何避免"狼来了"
大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。
【系统架构设计百科】复杂性管理:架构的核心战场
系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略