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

【分布式系统百科】存算分离架构

文章导航

标签入口
#存算分离#Snowflake#Aurora#Neon#云原生数据库#弹性伸缩

目录

存算分离架构

一个电商平台的核心交易数据库运行在三节点集群上,每个节点配备 64 核 CPU、512 GB 内存和 8 TB NVMe SSD。年底大促结束后,数据量从 5 TB 膨胀到 18 TB,存储空间告急。但 CPU 利用率在非促销期长期低于 15%。运维团队提交了扩容工单:要么给每个节点挂载更大的磁盘——但这要求停机迁移数据;要么加三个新节点——但这意味着花三倍的钱买完全用不上的 CPU 和内存。无论选哪条路,都在为不需要的资源买单。

这个困境并非个例。在传统的存算一体(Coupled Storage and Compute)架构下,计算资源和存储资源被绑定在同一个物理节点上,无法独立扩展。需要更多存储时必须同时购买计算能力,需要更多计算能力时又不得不附带存储开销。这种耦合在数据规模较小时尚可接受,但当数据量增长到 PB 级别、计算需求呈现明显的潮汐特征时,资源浪费就变得触目惊心。

存算分离(Disaggregated Storage and Compute)架构正是为了打破这一耦合而诞生的。它将计算层和存储层拆分为独立的服务,各自独立部署、独立扩展、独立演进。计算节点不持有持久数据,可以随时启停;存储层不承担查询计算,专注于数据的可靠存储和高效读写。两者通过网络连接,中间往往还有一层缓存来弥合延迟差距。

本文将从架构原理出发,深入剖析 Snowflake、Aurora、Neon 三个代表性系统的设计,讨论存算分离带来的性能权衡和对事务模型的影响,并展望这一架构的未来演进方向。

存算分离架构对比

一、一个数据库扩容的困境

1.1 耦合架构下的资源浪费

回到开头的例子。这个电商平台的数据库采用经典的 Shared-Nothing 架构,数据按哈希分片分布在三个节点上。每个节点既是计算节点又是存储节点,负责处理分配给它的数据分片上的所有读写请求。

当存储空间不足时,运维面临的选择是:

  1. 纵向扩展(Scale Up):更换更大的磁盘。这需要停机、数据迁移、重新配置,对线上业务影响巨大。而且买了更大的磁盘,CPU 和内存并没有增加,等于为存储问题付出了停机代价却没有获得任何计算能力的提升。

  2. 横向扩展(Scale Out):增加节点数量。每个新节点都必须配备与现有节点相同规格的 CPU、内存和磁盘,否则会成为性能瓶颈。然后还需要进行数据再平衡(Rebalancing),将部分数据从旧节点迁移到新节点。对于一个 18 TB 的数据库,这个过程可能持续数小时,期间系统性能显著下降。

  3. 冷热分层:将冷数据导出到廉价存储(如对象存储),只在主库保留热数据。但这需要应用层做大量改造,查询逻辑也要适配多个存储后端,工程复杂度陡增。

无论哪种方案,核心问题都是同一个:计算和存储被绑在了一起,无法单独调整其中一个维度。

1.2 潮汐负载下的困境

资源浪费在潮汐负载(Tidal Workload)场景下尤为严重。考虑一个数据分析平台,白天有 200 个分析师同时运行复杂查询,晚上只有定时任务跑批处理。白天需要 100 个计算核心,晚上只需要 10 个。但在存算一体架构下,你必须为峰值负载配置硬件,这意味着 90% 的计算资源在夜间完全闲置。

按典型的云服务器定价计算,一台 32 核 128 GB 内存的实例每小时费用约 2 美元。如果需要 4 台这样的机器来应对峰值,每月固定开销约 5760 美元。而实际利用率可能只有 30%,等于每月白花 4000 美元。

更糟糕的是,扩容和缩容都不是瞬时的。Shared-Nothing 架构的扩缩容需要数据再平衡,这可能需要几十分钟甚至几小时。等扩容完成,高峰期可能已经过去了。

1.3 存算分离的根本动机

存算分离的根本动机可以归结为一句话:让计算资源和存储资源各自按需伸缩

这种解耦带来的灵活性在云环境下尤其有价值。云厂商提供几乎无限的对象存储(如 AWS S3),按实际使用量计费,不需要预先配置容量。计算实例可以按秒计费、按需启停。存算分离架构恰好能充分利用这些云基础设施的弹性能力。

二、存算一体与存算分离的架构对比

下图从架构层面对比 Shared-Nothing 存算一体与存算分离两种范式的本质差异:

flowchart LR
    subgraph SN["Shared-Nothing 存算一体"]
        direction TB
        SN1["节点 A<br/>CPU + 内存<br/>+ 磁盘(分片 1)"]
        SN2["节点 B<br/>CPU + 内存<br/>+ 磁盘(分片 2)"]
        SN3["节点 C<br/>CPU + 内存<br/>+ 磁盘(分片 3)"]
        SN1 -.->|"RPC 协调"| SN2
        SN2 -.->|"RPC 协调"| SN3
    end

    subgraph DA["存算分离"]
        direction TB
        subgraph CP["计算池(按需伸缩)"]
            C1["计算节点 1"]
            C2["计算节点 2"]
            C3["计算节点 N..."]
        end
        NET["高速网络"]
        subgraph SP["存储池(独立伸缩)"]
            S1["存储节点 / 对象存储"]
        end
        CP -->|"独立扩缩容"| NET
        NET -->|"独立扩缩容"| SP
    end

Shared-Nothing 架构中,每个节点将计算与存储绑定在一起,扩展任一维度都需要同时调整另一维度。存算分离架构将两者解耦为独立的资源池,通过网络连接,各自按需弹性伸缩。这种解耦的代价是引入了网络延迟和缓存管理的复杂性。

2.1 Shared-Nothing 架构

