DARTS#02 | 从共识算法到存算分离:深度拆解云原生数据库的稳定性基石

第一章:权力的游戏------共识协议的演进

Raft:基于网络消息的"民主议会"

在分布式系统的世界里,最难的问题不是如何增加机器,而是如何让成百上千台机器像"单机"一样行动。Raft 协议的出现,为这个问题提供了一个优雅且易于理解的解法。


1. 核心逻辑:在不确定性中寻找确定性

分布式系统的本质是:如何在不可靠的硬件上实现可靠的软件。

在分布式环境下,网络会延迟、数据包会丢失、服务器会宕机。Raft 的核心任务就是确保即便部分节点"罢工",整个系统对外表现出来的数据依然是强一致的。

2. 痛点:为什么不能简单地用单机?

  • 单机模式的死穴:单点故障 (SPOF)。如果你的数据库只跑在一台机器上,一旦硬件损坏,服务直接瘫痪,数据可能永久丢失。

  • 多机模式的噩梦:一致性挑战。为了高可用,我们部署多台机器。但问题随之而来:

    • 如果客户端向 A 写入 A=1,向 B 写入 A=2,最终听谁的?

    • 如果网络断开,一部分机器认为 A=1,另一部分认为 A=2,系统就会出现"人格分裂"。

Raft 的作用,就是让这群散兵游勇通过一套规则,选举出一个"司令官",并保证所有士兵的笔记本(日志)记录得一模一样。


3. 算法拆解

Raft 将复杂的共识问题拆解为三个子问题:领导者选举、日志复制和安全性。

A. 领导者选举 (Leader Election):权力的游戏

在 Raft 中,节点有三种身份:Follower(跟随者)、Candidate(候选人)和Leader(领导者)。

  1. 心跳 (Heartbeat) 与超时:

    • Leader 会周期性地向所有 Follower 发送"心跳",宣告:"我还活着,不要反抗"。

    • 如果 Follower 在一段时间内没收到心跳,它就会认为 Leader 驾崩了,于是自荐为 Candidate,发起投票。

  2. 任期 (Term):

    • Raft 把时间分为一段段的"任期",就像大选年份。每届任期最多只有一个 Leader。这防止了旧时代的 Leader 诈尸。
  3. 为什么是奇数个节点?(脑裂预防):

    • 脑裂 (Split-brain):指网络分区将集群切成两半,两边都选出了自己的 Leader。

    • 法定人数 (Quorum):Raft 要求必须获得超过半数 (N/2 + 1) 的选票才能当选。如果你有 3 个节点,必须拿 2 票;如果你有 5 个节点,必须拿 3 票。

    • 结论:在 3 节点集群中,即使网络断开,最多只能有一边凑够 2 票。奇数节点能在保证容错的同时,最大程度利用资源并避免平票。

B. 日志复制 (Log Replication):步调一致

一旦 Leader 选出,所有的写操作都必须经过它。

  1. 提交过程:

    • Leader 接收到客户端指令,先写进自己的日志,但不立即生效。

    • Leader 将该日志分发给所有 Follower。

    • 大多数确认 (Quorum):一旦 Leader 收到超过半数节点的"已写入"确认,Leader 才会正式"提交 (Commit)"这条记录,并告诉 Follower 们也一起提交。

  2. 强制一致:

    • 如果某个 Follower 的日志跟 Leader 不一致(可能因为之前的网络故障),Leader 会采取强硬手段:覆盖掉 Follower 冲突的部分,强制其与自己同步。

4. 深度洞察:Raft 的局限与进阶

尽管 Raft 以易理解著称,但在极端工业场景下,它也有软肋:

局限性:读操作的陷阱
  • 过期的 Leader:假设 Leader A 被网络隔离了,其他节点选出了新 Leader B。但在 A 意识到自己被罢免前,如果客户端去 A 那里读数据,A 可能会返回旧的(过期的)数据。

  • 解决方案:Read Index。为了实现"线性一致性读",当 Leader 收到读请求时,它不能直接返回结果,而是要先去问一下其他节点:"大家看,我现在还是不是 Leader?"(通过一轮心跳确认)。只有确认自己依然握有大权,才返回数据。

