- Published on
AWS RDS HA Deep Dive
- Authors

- Name
- Guming
基于早期架构分析
背景
AWS RDS-HA(高可用性)系统是一个分布式数据库复制解决方案,旨在通过多可用区部署策略,确保数据库服务的连续性和数据完整性。系统核心采用主备复制架构,结合智能监控和自动故障转移机制,为客户提供接近零数据丢失和最小服务中断的数据库服务
系统设计目标
- 多可用区复制支持
系统支持数据库实例在不同可用区的EC2实例上运行复制配置 每个实例独立挂载专用EBS卷,无共享存储依赖
- 数据持久性保障
一旦数据库写入操作提交完成,系统保证数据不会丢失 仅在极端黑天鹅事件(如多可用区同时故障)下可能出现数据不可用
- 高可用性承诺
单个副本故障不会导致长时间数据库服务中断 主副本故障时,系统能够自动切换到备用副本 故障转移过程对应用层透明
- 增强的持久性
相比非复制数据库方案,提供更高的数据持久性 有效防御可用区级别故障和单一EBS卷故障
Hight Level 架构设计
- 系统采用双副本架构,每个数据库至少运行两个副本实例:
主副本(Primary):处理所有读写请求的活动实例 备用副本(Secondary):接收同步复制数据的待命实例
- 每个副本实例具有以下特征:
运行在独立的EC2实例上 挂载专用的EBS卷(非共享存储) 部署在不同的可用区内 通过DRBD进行块级同步复制
┌─────────────────┐ ┌─────────────────┐
│ 可用区 A │ │ 可用区 B │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Primary │ │◄───────►│ │ Secondary │ │
│ │ Instance │ │ DRBD │ │ Instance │ │
│ └─────┬─────┘ │ Sync │ └───────────┘ │
│ │ │ │ │
│ ┌─────▼─────┐ │ │ ┌───────────┐ │
│ │ EBS │ │ │ │ EBS │ │
│ │ Volume │ │ │ │ Volume │ │
│ └───────────┘ │ │ └───────────┘ │
└─────────────────┘ └─────────────────┘
▲ ▲
│ │
└───────────┬───────────────┘
│
┌──────▼──────┐
│ Observer │
│ (可用区 C) │
└─────────────┘
Core components
DRBD
DRBD是一个内核级模块,实现了无共享架构的复制存储解决方案。其工作机制如下:
- 块级镜像:在服务器间镜像块设备内容
- 主从架构:采用主从复制模式,主副本接收所有更新
- 同步复制:所有写入操作被DRBD内核模块拦截并同步复制
- I/O拦截:在块设备层面拦截所有I/O请求,确保数据一致性
应用层 [MySQL数据库]
↓ 读写请求
文件系统层 [EXT4/XFS文件系统]
↓ 块I/O
DRBD层 [DRBD内核模块] ← 在这里拦截所有I/O
↓ ↓
↓ ↓ (通过网络复制)
↓ ↓
物理设备层 [本地EBS卷] [远程EBS卷]
1. MySQL执行写入:UPDATE users SET ...
↓
2. 文件系统将写入转换为块I/O操作
↓
3. DRBD内核模块拦截该I/O请求
↓
4. DRBD执行双写:
├─→ 写入本地磁盘(主节点)
└─→ 通过网络发送到远程节点
↓
5. 远程节点DRBD接收数据
↓
6. 远程节点写入本地磁盘(从节点)
↓
7. 远程节点返回确认
↓
8. 主节点DRBD收到确认
↓
9. 向MySQL返回写入成功
代际标识符(Generation Identifiers)
DRBD使用代际标识符(GI)机制来管理数据版本和同步状态
GI的作用:
- 验证节点是否属于同一复制对
- 确定后台重新同步的方向
- 判断是否需要完全重新同步或部分重新同步
识别脑裂场景
GI生成触发条件:
- 复制对首次初始化时
- 断开的备用副本切换为主副本时
- 主副本与备用副本断开连接时
GI工作示例:
假设复制对S1(主)和S2(备):
- 初始化阶段:S1生成初始GI(GI1)
- 网络分区:S1与S2断开,S1进入降级模式并生成新GI(GI2)
- 元数据存储:S1保存当前GI(GI2)和前一个GI(GI1)
- 恢复优化:网络恢复后,S1识别S2的GI为其前一个GI,仅传输增量变更
- 主故障切换:S1故障后,S2被提升为主副本时生成新GI,确保S1重新加入时能识别数据代际变化
Observer
Observer是故障检测和故障转移的编排组件,负责:
- 健康监控:定期检测主副本和备用副本的健康状态
- 故障检测:识别各种故障场景和网络分区情况
- 故障转移决策:根据预定义算法决定是否触发故障转移
- DNS管理:更新DNS指向以切换流量到新的主副本
监控机制
心跳检测:
- Observer每隔T_heartbeat秒向主副本和备用副本发送心跳
- 通过HTTP getStatus()命令查询实例状态
- 副本执行DRBD状态检查并返回当前状态(IN_SYNC、STALLED、DEGRADED或DEAD)
状态信息收集:
- 副本报告当前代际标识符(GI)
- Observer在内存中维护主副本和备用副本的GI
- 新Observer启动时从强一致性数据存储读取副本对列表
- 每次状态检查时验证GI是否变化并更新内存记录
客户端连接机制
DNS路由策略: 系统通过DNS机制实现客户端与当前主副本的连接
- 主副本和备用副本:各自拥有独立的外部DNS名称
- DNS_primary:客户端连接的统一入口,CNAME指向当前主副本的DNS名称
- 故障转移更新:主副本故障时,Observer更新DNS_primary的CNAME指向新主副本
流量切换流程:
- 正常状态:DNS_primary → Primary Instance DNS
- 故障切换:DNS_primary → Secondary Instance DNS (promoted to Primary)
读写处理模式
写操作流程:
- 所有写请求发送到主副本数据库
- 主副本接收写请求后,通过DRBD同步写入备用副本
- 两个副本都成功写入后,确认写操作成功
- 客户端收到写入成功响应
读操作流程:
- 所有读请求仅在主副本上执行
- 备用副本不对外提供读取服务
- 确保读取数据的一致性和准确性
副本状态模型
主副本状态
观察状态(Observation State)
- OBSERVED:实例与Observer保持连接
- NOT_OBSERVED:实例与Observer断开连接
数据同步状态(Data Synchronization State)
IN_SYNC(同步状态)
- 主副本和备用副本正常通信
- 所有写入操作在两个副本间同步执行
- 系统处于最佳运行状态
STALLED(停滞状态)
- 主副本与备用副本断开连接
- 无法继续执行任何I/O操作
- 等待Observer或网络恢复的过渡状态
DEGRADED(降级状态)
- 主副本与备用副本断开连接
- 切换到非复制模式继续服务
- 仅在主副本上执行读写操作
- 备用副本不可用或不可达时的临时运行模式
- 通常不会长期保持,RDS会创建新的备用副本
SUICIDAL(自杀状态)
- 主副本与备用副本断开且与Observer失联
- 只能从STALLED状态转换而来
- T_failover秒后主副本主动关闭或重启自身
- 避免脑裂场景的保护机制
主副本状态转换图
┌──────────────┐
│ IN_SYNC │
│ /OBSERVED │
└──┬───────┬───┘
│ │
S断开 │ │ 与O和S同时断开
│ │
┌───▼───┐ │
│STALLED│ │
└───┬───┘ │
│ │
听到O │ │ 未听到O
│ │
┌───▼───┐ ┌─▼────────┐
│DEGRADED│ │ SUICIDAL │
│/OBSERVED│ │/NOT_OBS │
└────────┘ └──────────┘
备用副本状态
Connected(连接状态)
- 备用副本健康运行
- 与主副本保持通信连接
- 正常接收复制数据
Disconnected(断开状态)
- 备用副本健康运行
- 与主副本失去通信连接
- 无法接收复制数据
Observer根据主副本和备用副本的综合状态,维护整个数据库系统的全局状态视图:
- ALL_OK:主副本和备用副本都正常运行且同步
- P_ONLY:仅主副本可达,备用副本不可用
- S_ONLY:仅备用副本可达,主副本不可用
- P_DEGRADED:主副本处于降级模式运行
- NEED_STANDBY_RECOVERY:需要恢复或重建备用副本
Failover 算法
故障转移触发条件 Observer在满足以下所有条件时触发故障转移: 条件一:主副本失联超时
Observer与主副本失去联系超过T_failover秒 T_failover = n × T_heartbeat + T_buffer n ≥ 2,T_buffer为主副本自我终止的最大预期时间
条件二:备用副本状态确认
备用副本处于DISCONNECTED状态 备用副本无法与主副本通信
条件三:最后已知状态验证
失去联系时主副本的最后状态为IN_SYNC 如果无法确定主副本状态(如Observer重启),则视为DEGRADED,不触发故障转移
故障转移执行流程
步骤1:发起提升请求
- Observer向备用副本的HostManager发送promoteToPrimary(oldGI)请求
- oldGI是主副本的最后已知代际标识符
步骤2:备用副本验证
a. 确认仍无法与主副本通信
b. 检查当前GI是否等于oldGI
c. 如果GI匹配:
- 执行DRBD命令提升为主副本
- 返回新GI给Observer
- 挂载文件系统并启动MySQL
d. 如果GI不匹配:
- 可能存在脑裂场景
- 中止提升操作
- 触发告警,等待人工介入
步骤3:DNS切换
- Observer成功提升备用副本后
- 更新DNS_primary的CNAME指向新主副本
- 客户端流量自动路由到新主副本
步骤4:恢复标记
- 标记实例状态为需要备用副本恢复
- 启动备用副本重建流程
分布式监控架构
Event Processor(EP)架构
RDS监控系统由多个Event Processor(EP)节点组成,每个EP承担一部分监控工作负载:
- EP节点:运行Observer软件组件的监控节点
- 分布式架构:多个EP协同工作,无单点故障
- 工作负载分片:基于哈希的分区算法分配监控任务
分区策略:
- 使用DBInstanceIdentifier作为哈希键
- 确保监控工作负载均匀分布
- 支持动态扩展和缩减EP节点
┌─────────────────────────────────────────────────┐
│ RDS Monitoring System │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ EP-1 │ │ EP-2 │ │ EP-N │ │
│ │ (AZ-A) │ │ (AZ-B) │ │ (AZ-C) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
└───────┼─────────────┼─────────────┼────────────┘
│ │ │
└─────────────┴─────────────┴───────────┐
│
┌────────────────────────────────────────▼──┐
│ Database Instances │
│ ┌──────────┐ ┌──────────┐ │
│ │ DB1(P-S) │ │ DB2(P-S) │ ... │
│ │ AZ1-AZ2 │ │ AZ2-AZ3 │ │
│ └──────────┘ └──────────┘ │
└──────────────────────────────────────────┘
EP工作负载分区
设计原则
线性可扩展性:
- 监控系统随数据库实例数量线性扩展
- 通过简单添加EP节点实现扩展
- 无需修改现有架构
EP位置约束:
- EP必须部署在与其监控的数据库副本不同的可用区
- 防止可用区故障导致数据库和Observer同时失效
- 避免出现不可恢复的状态
EP故障处理:
- 所有数据库实例持续被监控
- EP故障时,其监控的数据库实例重新分配给其他EP
- 工作负载自动重新平衡
分区算法实现
EP分组策略:
- 按可用区将EP节点分组
- 每组EP仅监控副本不在该可用区的数据库
示例场景:
假设4个可用区,4个EP组:
G1 (AZ1的EP) → 监控副本在 (AZ2, AZ3), (AZ2, AZ4), (AZ3, AZ4) 的数据库
G2 (AZ2的EP) → 监控副本在 (AZ1, AZ3), (AZ1, AZ4), (AZ3, AZ4) 的数据库
G3 (AZ3的EP) → 监控副本在 (AZ1, AZ2), (AZ1, AZ4), (AZ2, AZ4) 的数据库
G4 (AZ4的EP) → 监控副本在 (AZ1, AZ2), (AZ1, AZ3), (AZ2, AZ3) 的数据库
分配算法
1. EP1启动,发现1000个数据库需要监控
2. EP1发现4个EP覆盖该可用区对
3. 按EP标识符字典序排序
4. 均匀分配:
- EP1: DB1 - DB250
- EP2: DB251 - DB500
- EP3: DB501 - DB750
- EP4: DB751 - DB1000
EP故障检测与恢复
EP间心跳机制
心跳协议:
- 每个EP定期向所有其他EP发送心跳消息(HTTP)
- 心跳间隔:10秒
- 传输EP的状态信息和监控负载
EP状态管理:
- 每个EP维护其他EP的状态列表
- 状态包括:AVAILABLE或DEAD
- 记录最后心跳检入时间
故障判定:
if (当前时间 - 最后心跳时间) > heartbeat_failure_time:
标记EP为DEAD
重新调整监控工作负载
heartbeat_failure_time = 6 × 心跳间隔
EP启动与恢复流程
BOOTSTRAP模式:
- EP启动进入BOOTSTRAP模式
- 持续时间:heartbeat_failure_time
- 操作:
- 接收其他EP的心跳
- 启动自己的心跳代理
- 不承担监控工作负载
OPERATIONAL模式:
- BOOTSTRAP期满后进入OPERATIONAL模式
- 基于当前EP状态确定监控负载
- 开始监控分配的数据库实例
多EP监控的一致性保证
潜在问题:
- 网络分区可能导致多个EP同时监控同一数据库
- 可能造成多个EP尝试故障转移
解决方案:租约机制
EP租约机制
确保在任何时刻,只有一个EP有权对特定数据库实例执行故障转移操作,防止脑裂场景。
租约获取流程
租约授予条件:
EP向数据库副本发送status ping时,主副本的HostManager仅在以下条件全部满足时授予租约:
- 该副本是DRBD主副本
- 副本处于同步状态
- 当前没有授予其他EP的有效租约
Lease 持久化
1. 主副本将租约信息写入DRBD磁盘:
- 租约到期时间
- EP标识符
2. 由于DRBD同步机制,备用副本自动收到租约信息
3. 写入成功(两个副本都完成)后,主副本向EP返回租约
租约参数:
- 租约有效期:T_lease
- 仅在租约期内EP有权执行故障转移
防止脑裂:
场景:网络分区导致两个EP分别可达主备副本
EP1: 有租约,可达主副本
EP2: 无租约,可达备用副本
结果:
- EP2无法触发故障转移(无租约)
- 只有EP1能执行故障转移操作
- 避免双主副本出现
一致性保证:
- 租约信息通过DRBD同步,保证主备一致
- 租约过期机制防止僵尸EP持续持有权限
- 基于时间的租约自动失效机制
可用区故障处理
初始配置:
- G1 (AZ1) → 监控 (AZ2, AZ3) 的数据库
- G2 (AZ2) → 监控 (AZ3, AZ1) 的数据库
- G3 (AZ3) → 监控 (AZ1, AZ2) 的数据库
AZ1故障时的问题:
- 需要调整EP部署策略
secondary-az-colocation-override flag = true
重新配置:
- EP组监控主副本不在该AZ的数据库
- 允许EP与备用副本在同一AZ
- 容忍包含主副本的AZ故障
示例重配置:
G2 (AZ2) → 监控主副本在AZ3的数据库(备用副本在AZ2)
G3 (AZ3) → 监控主副本在AZ2的数据库(备用副本在AZ3)
监控数据库分区
分区需求
监控数据库(Monitoring DB)会持久化与客户数据库及其安全组相关的事件。虽然我们可能会将这些事件保留更长时间,但计划仅向客户展示过去 14 天 的事件。随着数据库数量和客户数量的持续增长,单个数据库的存储空间可能不足
需要分区的数据:
- 数据库实例列表(db_poll_list)
- 客户事件记录(db_events表)
- 安全组事件(security group events表)
设计目标:
- 支持水平扩展
- 超越单一物理系统限制
- 提前规划架构以支持跨多数据库系统扩展
分区键选择
分区键:customer_id(客户标识符)
选择理由:
事件的广泛性
- 某些事件不限于单个数据库
- 安全组变更影响多个数据库
- 客户级别事件需要统一存储
可扩展性假设
- 单个客户的事件不会超过单数据库存储容量
- 事件仅保留14天
- 合理的数据增长预期
桶分区(Bucket Partitioning)
桶分区概念:
在数据和物理分区之间增加抽象层:
客户数据 → 哈希 → 桶(Bucket) → 映射 → 物理分区
系统设置:
桶数量
- 初始配置:65,000个桶
- 桶数量在系统生命周期内固定
- 足够大以确保负载均匀分布
数据到桶的映射
- 使用一致性哈希算法
- 客户标识符 → 桶编号
- 客户永久关联到特定桶
桶到分区的映射
- 使用范围映射,方便配置
- 多个桶映射到一个物理分区
初始配置(2个分区):
分区1 = {桶1 - 桶25000}
分区2 = {桶25001 - 桶50000}
示例:
客户A的哈希 → 桶100
客户A的所有数据存储在分区1
动态重分区
扩展场景:添加新分区以分担负载
初始状态:
分区1 = {1-25000}
分区2 = {25001-50000}
添加分区3后:
分区1 = {1-16666}
分区2 = {33333-50000}
分区3 = {16667-33333}
重分配结果:
从分区1迁移桶16667-25000到分区3(8334个桶)
从分区2迁移桶25001-33333到分区3(8333个桶)
实现负载的公平重分配
数据迁移问题:
重分区后,同一桶的数据可能分散在不同物理分区
解决方案:双分区映射
每个桶可以关联两个分区:
- 当前分区:新数据写入位置
- 历史分区:旧数据存储位置
写入新数据进入新分区
查询处理:
查询桶11000的事件:
1. 首先查询当前分区(分区3)
2. 如果需要,查询历史分区(分区1)
3. 合并两个分区的结果
性能考虑:
- 优先查询当前分区(新数据为主)
- 历史分区查询可能未命中
- 由于仅保留14天数据,历史数据量可控
副本故障恢复流程
主副本重启
重启后状态变化:
重启前:DRBD状态为 primary/secondary
重启后:DRBD状态变为 secondary/secondary
恢复流程:
主副本重启后行为
- HostManager检测到DRBD状态变化
- 主副本以secondary身份上线
- 不自动提升为主副本
Observer决策
- 检测到两个副本都是secondary状态
- 确定哪个副本应为主副本
- 向选定的副本发送promoteToPrimary()请求
状态恢复
- 选定副本提升为主副本
- 另一副本保持备用副本角色
- 恢复正常的主备复制
与故障转移的区别:
- 如果Observer尚未执行故障转移,原主副本可能被重新提升
- 如果Observer已完成故障转移,原主副本作为新的备用副本
备用副本重启
Observer行为:
- 检测到备用副本离线
- 标记实例需要恢复
- 启动备用副本恢复工作流
快速恢复路径:
if 备用副本在短时间内重新上线:
恢复工作流检测到备用副本已返回
请求备用副本HostManager重新连接到主副本
避免创建新备用副本的开销
else:
执行完整的备用副本重建流程
优化效果:
- 简单重启场景快速恢复
- 减少不必要的资源消耗
- 缩短恢复时间窗口
副本实例故障
主副本故障恢复
故障检测与初步恢复:
- Observer检测到主副本故障
- 提升备用副本为新主副本
- 更新DNS_primary指向新主副本
- 在Admin DB中标记状态: PENDING/DEGRADED_NEED_SECONDARY_RECOVERY
- 触发恢复扫描器启动恢复工作流
恢复工作流决策树:
恢复工作流检查旧主副本状态:
if 旧主副本已作为secondary上线:
// 可能重启时间较长导致被标记为死亡
将旧主副本连接到新主副本
等待副本完全同步
标记数据库状态为 OK
恢复完成
else if 旧主副本完全不可达:
终止旧实例
使用创建复制实例的标准流程创建新备用副本:
1. 启动新EC2实例
2. 配置EBS卷
3. 从新主副本同步数据
4. 建立DRBD复制
恢复完成
- 优先尝试重用旧实例
- 仅在必要时创建新实例
- 确保最终恢复到双副本状态
备用副本故障恢复
故障检测:
- Observer检测到备用副本故障
- 标记Admin DB状态: PENDING/DEGRADED_NEED_SECONDARY_RECOVERY
- 触发恢复工作流
恢复流程:
与主副本故障类似,但更简单:
- 主副本保持正常运行
- 主副本进入DEGRADED模式
- 恢复工作流:
- 检查旧备用副本是否可恢复
- 如可恢复:重新建立复制连接
- 如不可恢复:创建新备用副本
- 完成后恢复IN_SYNC状态
优先级:
- 主副本故障优先级更高(影响服务)
- 备用副本恢复可以后台进行
- 两种故障都需要最终恢复双副本状态
数据库进程崩溃
自动恢复机制:
1. 数据库进程(MySQL)因任何原因崩溃
2. 主副本的HostManager作为守护进程监控数据库状态
3. 检测到数据库进程死亡
4. 自动重启数据库进程
5. 恢复服务
HostManager的职责:
- 持续监控数据库进程健康状态
- 进程崩溃时自动重启
- 记录崩溃事件和恢复操作
- 无需Observer介入
优势:
- 快速本地恢复
- 减少服务中断时间
- 进程级故障无需实例级故障转移
- 降低恢复复杂度
设计要点总结
可靠性保障
数据持久性:
- 同步复制确保数据写入双副本后才确认成功
- DRBD块级复制提供强一致性保证
- 代际标识符防止数据不一致
故障隔离:
- 跨可用区部署避免单点故障
- Observer部署在独立可用区
- 故障域隔离设计
自愈能力:
- 自动故障检测
- 自动故障转移
- 自动副本恢复
性能与可扩展性
监控系统扩展:
- EP节点水平扩展
- 基于哈希的负载均衡
- 监控数据库分区支持
故障转移速度:
- T_failover参数可调优
- 预防性超时机制
- DNS快速切换
资源利用:
- 仅主副本处理客户请求
- 备用副本专注于复制
- 合理的资源分配策略
安全性设计
防止脑裂:
- 主副本SUICIDAL机制
- EP租约系统
- GI验证机制
一致性保证:
- 同步复制
- 代际跟踪
- 状态机严格转换
最小化数据丢失:
- 仅在IN_SYNC状态下故障转移
- 严格的故障转移前置条件
- 不确定状态下保守处理