Shared-Nothing 架构是过去二十年分布式数据库的主流范式。Cassandra、CockroachDB、TiDB(的早期版本)、MongoDB 等系统都采用或借鉴了这种架构。其核心思想是:每个节点拥有自己的计算资源和存储资源,节点之间不共享任何状态,通过网络消息传递来协调。

┌────────────┐  ┌────────────┐  ┌────────────┐
│  Node A    │  │  Node B    │  │  Node C    │
│ ┌────────┐ │  │ ┌────────┐ │  │ ┌────────┐ │
│ │ CPU/Mem│ │  │ │ CPU/Mem│ │  │ │ CPU/Mem│ │
│ ├────────┤ │  │ ├────────┤ │  │ ├────────┤ │
│ │ Disk   │ │  │ │ Disk   │ │  │ │ Disk   │ │
│ │ Shard 1│ │  │ │ Shard 2│ │  │ │ Shard 3│ │
│ └────────┘ │  │ └────────┘ │  │ └────────┘ │
└────────────┘  └────────────┘  └────────────┘

优势:

劣势:

2.2 Shared-Disk 架构

Shared-Disk 架构(又称共享存储架构)让多个计算节点共享同一个存储系统。Oracle RAC(Real Application Clusters)是这种架构的经典代表。每个计算节点都可以读写所有数据,节点之间通过缓存一致性协议(Cache Coherence Protocol)来保证数据一致性。

优势:

劣势:

2.3 存算分离架构

存算分离架构可以看作 Shared-Disk 架构在云时代的进化。它借鉴了 Shared-Disk 的思想——多个计算节点共享存储——但做了几个关键改变:

  1. 存储层使用云对象存储或分布式存储:不再依赖 SAN 等专用硬件,而是使用 S3、GCS、Azure Blob Storage 等云对象存储,或者自建的分布式存储系统(如 Aurora 的存储层)。这些存储系统本身就是高可用、高耐久、自动扩展的。

  2. 计算节点无状态化:计算节点不持有任何持久状态,所有数据都来自存储层。计算节点可以随时启动和关闭,故障后可以在秒级时间内重新启动一个新实例接管服务。

  3. 引入缓存层:为了弥补网络存储相比本地磁盘的延迟差距,在计算节点上引入本地缓存(通常利用本地 SSD 或内存),缓存热点数据。

  4. 日志与数据分离:许多系统进一步将写入路径上的日志(WAL)和数据存储分开处理,日志走低延迟路径,数据存储走高吞吐路径。

          ┌──────────┐ ┌──────────┐ ┌──────────┐
          │ 计算节点1 │ │ 计算节点2 │ │ 计算节点3 │   ← 无状态,可弹性伸缩
          │ (CPU/Mem) │ │ (CPU/Mem) │ │ (CPU/Mem) │
          └────┬─────┘ └────┬─────┘ └────┬─────┘
               │            │            │
          ┌────▼────┐ ┌────▼────┐ ┌────▼────┐
          │本地 SSD  │ │本地 SSD  │ │本地 SSD  │   ← 缓存层
          │ 缓存    │ │ 缓存    │ │ 缓存    │
          └────┬────┘ └────┬────┘ └────┬────┘
               │            │            │
               └────────────┼────────────┘
                            │
                   ┌────────▼────────┐
                   │   共享存储层     │               ← 持久化,独立扩展
                   │ (S3 / EBS / 自建)│
                   └─────────────────┘

2.4 云对象存储的角色

云对象存储(Object Storage)是存算分离架构的基石。以 AWS S3 为例,它提供了以下关键特性:

特性 说明
持久性 11 个 9(99.999999999%)
可用性 99.99%(标准层)
容量 无上限,按需使用
计费 按存储量 + 请求数 + 数据传输量
访问方式 HTTP REST API(PUT/GET/DELETE)
一致性 强一致性(2020 年 12 月起)

对象存储的核心优势在于”无限容量、按需计费”,这正好满足了存算分离中存储层的需求。但对象存储也有明显的缺陷:访问延迟高(首字节延迟通常在 10-50 毫秒),不支持原地更新(Immutable),不支持追加写入。这些限制深刻影响了存算分离系统的设计选择——数据格式必须是不可变的(如 Snowflake 的 Micro-Partition),写入必须是批量写入而非逐条写入,读取必须依赖缓存来降低平均延迟。

三、Snowflake:云原生数据仓库的典范

Snowflake 是存算分离架构在分析型数据库(OLAP)领域的标杆。它在 2012 年创立时就选择了完全构建在云基础设施之上的路线,没有任何本地部署版本。这个设计选择让 Snowflake 能够彻底贯彻存算分离的理念。

3.1 三层架构

Snowflake 的架构由三个独立的层组成:

云服务层(Cloud Services Layer):负责元数据管理、访问控制、查询解析与优化、事务管理。这一层是多租户共享的,由 Snowflake 统一运维。它本身也是高可用的,基于分布式元数据存储构建。

虚拟仓库层(Virtual Warehouse Layer):即计算层。每个虚拟仓库是一组 EC2 实例组成的计算集群,用于执行 SQL 查询。虚拟仓库之间完全独立,没有任何共享状态。

集中存储层(Centralized Storage Layer):所有数据存储在 S3(AWS)、Azure Blob Storage 或 GCS 上。数据以 Micro-Partition 格式存储,每个 Micro-Partition 是一个不可变的、列式存储的、压缩的文件。