性能瓶颈
  • 因为所有压力都在 Leader 身上,且每一条日志都要经过网络往返(Round Trip),在高并发场景下,Leader 的网络 I/O 会成为系统瓶颈。

  • 这就是为什么像 TiDB(使用 Raft 的国产数据库)会采用 Multi-Raft 架构------将数据分成很多小份(Region),每一份都有自己的 Raft 组,把压力分散到多台机器上。

总结

Raft 协议的精髓在于:通过"大多数"的共识,屏蔽了"少数派"的不可靠。 它的设计哲学不仅仅是数学上的严谨,更像是一套人类社会的民主运作机制------有任期、有竞选、有少数服从多数,最终在混乱的分布式世界中,建立起稳固的信任基石。

VDS:基于共享存储的"圣杯挑战"。

Raft 解决了分布式环境下'靠消息传递'达成一致的问题。但在云原生时代,我们拥有了'共享存储'这一物理挂载优势,共识机制是否能从'民主投票'进化为'夺旗竞速'?这就是 PolarDB VDS 带来的存储层共识。它不再依赖议员们口头传达指令,而是通过共享存储上的'原子金杯'(CAS Block)来决定王权的归属。这种从'网络共识'向'存储共识'的跃迁,正是数据库实现秒级高可用的关键。


1. 核心共同点:解决"权力"与"共识"问题

无论是 Raft 还是 VDS,它们的核心目标是一致的:确保集群中永远只有一个合法的 Leader(写节点),并防止"脑裂"。

  • 角色分配:

    • Raft 有 Leader, Follower, Candidate。

    • VDS 也有 Leader(主节点)、Follower(热备节点)和 Observer(只读节点)。

  • 任期/租约制: 两者都利用时间来管理领导权。Raft 用的是 Term(任期);VDS 用的是 Lease(租约)。如果 Leader 在规定时间内没有"续命",权力就会被收回。

  • 故障自动触发: 当 Follower 监测到 Leader 异常(心跳超时或租约到期),都会自动触发选主流程,不需要人工干预。


2. 本质区别:通信媒介的不同(网络 vs. 存储)

这是两者最根本的区别。Raft 是"基于消息传递"的共识,而 VDS 是"基于共享状态"的共识。

|------|----------------------------------|----------------------------------------|
| 维度 | Raft 协议 | VDS 技术 (PolarDB) |
| 信使 | 网络 (Network)。节点之间通过发消息(RPC)来投票。 | 存储 (Storage)。节点通过读写共享存储上的特定数据块来通信。 |
| 选主依据 | 少数服从多数 (Quorum)。必须得到集群超过半数节点的选票。 | 原子锁 (CAS)。看谁能先在共享存储的 CAS Block 上抢到锁。 |
| 仲裁者 | 集群中的其他节点是仲裁者。 | 底层的共享存储 (PolarStore) 是仲裁者。 |
| 数据同步 | 必须通过网络将日志流(Log Stream)发送给多个节点。 | 物理日志直接写到共享存储,从库直接从存储读,不依赖主库发送。 |


3. VDS 的核心黑科技:CAS Block 与 PCR

在文章中,VDS 引入了两个 Raft 中没有的概念,这是针对共享存储优化的:

  • CAS Block (原子数据块):

    • Raft 的选主像是一场"拉票大会",大家互发传单(传消息),效率受网络波动影响。

    • VDS 的选主像是一场"抢旗比赛"。存储层提供了一个支持 CAS(Compare-And-Swap)操作的内存块。谁先成功把自己的 ID 写进去,谁就是 Leader。这种硬件级别或存储引擎级别的原子操作,比网络投票要快得多。

  • PCR (Polar Cluster Registry): 这是一个维护集群拓扑的"公告栏"。新 Leader 诞生后,直接修改 PCR,所有 Observer(只读节点)观察到 PCR 变化,自动连接新 Leader。这消除了 Raft 改变配置时复杂的成员变更协议。


4. 为什么 PolarDB 要用 VDS 而不是原生 Raft 做选主?

