惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
S
Schneier on Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Scott Helme
Scott Helme
V
Vulnerabilities – Threatpost
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
Latest news
Latest news
Google Online Security Blog
Google Online Security Blog
Google DeepMind News
Google DeepMind News
K
Kaspersky official blog
Forbes - Security
Forbes - Security
T
Tenable Blog
The Last Watchdog
The Last Watchdog
T
Tor Project blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Simon Willison's Weblog
Simon Willison's Weblog
Project Zero
Project Zero
O
OpenAI News
L
LINUX DO - 热门话题
P
Privacy International News Feed
月光博客
月光博客
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
C
Cyber Attacks, Cyber Crime and Cyber Security
量子位
博客园 - 【当耐特】
罗磊的独立博客
T
Threatpost
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Cisco Blogs
S
SegmentFault 最新的问题
博客园 - 三生石上(FineUI控件)
N
News | PayPal Newsroom
腾讯CDC
Security Latest
Security Latest
J
Java Code Geeks
L
LINUX DO - 最新话题
N
Netflix TechBlog - Medium
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
The Exploit Database - CXSecurity.com
L
Lohrmann on Cybersecurity
D
Docker
Spread Privacy
Spread Privacy
S
Security @ Cisco Blogs
A
Arctic Wolf
H
Hacker News: Front Page
Help Net Security
Help Net Security
Recorded Future
Recorded Future
V2EX - 技术
V2EX - 技术

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 【Linux 网络子系统深度拆解】内核网络调优方法论:从基准测试到生产验证 排序基准测试:用数据说话
【系统架构设计】高可用设计模式:冗余、故障转移与仲裁
Liao Tonglang · 2026-04-13 · via 土法炼钢兴趣小组的算法知识备份

一个支付系统的数据库主节点在凌晨 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

工作机制:

  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-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。

工作机制:

  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 BACKUPpriority 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 版本中是永久的,需要通过 JVM 参数 -Dsun.net.inetaddr.ttl=30java.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"
        ;;
esac

Patroni 集群管理操作

# 查看集群状态
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 等。

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计】容灾架构:多活与灾备设计

同城双活、异地多活、两地三中心——名词背后是完全不同的 RPO/RTO 和成本曲线。本文从容灾基础概念出发,拆解数据同步的五种拓扑、流量调度与 DNS 切换的工程细节,深入分析蚂蚁金服 LDC(逻辑数据中心)的多活架构,最后给出不同容灾等级的成本对比与选型建议。

2026-04-13 · architecture

【系统架构设计】什么是软件架构:从代码结构到系统决策

软件架构的定义至少有三种主流理解,每种指向不同的关注点。本文对比 Booch、Fowler、Johnson 三家定义,分析架构决策的不可逆性特征,拆解 Krutchten 4+1 视图模型,回答一个核心问题:架构和设计的边界到底在哪里。

2026-04-13 · architecture

【系统架构设计】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计】架构决策与 ADR:如何做出可追溯的技术决策

口头约定的架构决策会在人员流动中丢失,会在争论中反复翻车。ADR(Architecture Decision Records)用一种轻量的文档格式,把每一个关键技术决策的背景、选项、理由和代价写下来,跟着代码一起版本管理。本文从 ADR 的三种主流格式讲到 Git 仓库中的实操管理,再拆解 Spotify 和 Uber 的工业实践。