┌─────────────────────────────────────────────────────┐
│                 云服务层                             │
│  元数据管理 │ 访问控制 │ 查询优化 │ 事务管理         │
└─────────────────────────────────────────────────────┘
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
   ┌────────────┐ ┌────────────┐ ┌────────────┐
   │ 虚拟仓库 A │ │ 虚拟仓库 B │ │ 虚拟仓库 C │
   │ (ETL 专用) │ │ (BI 查询)  │ │ (数据科学) │
   │  XL: 16节点│ │  M: 4节点  │ │  S: 1节点  │
   └──────┬─────┘ └──────┬─────┘ └──────┬─────┘
          │              │              │
          └──────────────┼──────────────┘
                         ▼
            ┌─────────────────────────┐
            │     S3 / Azure Blob     │
            │     (Micro-Partitions)  │
            └─────────────────────────┘

3.2 虚拟仓库的设计

虚拟仓库是 Snowflake 弹性能力的核心。每个虚拟仓库有以下特征:

这种设计使得不同工作负载可以使用不同大小的虚拟仓库,互不干扰。ETL 任务用大仓库快速完成后立即挂起;BI 仪表盘查询用中等仓库全天运行;临时的数据探索用小仓库按需启动。

-- 创建一个中等大小的虚拟仓库,空闲 5 分钟后自动挂起
CREATE WAREHOUSE analytics_wh
  WAREHOUSE_SIZE = 'MEDIUM'
  AUTO_SUSPEND = 300
  AUTO_RESUME = TRUE
  MIN_CLUSTER_COUNT = 1
  MAX_CLUSTER_COUNT = 5
  SCALING_POLICY = 'STANDARD';

-- ETL 仓库:大规格,任务完成立即挂起
CREATE WAREHOUSE etl_wh
  WAREHOUSE_SIZE = 'X-LARGE'
  AUTO_SUSPEND = 60
  AUTO_RESUME = TRUE;

-- 在不同仓库上运行查询,互不影响
USE WAREHOUSE analytics_wh;
SELECT customer_segment, SUM(revenue)
FROM sales_fact
WHERE sale_date >= '2025-01-01'
GROUP BY customer_segment;

-- 动态调整仓库大小(在线操作,不中断运行中的查询)
ALTER WAREHOUSE analytics_wh SET WAREHOUSE_SIZE = 'LARGE';

3.3 本地 SSD 缓存

虚拟仓库中的每个计算节点都配备了本地 SSD,用于缓存从 S3 读取的 Micro-Partition 数据。Snowflake 使用一致性哈希(Consistent Hashing)来分配缓存职责:每个 Micro-Partition 根据其文件名的哈希值被分配给特定的计算节点缓存。

这种设计的好处是:

在实际生产环境中,热数据的缓存命中率通常可以达到 80% 以上,这意味着大部分查询的 I/O 延迟接近本地 SSD 的水平(~100 微秒),而非 S3 的水平(~10 毫秒)。

3.4 Micro-Partition 与不可变存储

Snowflake 将表数据划分为大量的 Micro-Partition,每个 Micro-Partition 包含 50 MB 到 500 MB 的压缩数据(对应原始数据约 500 万行)。Micro-Partition 具有以下特征:

不可变设计与对象存储完美契合——S3 本身就不支持原地更新。这也为时间旅行(Time Travel)和零拷贝克隆(Zero-Copy Clone)功能提供了天然支持:

-- 时间旅行:查询 1 小时前的数据状态
SELECT * FROM orders AT(OFFSET => -3600);

-- 查询特定时间点的数据
SELECT * FROM orders AT(TIMESTAMP => '2025-12-01 10:00:00'::TIMESTAMP);

-- 零拷贝克隆:立即创建表的逻辑副本,不复制任何数据
CREATE TABLE orders_backup CLONE orders;

-- 克隆整个数据库用于开发环境
CREATE DATABASE dev_db CLONE prod_db;

零拷贝克隆只是在元数据层创建了新的指针,指向相同的 Micro-Partition 文件。只有当克隆表发生写入时,才会创建新的 Micro-Partition。一个 10 TB 的表,克隆操作只需要几秒钟,不产生任何额外的存储开销(直到写入发生)。

3.5 弹性伸缩的实际效果

Snowflake 的存算分离架构在弹性伸缩方面展现了显著的优势。以下是几个实际场景:

场景一:突发分析需求。数据分析团队需要对过去一年的用户行为数据运行一系列复杂查询。他们临时启动一个 X-Large 仓库(16 节点),查询在 10 分钟内完成,仓库自动挂起。总费用约 3 美元(按 16 节点 * 10 分钟计算)。如果在传统架构下,要么等待现有集群慢慢跑完(可能需要数小时),要么永久增加节点(每月多花数千美元)。

场景二:读写隔离。ETL 管道使用一个专用仓库加载数据,BI 仪表盘使用另一个仓库查询数据。两个仓库读写同一份数据,但互不影响。传统架构下,ETL 的写入负载经常导致查询延迟飙升,这在存算分离架构下完全消除了。

场景三:多时区团队。北美团队和亚太团队使用不同的虚拟仓库,分别在各自的工作时间活跃,非工作时间自动挂起。同一份数据被全球团队共享,但计算费用只在使用时产生。

四、Aurora:日志即数据库

如果说 Snowflake 代表了 OLAP 领域的存算分离典范,那么 Amazon Aurora 则是 OLTP 领域的标杆。Aurora 的核心洞察可以用一句话概括:日志即数据库(The Log Is the Database)

4.1 传统 MySQL 的 I/O 放大问题

要理解 Aurora 的设计动机,需要先理解传统 MySQL(使用 InnoDB 引擎)在复制场景下的 I/O 路径。一次写入操作会产生以下 I/O:

  1. 写入 Redo Log(WAL)
  2. 写入 Binlog
  3. 写入数据页(刷脏页)
  4. 写入 Double Write Buffer(防止部分写入导致的页损坏)
  5. 如果有副本,还需要将 Binlog 传输到副本,副本重放 Binlog 产生自己的 Redo Log 和数据页写入