虽然 PolarDB 底部存储(PolarFS)内部用了 ParallelRaft 来保证三副本数据一致性,但在计算层(计算节点切换)使用 VDS 有三个明显优势:

  1. 更快的故障发现: Raft 的心跳如果设得太短(比如 <100ms),极易受网络抖动影响导致误选。VDS 基于磁盘租约,只要存储链路没断,判断就非常稳定且极快。

  2. 无视节点数量限制: Raft 通常需要 3 或 5 个节点(奇数),因为要凑够半数。而 VDS 哪怕只有 1 个主和 1 个备(2 节点),也可以通过抢占共享存储的锁来选主,不会因为只有 2 个节点无法达成"过半数"而卡死。

  3. 确定性: Raft 选主可能平票(Split Vote),需要随机退避重试。VDS 抢锁是确定性的,第一名抢到就是抢到了。

共识的本质,是在不可靠的网络上构建可靠的权力交接。

第二章:疏通神经------连接池打满的攻守道

即便 VDS 实现了秒级选主,保证了'大脑'不宕机,但如果业务请求的'血管'塞住了,系统依然会瘫痪。这就是我们要讨论的------连接池打满。

在分布式数据库中,选主再快,如果连接被事务拖垮,业务依然会感知到不可用。

连接池打满(Connection Pool Exhaustion)是分布式系统和高并发应用中常见的"性能杀手"。当客户端请求无法从池中获取可用连接时,会导致应用响应变慢、超时甚至崩溃。

攻(业务侧):开发者自律------事务剥离与瘦身

介绍一个针对该问题的核心优化技巧:"事务剥离与外部调用瘦身" (Transaction Slimming & External Call Stripping)。


核心技巧:事务剥离与外部调用瘦身

1. 现象描述

很多Java开发者习惯在 Service 层方法上直接加 @Transactional 注解。如果这个方法内部包含了非数据库操作(如调用第三方 HTTP 接口、发送邮件、复杂的本地计算或文件 IO),连接池就会被迅速占满。

原因: 连接池中的连接是在事务开始时被获取的,并且只有在事务提交或回滚后才会释放。如果事务中包含了一个耗时 2 秒的外部 API 调用,那么这个数据库连接就会被白白占用 2 秒,而实际上它在这 2 秒内并没有执行任何 SQL。

2. 优化方案:缩小事务边界

原则:仅在真正需要数据库操作的代码块上开启事务,将所有非数据库耗时操作移出事务范围。

错误示范 (连接长时间占用):
复制代码
@Transactional
public void processOrder(Order order) {
    // 1. 数据库操作:保存订单 (获取连接)
    orderRepo.save(order); 
    
    // 2. 外部调用:调用远程支付网关 (耗时 2s,此时连接一直被占用)
    boolean success = paymentService.callRemoteApi(order); 
    
    // 3. 数据库操作:更新状态
    if(success) {
        orderRepo.updateStatus(order.getId(), "PAID");
    }
} // 事务结束,释放连接
优化方案 (快速释放连接):
复制代码
public void processOrder(Order order) {
    // 1. 外部调用:先做耗时且不依赖事务的操作
    boolean success = paymentService.callRemoteApi(order);

    // 2. 事务操作:仅包裹必要的数据库逻辑
    if(success) {
        transactionTemplate.execute(status -> {
            orderRepo.save(order);
            orderRepo.updateStatus(order.getId(), "PAID");
            return null;
        });
    }
} // 连接仅在 transactionTemplate 内部被占用,耗时可能仅需 10ms
3. 设置合理的连接泄漏检测 (Leak Detection)

有时候连接池满是因为代码里拿了连接没还(连接泄漏)。

  • 技巧: 以 Java 的 HikariCP 为例,设置 leakDetectionThreshold(如 2000ms)。

  • 效果: 如果一个连接被借出超过 2 秒还没归还,连接池会打印出一条包含堆栈信息的警告日志,帮你快速定位是哪行代码没关连接。

4. 引入"快速失败"与"请求排队"保护

不要让客户端无限期等待连接。

  • 技巧: 设置合理的 connectionTimeout(连接获取超时,建议 2-5 秒)。

  • 效果: 如果池子满了,与其让请求挂起导致整站瘫痪,不如快速返回"系统繁忙"错误。这能保护上游服务,防止雪崩。