在一主两从的配置下,一次逻辑写入可能产生 5 次以上的磁盘 I/O 和多次网络传输。更重要的是,在基于 EBS(Elastic Block Store)的部署中,每一次磁盘 I/O 都是一次网络往返(因为 EBS 本身就是网络存储)。Aurora 论文测量发现,传统 MySQL 在 EBS 上的网络 I/O 是 Aurora 的 7.7 倍。

4.2 Aurora 的架构

Aurora 的核心思想是:只将 Redo Log 记录通过网络传输到存储层,由存储层负责将日志应用为数据页。这从根本上减少了网络 I/O 量。

              ┌─────────────────┐
              │  写入实例        │
              │ (SQL + 事务管理) │
              └───────┬─────────┘
                      │ 只发送 Redo Log 记录
                      ▼
     ┌────────────────────────────────────┐
     │         Aurora 存储层               │
     │                                    │
     │  AZ-a      AZ-b      AZ-c         │
     │ ┌──────┐ ┌──────┐ ┌──────┐        │
     │ │ Seg1 │ │ Seg1 │ │ Seg1 │ ← 副本1│
     │ │ Copy │ │ Copy │ │ Copy │        │
     │ ├──────┤ ├──────┤ ├──────┤        │
     │ │ Seg1 │ │ Seg1 │ │ Seg1 │ ← 副本2│
     │ │ Copy │ │ Copy │ │ Copy │        │
     │ └──────┘ └──────┘ └──────┘        │
     │                                    │
     │ 每个 Segment: 10 GB,6 副本跨 3 AZ │
     └────────────────────────────────────┘
                      │
              ┌───────┴───────────┐
              ▼                   ▼
     ┌──────────────┐   ┌──────────────┐
     │  读副本 1     │   │  读副本 2     │
     │ (SQL 只读)    │   │ (SQL 只读)    │
     └──────────────┘   └──────────────┘

关键设计决策:

4.3 基于日志的协议

Aurora 写入路径的核心流程如下:

  1. 写入实例执行事务,生成 Redo Log 记录。
  2. 将 Redo Log 记录发送到 6 个存储节点(异步并行发送)。
  3. 等待 4 个(6 个中的任意 4 个)存储节点确认收到,即满足写入仲裁(Write Quorum)。
  4. 事务提交确认返回给客户端。
  5. 存储节点在后台异步将 Redo Log 应用为数据页。
# Aurora 写入路径的简化伪代码
class AuroraWriter:
    def __init__(self, storage_nodes: list):
        self.storage_nodes = storage_nodes  # 6 个存储节点
        self.current_lsn = 0

    def write(self, redo_record: bytes) -> bool:
        self.current_lsn += 1
        lsn = self.current_lsn

        # 并行发送 Redo Log 到所有存储节点
        acks = []
        for node in self.storage_nodes:
            future = node.append_log_async(lsn, redo_record)
            acks.append(future)

        # 等待写入仲裁(4/6)
        ack_count = 0
        for future in as_completed(acks):
            if future.result():
                ack_count += 1
            if ack_count >= 4:  # Write Quorum
                return True

        return False  # 写入失败

    def read_page(self, page_id: int) -> bytes:
        # 读取时只需要 3/6(Read Quorum)
        # 先查本地缓存
        if page_id in self.buffer_pool:
            return self.buffer_pool[page_id]
        # 从存储层读取最新页面
        # 存储节点已经在后台将日志应用为数据页
        return self.storage_nodes.read_quorum(page_id, quorum=3)

网络 I/O 的大幅减少:在传统 MySQL 中,一次写入需要传输完整的数据页(通常 16 KB)加上多种日志。在 Aurora 中,只需要传输 Redo Log 记录(通常几百字节)。论文数据显示,对于一个典型的 OLTP 工作负载,Aurora 的网络 I/O 仅为传统 MySQL 的 1/7.7。

4.4 仲裁模型详解

Aurora 使用的仲裁模型(Quorum Model)需要满足以下条件:

Aurora 引入了两个关键的一致性点(Consistency Points)来管理日志的应用进度:

读副本通过接收写入实例转发的日志记录来更新自己的缓冲池。读副本收到日志记录后,将对应的缓存页面标记为无效。当读副本需要读取该页面时,可以从存储层获取最新版本(存储层已经在后台应用了日志)。读副本的延迟(Replication Lag)通常在 10-20 毫秒以内。

4.5 即时崩溃恢复

传统数据库崩溃恢复需要重放(Replay)Redo Log 来恢复到崩溃前的状态。对于写入密集的工作负载,Redo Log 可能积累了大量未刷盘的数据,重放过程可能需要数分钟甚至数十分钟。

Aurora 的崩溃恢复几乎是瞬时的。原因是:

  1. 日志应用在存储层持续进行:存储节点在后台不断将收到的日志应用为数据页,所以存储层的数据几乎总是最新的。
  2. 恢复只需确定截断点:写入实例崩溃后,新实例启动时只需要联系存储节点,确定 VDL(最后一个持久化的日志点),截断 VDL 之后的未提交日志即可。
  3. 无需重放日志:因为存储层已经完成了日志到数据页的转换,新实例不需要重新执行这个过程。

实际测量显示,Aurora 的崩溃恢复时间通常在 10 秒以内,与数据库大小无关。相比之下,传统 MySQL 的恢复时间与 Redo Log 的大小成正比。

4.6 Aurora Multi-Master

Aurora 的标准架构是单写入实例,这在写入密集场景下可能成为瓶颈。Aurora Multi-Master 扩展允许多个实例同时写入。

Multi-Master 面临的核心挑战是写入冲突。当两个实例同时修改同一行数据时,需要一种冲突检测和解决机制。Aurora Multi-Master 采用乐观冲突检测:

  1. 两个实例各自本地执行事务,生成 Redo Log。
  2. 在提交时,Redo Log 被发送到存储层。
  3. 存储层检测是否有冲突(同一数据页在两个实例的写入之间发生了变化)。
  4. 如果检测到冲突,后到的事务被回滚(Abort)。

这种乐观方案在冲突率较低时(如不同实例操作不同分区的数据)性能很好,但在高冲突场景下回滚率可能较高。

五、Neon:PostgreSQL 的存算分离开源实现

Neon 是一个将存算分离理念应用到 PostgreSQL 的开源项目。它的目标是将 PostgreSQL 变成一个真正的云原生数据库:计算按秒计费,存储按量计费,启动延迟低至毫秒级。

5.1 设计动机

PostgreSQL 是世界上最流行的开源关系型数据库之一,但它的架构本质上是为单机设计的。PostgreSQL 的数据文件存储在本地磁盘上,WAL(Write-Ahead Log)也写在本地。这意味着:

Neon 的目标是在不修改 PostgreSQL 核心 SQL 引擎的前提下,通过替换其存储层来实现存算分离。

5.2 Neon 的架构

Neon 的架构由三个核心组件构成:

计算节点(Compute Nodes):运行标准的 PostgreSQL 进程,但对 WAL 处理和页面存储做了定制。计算节点不在本地保留任何持久化数据,所有数据页面都从 Pageserver 获取。计算节点是真正无状态的,可以在秒级时间内启动和关闭。

Pageserver:存储和服务数据页面的核心组件。Pageserver 接收来自 Safekeeper 转发的 WAL 记录,将其应用到对应的数据页面上,并在需要时将页面物化(Materialize)后返回给计算节点。Pageserver 将数据最终持久化到对象存储(如 S3)上。

Safekeeper:WAL 持久化层。它是一组运行 Paxos 协议的节点(通常 3 个),负责接收来自计算节点的 WAL 记录并确保其持久性。Safekeeper 的角色类似于 Aurora 中的日志存储路径——提供低延迟的持久化保证。

┌──────────────────────────────────────────────────────┐
│                     计算层                            │
│  ┌──────────────┐  ┌──────────────┐                  │
│  │ PostgreSQL   │  │ PostgreSQL   │  ← 无状态        │
│  │ Compute 1    │  │ Compute 2    │     按需启停      │
│  └──────┬───────┘  └──────┬───────┘                  │
│         │ WAL             │ WAL                      │
└─────────┼─────────────────┼──────────────────────────┘
          ▼                 ▼
┌──────────────────────────────────────────────────────┐
│                  Safekeeper 层                        │
│  ┌────────────┐ ┌────────────┐ ┌────────────┐       │
│  │ Safekeeper │ │ Safekeeper │ │ Safekeeper │       │
│  │     1      │ │     2      │ │     3      │       │
│  └──────┬─────┘ └──────┬─────┘ └──────┬─────┘       │
│         └──────────────┼──────────────┘              │
│                   Paxos 共识                         │
└────────────────────────┬─────────────────────────────┘
                         │ WAL Stream
                         ▼
┌──────────────────────────────────────────────────────┐
│                    Pageserver                         │
│  ┌─────────────────────────────────────┐             │
│  │ WAL 接收 → 页面物化 → 缓存 → 服务  │             │
│  └───────────────────────┬─────────────┘             │
│                          │                           │
│                    ┌─────▼──────┐                    │
│                    │  S3 存储    │                    │
│                    └────────────┘                    │
└──────────────────────────────────────────────────────┘

5.3 工作流程

Neon 的写入和读取流程如下:

写入路径:

  1. 应用向 PostgreSQL 计算节点发送 SQL 写入语句。
  2. PostgreSQL 执行事务,生成 WAL 记录。
  3. WAL 记录被发送到 Safekeeper 集群。
  4. Safekeeper 通过 Paxos 协议将 WAL 记录持久化到多数节点。
  5. 持久化确认返回给 PostgreSQL,事务提交。
  6. Safekeeper 异步将 WAL 流转发给 Pageserver。
  7. Pageserver 将 WAL 记录应用到对应的数据页面,并定期将页面快照写入 S3。

读取路径:

  1. 应用向 PostgreSQL 计算节点发送 SQL 查询。
  2. PostgreSQL 检查本地缓冲池(Buffer Pool)。
  3. 如果页面不在缓冲池中,向 Pageserver 请求该页面。
  4. Pageserver 根据基准页面(Base Image)加上增量 WAL 记录,物化出指定 LSN 处的页面版本。
  5. 页面返回给 PostgreSQL,加入缓冲池。
# Pageserver 页面物化的简化逻辑
class Pageserver:
    def get_page(self, page_id: int, lsn: int) -> bytes:
        """获取指定 LSN 版本的页面"""
        # 1. 找到该页面最近的基准镜像
        base_image = self.find_latest_base_image(page_id, lsn)
        page = base_image.data

        # 2. 获取基准镜像之后到目标 LSN 之间的所有 WAL 记录
        wal_records = self.get_wal_records(
            page_id,
            start_lsn=base_image.lsn,
            end_lsn=lsn
        )

        # 3. 按顺序应用 WAL 记录
        for record in wal_records:
            page = self.apply_wal_record(page, record)

        return page

    def find_latest_base_image(self, page_id: int, lsn: int):
        """从本地缓存或 S3 找到最近的基准镜像"""
        # 先查本地缓存
        cached = self.cache.get(page_id, lsn)
        if cached:
            return cached
        # 从 S3 加载
        return self.s3.load_base_image(page_id, lsn)

5.4 分支模型

Neon 最引人注目的功能之一是数据库分支(Database Branching)。类似于 Git 的分支操作,Neon 可以在任意时间点创建数据库的逻辑分支,且创建过程是即时的(通常在几百毫秒内完成)。

分支的实现基于写时复制(Copy-on-Write,CoW)机制:

这个功能在开发和测试场景下极其有用:

5.5 与 Aurora 的对比