5. 读写分离与多池化
  • 技巧: 针对读多写少的场景,配置两个独立的连接池。

  • 效果: 复杂的报表查询(慢 SQL)放在"读池"中,核心的业务写入放在"写池"中。这样即便读操作把池子占满了,也不会影响用户下单等关键写入操作。

6. 参数调优:并非越大越好
  • 技巧: 遵循 PostgreSQL 的建议公式:connections = ((core_count * 2) + effective_spindle_count)

  • 原则: 过大的连接池会导致频繁的 CPU 上下文切换。通常一个 8 核的数据库服务器,连接池设为 20-50 往往比设为 500 性能更好。

守(架构侧):平台侧赋能------透明的连接复用。

业务侧优化技巧有一定的用户使用门槛,因此云厂商为了降低使用门槛,往往也会针对这一问题进行一些架构上的优化,进而用户可以减少这类问题带来的业务影响,例如PolarDB的连接池技术。它通过将成千上万个应用会话映射到少量的物理连接上,彻底解决了高并发下的连接爆炸问题,让数据库能够专注于计算本身,而不是被连接管理拖垮

一、两种连接池类型

1. 会话级连接池
  • 适用场景:纯短连接业务,频繁建立/断开连接

  • 工作原理:

    • 连接断开时,系统判断是否为闲置连接,若是则保留在池中短暂时间

    • 新连接建立时,根据 userclientipdbname 等条件匹配复用池中连接

    • 减少与数据库的建连开销,降低 MySQL 主线程负载

  • 局限性:

    • ❌ 不能减少数据库的并发连接数(仅降低建连速率)

    • ❌ 不能解决慢 SQL 导致的连接堆积问题

2. 事务级连接池
  • 适用场景:连接数需求大(如上万)或 Serverless 服务(连接数随业务扩容线性增长)

  • 工作原理:

    • 客户端与代理间可存在上千连接,但代理与后端数据库仅维持几十/几百个连接

    • 事务结束后连接自动归还连接池供复用

    • 显著降低后端数据库的实际连接压力

  • 关键限制(触发以下操作会锁定连接,不再复用):

    • 执行 PREPARE 语句、创建临时表、修改用户变量

    • 大报文(≥16 MB)、LOCK TABLE、多语句、存储过程调用

    • FOUND_ROWS()/ROW_COUNT()/LAST_INSERT_ID() 函数结果可能不准确

    • sql_mode 等 4 个变量外,其他会话级系统变量需客户端显式 SET

    • connection_id() 可能变化,SHOW PROCESSLIST 显示的 IP/端口可能与实际不符

总结

解决连接池打满的最佳实践是"快进快出":

  1. 缩短事务: 绝对不要在事务里写远程调用。

  2. 优化 SQL: 解决慢查询,让连接处理变快。

  3. 精确监控: 监控 ActiveConnections(活跃连接数)和 PendingThreads(等待线程数)的比例。

  4. 云原生数据库透明连接池功能:它能优化业务侧的连接池管理不当问题,显著降低使用门槛。

最好的连接池优化,不是扩容,而是让连接'用完即走'。

第三章:架构的终局------从存算分离到全解耦

Aurora 论文:开启"日志即数据库"的 1.0 时代

《Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases》

这篇发表于 SIGMOD 2017 的论文是数据库领域的"分水岭"之作。它不仅成就了 AWS 最赚钱的数据库业务,更定义了"云原生数据库(Cloud-Native Database)"的标准范式。

如果说传统的数据库是"搬运砖块(数据页)",那么 Aurora 的核心理念就是 "只传信件(日志)"。以下是针对这篇论文的深度解析。


1. 核心观点:网络是瓶颈 (The Network is the Bottleneck)

论文指出,在现代分布式云环境下,数据库的瓶颈已经从传统的 CPU 和存储 I/O 转移到了网络带宽上。传统数据库(如 MySQL)在向云端存储写入时,会发送大量重复数据(如数据页、Redo Log、Binlog、双写缓冲等),导致严重的写放大,限制了吞吐量。

2. 核心架构:日志即数据库 (The Log is the Database)