Neon 和 Aurora 在存算分离的大方向上一致,但在具体实现上有显著差异:

维度 Aurora Neon
目标数据库 MySQL / PostgreSQL(定制) PostgreSQL(标准)
存储层 自建分布式存储 Pageserver + S3
WAL 持久化 存储层直接接收日志(6 副本) Safekeeper(Paxos,3 副本)
页面物化 存储节点后台应用 Pageserver 按需物化
分支功能 不支持 原生支持
部署模式 AWS 专有 开源,可自部署
写入路径 日志直接发送到 6 个存储节点 日志先经过 Safekeeper 共识
崩溃恢复 即时(存储层已有最新页面) 快速(Pageserver 重放增量 WAL)

Neon 的一个重要优势是开源。开发者可以在本地开发环境运行完整的 Neon 栈,也可以贡献代码改进系统。Aurora 作为 AWS 的托管服务,在运维便利性和性能优化方面有优势,但用户被锁定在 AWS 生态中。

六、Trade-off:网络延迟与弹性伸缩的博弈

存算分离不是免费的午餐。将存储从本地磁盘搬到远端网络存储,必然引入额外的网络延迟。这是存算分离架构面临的核心权衡(Trade-off)。

6.1 延迟层级分析

以下是不同存储介质的典型访问延迟对比:

存储介质 典型延迟 带宽
本地 NVMe SSD 随机读取 ~100 μs ~6 GB/s
本地 NVMe SSD 顺序读取 ~10 μs ~6 GB/s
同 AZ 内 EBS(gp3) ~200-500 μs ~1 GB/s
同 AZ 内网络存储节点 ~500 μs - 1 ms ~25 Gbps
跨 AZ 网络(同 Region) ~1-2 ms ~25 Gbps
对象存储(S3 首字节) ~10-50 ms ~100 Gbps(聚合)
对象存储(S3 大文件吞吐) ~100 ms ~100 Gbps(聚合)

从延迟角度看,本地 NVMe 到远端对象存储有 100 到 500 倍的差距。对于延迟敏感的 OLTP 工作负载,每个查询可能涉及数十到数百次页面读取,如果每次都要走网络,性能将不可接受。

6.2 缓存策略

各系统采用不同的缓存策略来弥合延迟差距。下图展示了存算分离架构中一次查询的完整数据访问路径,包括缓存命中与缓存未命中两种场景:

sequenceDiagram
    participant Client as 客户端
    participant Compute as 计算节点
    participant Cache as 本地缓存
    participant Storage as 远端存储层

    Client->>Compute: 发送查询请求
    Compute->>Cache: 检查本地缓存

    alt 缓存命中(热路径)
        Cache-->>Compute: 返回缓存数据(~100us)
        Note over Compute,Cache: 命中率 80%+ 时<br/>大部分查询走此路径
    else 缓存未命中(冷路径)
        Cache-->>Compute: 未命中
        Compute->>Storage: 通过网络请求数据
        Note over Compute,Storage: 延迟取决于存储类型<br/>EBS: ~500us / S3: ~10-50ms
        Storage-->>Compute: 返回数据
        Compute->>Cache: 写入本地缓存
    end

    Compute-->>Client: 返回查询结果

缓存命中时,数据访问延迟接近本地 SSD 水平(约 100 微秒)。缓存未命中时,需要跨网络访问远端存储,延迟可能高出两到三个数量级。因此,缓存命中率是存算分离系统性能的决定性因素。

下图进一步对比了冷访问路径与热访问路径的延迟差异:

flowchart TB
    subgraph Warm["热访问路径(缓存命中)"]
        W1["计算节点<br/>发起读取"] --> W2["本地缓存<br/>NVMe SSD / 内存"]
        W2 --> W3["返回数据"]
        W4["总延迟: ~100us - 1ms"]
    end

    subgraph Cold["冷访问路径(缓存未命中)"]
        C1["计算节点<br/>发起读取"] --> C2["本地缓存<br/>未命中"]
        C2 --> C3["网络传输<br/>~1-2ms"]
        C3 --> C4["远端存储节点<br/>读取数据"]
        C4 --> C5["网络返回<br/>~1-2ms"]
        C5 --> C6["写入本地缓存<br/>+ 返回数据"]
        C7["总延迟: ~5-50ms"]
    end

冷路径的延迟是热路径的 50-500 倍,这解释了为什么缓存预热策略在存算分离系统中至关重要。

Snowflake 的本地 SSD 缓存

Snowflake 的计算节点配备本地 SSD,用于缓存从 S3 读取的 Micro-Partition 文件。由于 Micro-Partition 是不可变的,缓存不存在一致性问题。缓存使用一致性哈希分配,同一数据总是缓存在同一节点上。热数据命中率可达 80% 以上。

对于 Snowflake 的 OLAP 工作负载,这种缓存策略非常有效,因为分析查询通常扫描大量数据,顺序读取模式让预取(Prefetch)也很有效。而且 OLAP 查询对单次延迟的容忍度较高——一个查询本身可能运行数秒到数分钟,几十毫秒的 S3 延迟在总体延迟中占比很小。

Aurora 的缓冲池缓存

Aurora 的计算实例维护标准的数据库缓冲池(Buffer Pool),缓存最近访问的数据页。由于 Aurora 的存储节点延迟较低(同 AZ 网络通信,~500 μs),缓冲池未命中的代价相对可控。Aurora 论文报告,在典型 OLTP 工作负载下,缓冲池命中率在 95% 以上。

此外,Aurora 的读副本可以通过接收写入实例转发的 WAL 记录来主动更新缓冲池,而不是等到读取时才发现缓存失效。这进一步降低了缓冲池未命中的概率。

分层缓存

一些系统采用多级缓存策略:

  1. 内存缓存(Buffer Pool):最热的数据,容量受限于实例内存大小。
  2. 本地 SSD 缓存:次热数据,容量通常在百 GB 到 TB 级别。
  3. 远端存储缓存(如 EBS 卷):更大容量,延迟略高。
  4. 对象存储(S3):冷数据,容量无限,延迟最高。
访问频率 ↑
    │
    │  ┌─────────────┐
    │  │ Buffer Pool │  ← 内存,~100 ns,GB 级
    │  ├─────────────┤
    │  │ 本地 SSD    │  ← NVMe,~100 μs,TB 级
    │  ├─────────────┤
    │  │ 网络存储     │  ← EBS/远端,~500 μs,多 TB 级
    │  ├─────────────┤
    │  │ 对象存储     │  ← S3,~10-50 ms,PB 级
    │  └─────────────┘
    └────────────────────→ 容量

6.3 存储节点故障与恢复

存算分离架构中,存储节点的故障处理是一个必须精心设计的环节。与存算一体架构不同,存储层故障不会导致计算能力丧失,但可能导致数据不可访问。

故障检测。计算节点通过以下机制检测存储节点故障:心跳超时(通常 1-3 秒未收到响应判定为疑似故障)、读取超时(单次 I/O 请求超过阈值自动重试到其他副本)、以及存储层控制面的主动通知(如 Aurora 的存储服务会向计算实例推送存储段的可用性变化)。

故障切换。以 Aurora 为例,当一个存储段(10 GB Segment)的某个副本不可用时,系统的行为取决于剩余可用副本数量:

缓存失效处理。存储节点故障时,计算节点本地缓存中可能存在指向失败存储节点的缓存条目。处理策略包括:标记相关缓存条目为待验证状态,下次读取时强制走存储层验证版本号;或者在收到故障通知后主动清除可能受影响的缓存分区。Snowflake 由于采用不可变的 Micro-Partition 设计,缓存失效问题较为简单——文件要么完整存在于 S3,要么不存在,不存在部分损坏的情况。

6.4 缓存预热与局部性优化

冷启动惩罚(Cold Start Penalty)是存算分离系统的一个实际工程挑战。当计算节点首次启动、仓库恢复运行或发生扩容时,新节点的本地缓存为空,所有数据访问都需要走远端存储路径,延迟显著增加。

冷启动场景与影响

缓存预热策略

  1. 后台预取(Background Prefetch):在仓库恢复或扩容时,基于历史访问统计预测热点数据,在后台异步预加载到本地缓存。Snowflake 的云服务层维护了每个仓库的 Micro-Partition 访问频率统计,可用于指导预热。
  2. 渐进式负载迁移(Gradual Load Shifting):扩容时不立即将流量切到新节点,而是逐步增加新节点的请求比例,让新节点在低流量下完成缓存预热。
  3. 缓存层次化:使用多级缓存(内存 + 本地 SSD + 远端缓存层)降低最坏情况下的延迟。即使内存缓存为空,本地 SSD 缓存(如果是持久化的)可能仍保留上次运行的数据。
  4. 跨网络数据移动最小化:通过智能的查询路由,将查询优先调度到已缓存相关数据的计算节点。Aurora 的读副本通过接收 WAL 流主动更新缓冲池,避免读取时的缓存未命中。Snowflake 的一致性哈希保证同一数据的重复查询总是命中同一节点。

在实践中,一个配置合理的 Snowflake 仓库从冷启动到达到 80% 缓存命中率通常需要 3-10 分钟的预热期,具体取决于工作集大小和查询模式。对于延迟敏感的场景,建议使用预留仓库(始终保持运行)而非按需挂起/恢复。

6.5 成本分析

存算分离并不总是更便宜。成本取决于具体的工作负载模式:

存算分离更省钱的场景:

存算分离可能更贵的场景:

6.6 运维复杂度对比

维度 存算一体 存算分离
部署复杂度 单一组件 多个独立组件(计算、存储、缓存)
监控项 节点级指标 计算层指标 + 存储层指标 + 网络指标
故障域 节点级别 计算层和存储层可能独立故障
扩缩容 需要数据再平衡 计算层秒级,存储层自动
备份恢复 需要全量或增量备份 时间点恢复(存储层内建)
升级 需要滚动升级 计算层可独立升级

存算分离在运维方面既有优势也有挑战。优势在于计算层无状态化后,故障恢复和版本升级变得极快——启动一个新实例即可。挑战在于系统整体的组件更多、依赖关系更复杂,对网络可靠性的要求更高。

七、对事务模型的影响

存算分离架构对数据库的事务模型产生了深远的影响。当计算和存储物理分离后,传统的事务实现方式面临新的挑战。

7.1 分布式提交的挑战

在存算一体架构下,事务提交可以通过本地磁盘的 fsync 来保证持久性。一次 fsync 调用(在 NVMe SSD 上约 50-100 μs)即可确保数据已持久化。但在存算分离架构下,“持久化”意味着数据已经通过网络写入远端存储并收到确认。这引入了额外的网络延迟。

不同系统处理这个问题的方式不同:

Aurora 的方案:写入实例只需将 Redo Log 发送到 4/6 个存储节点即可确认提交。由于存储节点位于同 Region 的不同 AZ,网络延迟约 1-2 ms。相比传统 MySQL 的多次同步 I/O(Redo Log + Binlog + 数据页 + Double Write),Aurora 的提交延迟实际上更低。

Snowflake 的方案:Snowflake 面向 OLAP 工作负载,事务主要是批量 DML(如 INSERT INTO … SELECT、MERGE)。这些操作一次性写入大量 Micro-Partition 到 S3,然后在元数据层原子地更新文件列表。单条事务的提交延迟可能较高(几秒),但单条记录级别的低延迟事务不是 Snowflake 的设计目标。