这是 Aurora 最著名的创新。其核心思路是"存算分离":

  • 计算层 (Database Tier): 剥离存储功能,保留 SQL 解析、事务管理和缓存。它不再向存储层刷入"数据页",而是只发送"Redo Log"。

  • 存储层 (Storage Tier): 一个独立的、多租户的、专门设计的存储服务。它不仅存储日志,还具备"智能":它在后台异步地将收到的 Redo Log 合并到数据页中(物化),从而生成最新的数据页。

  • 结果: 网络 I/O 大幅减少(减少了约 7-8 倍),吞吐量提升至 MySQL 的 5 倍。

3. 高可用与容错机制 (Durability at Scale)

Aurora 采用了一套严苛的复制策略来保证在云环境下的高可靠性:

  • 6 副本/3 可用区 (AZ): 数据被复制 6 份,分布在 3 个不同的可用区(每个区 2 份)。

  • Quorum 机制: 采用 4/6 写、3/6 读的法定人数协议。这意味着即便丢失一个整个可用区(2 份数据)再外加一个节点(共 3 份),依然能保证数据不丢且可读。

  • 分段存储 (Segmentation): 将整个数据库卷切成一个个 10GB 的小块(Segments),这样在某个节点坏掉时,可以利用多节点并发并行修复,大幅缩短 MTTR(平均修复时间)。

4. 异步共识与快速恢复 (Fast Recovery)

  • 无 2PC 依赖: Aurora 避免了昂贵的分布式两阶段提交(2PC)协议,而是利用 LSN(日志序列号)和异步确认来推进一致性位点。

  • 秒级恢复: 传统数据库重启时需要重放大量的 Redo Log(这个过程非常慢)。在 Aurora 中,存储层一直在后台重放日志,因此计算节点重启后几乎可以"瞬间"恢复服务,无需再次重放。

  • 只读副本: 主库和从库共享同一份存储,从库通过异步接收主库发来的日志流来更新缓存,延迟通常在 20ms 以内。

5. 论文总结的三个贡献

  1. 大规模可靠性设计: 证明了如何在云端通过 Quorum 和分段设计实现容错。

  2. 智能存储下推: 演示了将数据库底层的日志处理下推到存储层的巨大优势。

  3. 消除同步点: 通过异步处理消除了分布式系统中的性能抖动和检查点(Checkpoint)带来的开销。

结论

Amazon Aurora 通过"存算分离"和"日志即数据库"的设计,彻底改变了数据库与云基础设施交互的方式。它证明了通过将"重活"丢给智能存储层,可以极大提升关系型数据库的性能、可扩展性和韧性。

PolarDB Serverless 论文:内存池化,进入全分离的 2.0 时代

如果说 Amazon Aurora 开启了"存算分离"的时代,那么这篇发表于 SIGMOD 2021 的论文 《PolarDB Serverless: A Cloud Native Database for Disaggregated Data Centers》 则标志着云原生数据库进入了"全解耦"的 2.0 时代。

  • Aurora (1.0):解决了"计算与存储分离",核心是 Log is the Database。

  • PolarDB (2.0):解决了"计算与内存分离",核心是 Memory Disaggregation。

Aurora 解决了存储的弹性,但计算节点的内存依然像一块'压舱石',让缩容变得笨重。PolarDB 2.0 的意义在于,它通过 DMP(分布式内存池)把这块石头也浮了起来。

这篇论文详细介绍了阿里巴巴 PolarDB 如何通过解耦内存(Memory Disaggregation),实现真正的 Serverless 化。


1. 核心背景:从"二层解耦"到"三层解耦"

|------|----------------------------|-----------------------------|
| 维度 | Amazon Aurora | PolarDB Serverless |
| 解耦程度 | 存算分离 (2 层) | 计算、内存、存储全分离 (3 层) |
| 缓存位置 | 计算节点本地 (Local Buffer Pool) | 分布式内存池 (Shared Memory Pool) |
| 缩容影响 | 缓存随实例销毁,性能抖动大 | 缓存独立存在,缩容无感 |
| 扩容速度 | 分钟级 (受限于冷数据预热) | 秒级 (热数据直接挂载) |