Neon 的方案:计算节点将 WAL 发送到 Safekeeper 集群,Safekeeper 通过 Paxos 协议在多数节点上持久化 WAL 后确认提交。Safekeeper 节点通常部署在同 AZ 或相邻 AZ,延迟在 1-5 ms 范围内。

7.2 日志驱动 vs 页面驱动

存算分离系统的事务持久化有两种主要方式:

日志驱动(Log-Based):如 Aurora 和 Neon。计算层只需要将 WAL/Redo Log 发送到存储层,存储层负责将日志应用为数据页面。

页面驱动(Page-Based):计算层将修改后的完整数据页面写入存储层。

在实践中,日志驱动方案已成为主流。Aurora 的成功证明了这种方式在 OLTP 场景下的性能优势。

7.3 对隔离级别的影响

存算分离对事务隔离级别的实现提出了新的要求:

快照隔离(Snapshot Isolation,SI):在存算分离架构下,实现 SI 需要在计算层维护事务的读取快照。由于存储层保留了数据的多个版本(无论是通过日志还是 MVCC 版本链),计算层可以根据事务的开始时间戳请求对应版本的数据。Snowflake 和 Neon 都采用了这种方式。

可串行化(Serializable):最高隔离级别,要求事务的执行效果等价于某种串行执行顺序。在存算分离架构下,实现可串行化需要在计算层进行冲突检测(如乐观并发控制的验证阶段),或者使用锁机制来阻止冲突。锁的管理可以放在计算层(如果是单写入节点)或者一个独立的锁管理服务中。

7.4 MVCC 与存算分离

多版本并发控制(Multi-Version Concurrency Control,MVCC)与存算分离架构有天然的契合性。MVCC 要求维护数据的多个历史版本以支持快照读取。在存算一体架构下,历史版本占用本地磁盘空间,需要定期通过垃圾回收(Garbage Collection)来清理。

在存算分离架构下:

7.5 计算侧事务处理 + 存储侧持久性

一种正在形成的共识是在计算层处理事务逻辑,在存储层保证持久性。这种分工有以下好处:

八、未来演进方向

存算分离架构仍在快速演进中。以下是几个值得关注的方向。

8.1 AI/ML 工作负载的融合

机器学习训练和推理工作负载对存储和计算的需求模式与传统数据库截然不同:

存算分离架构天然适合这种异构计算需求。未来的发展方向包括:在存储层统一管理训练数据和特征数据;在计算层灵活调度 CPU、GPU、TPU 等不同计算资源;通过缓存层为训练提供高吞吐的数据喂入管道。

Databricks 的 Lakehouse 架构已经在这个方向上做出了探索,将数据仓库和机器学习平台统一在 Delta Lake 存储层之上。

8.2 基于 CXL 的细粒度分解

当前的存算分离主要在网络层面进行——计算节点和存储节点通过以太网或 RDMA 连接。CXL(Compute Express Link)技术提供了一种更细粒度的分解可能性。

CXL 3.0 支持内存池化(Memory Pooling)和缓存一致性,允许多个计算节点以接近本地 DRAM 的延迟(~200 ns)共享远端内存。这意味着:

但 CXL 目前仍处于早期阶段。CXL 3.0 设备尚未大规模商用,软件栈也不够成熟。从 CXL 在实验室到进入云数据中心,预计还需要 3-5 年。

8.3 多云与跨云存算分离

当前的存算分离系统大多绑定在单一云厂商上。Aurora 只在 AWS 上运行,Snowflake 虽然支持多云部署但每个部署仍然绑定单一云。未来的趋势是跨云的存算分离:

Snowflake 的 Data Cloud 和 Databricks 的 Unity Catalog 已经在朝这个方向努力,但真正的跨云透明访问仍然面临网络延迟、数据传输成本和安全性等挑战。

8.4 标准化与互操作性

存算分离架构中,存储格式和访问协议的标准化至关重要。开放格式的兴起是一个积极趋势:

这些开放标准使得数据可以在不同的计算引擎之间共享。同一份存储在 S3 上的 Iceberg 表,可以同时被 Spark、Trino、Flink、Snowflake 等不同引擎访问和处理。这种互操作性是存算分离架构的重要优势——数据不再被锁定在某个特定的计算引擎中。

参考文献

  1. B. Dageville et al. “The Snowflake Elastic Data Warehouse.” SIGMOD, 2016. https://dl.acm.org/doi/10.1145/2882903.2903741

  2. A. Verbitski et al. “Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases.” SIGMOD, 2017. https://dl.acm.org/doi/10.1145/3035918.3056101

  3. A. Verbitski et al. “Amazon Aurora: On Avoiding Distributed Consensus for I/Os, Commits, and Membership Changes.” SIGMOD, 2018. https://dl.acm.org/doi/10.1145/3183713.3196937

  4. Neon. “Neon Documentation.” https://neon.tech/docs

  5. A. Depoutovitch et al. “Taurus Database: How to Be Fast, Available, and Frugal in the Cloud.” SIGMOD, 2020. https://dl.acm.org/doi/10.1145/3318464.3386129

  6. W. Cao et al. “PolarDB Serverless: A Cloud Native Database for Disaggregated Data Centers.” SIGMOD, 2021. https://dl.acm.org/doi/10.1145/3448016.3457560

  7. M. Armbrust et al. “Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics.” CIDR, 2021. http://cidrdb.org/cidr2021/papers/cidr2021_paper17.pdf

  8. N. Shamgunov. “The Design of Neon.” 2022. https://neon.tech/blog/architecture-decisions-in-neon

  9. CXL Consortium. “Compute Express Link Specification.” https://www.computeexpresslink.org/

  10. A. Lamb et al. “The Vertica Analytic Database: C-Store 7 Years Later.” VLDB, 2012. https://dl.acm.org/doi/10.14778/2367502.2367518


上一篇新硬件对分布式系统的冲击
下一篇Serverless 的分布式系统挑战


By .