在 Aurora 的架构中,计算和存储已经分离,但计算和内存仍然是绑定的。这带来了一个棘手的 Serverless 问题:

  • 资源耦合严重: CPU 和内存通常绑定在同一个计算节点内,导致"装箱问题"(Bin-packing),难以实现独立、灵活的按需扩容。

  • 资源利用率低: 为了应对峰值,用户不得不预留过剩的 CPU 或内存,造成浪费。

  • 故障恢复慢: 计算节点宕机时,内存状态(Buffer Pool)随之丢失,新节点启动需要漫长的预热过程(冷启动)。

  • Serverless 局限性: 现有的 Serverless 数据库在暂停(Auto-pause)后恢复速度慢,且扩缩容步长受限。

PolarDB Serverless 提出的方案是:把内存也从计算节点中剥离出来,做成一个独立的资源池。


2. 核心架构:三层解耦 (The Disaggregation Architecture)

PolarDB Serverless 提出了从"存算分离"演进到**"全解耦"**的架构,将资源池化分为三层:

  • 计算层 (Compute Layer): 纯粹的 CPU 资源,负责 SQL 解析和事务执行,不再保留本地状态。

  • 内存层 (Remote Memory Pool): 独立的分布式内存池,通过 RDMA 网络连接,存储共享的 Buffer Pool(数据页)。

  • 存储层 (Storage Layer): 基于共享存储(PolarStore),负责数据的持久化和日志处理。


3. 关键技术: 远程内存池(Remote Memory Pool)

为了解决解耦带来的网络延迟和一致性问题,论文介绍了多项黑科技:

  • 远程内存管理 (librmem): 开发了专用接口管理远程内存页的注册、读写和失效。

  • 缓存一致性协议 (Cache Coherency): 通过 PIB(失效位图)和 PRD(引用目录)实现了跨节点的缓存失效机制,确保多个只读节点(RO)能看到主节点(RW)最新的修改。

  • 全局物理锁 (Global Page Latches): 使用 RDMA CAS(原子操作)实现了轻量级的远程锁,保护 B+ 树在多节点并发访问时的结构完整性(SMO 操作)。

  • 乐观锁与预取优化: 为了弥补远程访问的延迟,引入了乐观锁定机制减少锁竞争,并开发了"索引感知预取"(BKP)技术,根据执行计划提前将数据从存储/内存加载到本地。

  • 物化下推 (Page Materialization Offloading): 遵循"日志即数据库"理念,将 Redo Log 的应用(物化)下推到存储层异步完成,减轻了计算节点的负担。


4. Serverless 的终极体验:无感缩放

  • 极致弹性: CPU、内存、存储可以独立地根据负载进行毫秒级/秒级的扩缩容。

  • 极速恢复: 由于数据页保留在独立的内存池中,计算节点宕机后,新节点无需从磁盘预热数据。实验显示其故障恢复速度比传统架构快 5.3 倍。

  • 性能持平: 尽管引入了网络延迟,但通过 RDMA 优化和本地缓存(Local Cache)技术,其性能与本地内存架构相当,在某些场景下甚至由于共享内存减少了副本开销而表现更好。

  • Serverless 友好: 支持"无感"缩放,在资源回收时能保留热数据,实现真正的秒级恢复。

相关推荐
咚咚?2 小时前
麒麟操作系统达梦数据库集群安装(一主一从)
数据库
Mr_Xuhhh2 小时前
MySQL复合查询详解:多表查询、子查询与合并查询
数据库·sql·mysql
Warren982 小时前
Pytest Fixture 到底该用 return 还是 yield?
数据库·oracle·面试·职场和发展·单元测试·pytest·pyqt
武超杰2 小时前
深入理解JDBC:Java数据库连接的核心技术与实践
java·开发语言·数据库·jdbc
JSON_L2 小时前
使用 SQLite 创建数据库和表
数据库·sqlite·php
m0_706653232 小时前
自然语言处理(NLP)入门:使用NLTK和Spacy
jvm·数据库·python
m0_736919102 小时前
Python游戏中的碰撞检测实现
jvm·数据库·python
猴哥聊项目管理2 小时前
2026年免费项目管理工具,支持任务分配+甘特图+协作 推荐
大数据·数据库·甘特图·项目管理工具·项目管理软件·免费项目管理软件·研发项目管理软件
jiunian_cn2 小时前
【Redis】list数据类型相关指令
数据库·redis·list