编程 Neon 深度拆解:Serverless Postgres 的存储计算分离架构——从 Safekeeper 到 Pageserver,手写一个生产级分支数据库引擎

2026-08-03 03:12:43 +0800 CST views 6

Neon 深度拆解:Serverless Postgres 的存储计算分离架构——从 Safekeeper 到 Pageserver,手写一个生产级分支数据库引擎

引言:为什么 PostgreSQL 需要「 Serverless 化」?

传统 PostgreSQL 部署有一个根本性的架构矛盾:存储与计算耦合在同一进程中。一个 PostgreSQL 实例启动时,wal_receiver、background writer、checkpointer、autovacuum launcher 等 20+ 个后台进程同时运行,即使你只跑一条 SELECT 1,整套进程树也必须全部就位。这意味着:

  1. 无法 Scale-to-Zero:即使没有查询,实例也在消耗 CPU 和内存
  2. 分支成本极高:想创建一个生产数据库的测试副本,需要完整复制整个数据目录(TB 级数据 = 数小时等待)
  3. 恢复时间不可控:PostgreSQL 的 PITR(Point-in-Time Recovery)需要重放 WAL 日志,从全量备份恢复可能需要数小时

Neon 的回答是:把存储层从 PostgreSQL 中抽离出来,用 Rust 重写一个专用的存储引擎,让 PostgreSQL 变成一个无状态的「计算节点」

这不是一个简单的「加个缓存层」的优化。Neon 重新设计了 PostgreSQL 的 WAL(Write-Ahead Log)流转路径,引入了 Safekeeper(WAL 持久化)和 Pageserver(页面存储与懒加载)两层架构,让数据库的存储变成了一个可以按需扩展、按需挂载的独立服务。

2026 年 7 月,Neon 被 Databricks 以 10 亿美元收购,正式并入 Databricks 平台,成为「Lakebase Postgres」的核心引擎。这意味着 Serverless Postgres 从一个独立产品变成了企业级数据平台的一部分。

本文将从架构层面深度拆解 Neon 的每一个核心组件,分析其存储引擎的设计哲学,并手写一个最小化的「存储计算分离」Postgres 分支引擎。


第一章:Neon 架构全景——六层抽象的 Serverless 数据库

1.1 整体架构概览

Neon 的架构可以用一句话概括:PostgreSQL 是无状态的计算节点,Neon 是有状态的存储服务

┌─────────────────────────────────────────────────────────┐
│                     Client (你的应用)                     │
└──────────────────────┬──────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────┐
│                   Proxy (连接代理)                        │
│            路由查询到正确的 Compute 节点                    │
└──────────┬───────────────────────────┬──────────────────┘
           │                           │
    ┌──────▼──────┐            ┌──────▼──────┐
    │  Compute A  │            │  Compute B  │
    │ (PostgreSQL)│            │ (PostgreSQL)│
    │  无状态节点  │            │  无状态节点  │
    └──────┬──────┘            └──────┬──────┘
           │                           │
┌──────────▼───────────────────────────▼──────────────────┐
│              Safekeeper (WAL 持久化层)                   │
│         接收 WAL、持久化、仲裁复制                          │
└──────────┬───────────────────────────┬──────────────────┘
           │                           │
┌──────────▼───────────────────────────▼──────────────────┐
│              Pageserver (页面存储引擎)                     │
│    懒加载页面、创建分支、管理 Snapshot、处理 Compaction      │
└──────────┬──────────────────────────────────────────────┘
           │
┌──────────▼──────────────────────────────────────────────┐
│              Cloud Storage (S3 / Azure Blob)             │
│           最终的持久化存储层(对象存储)                      │
└─────────────────────────────────────────────────────────┘

1.2 六层抽象详解

第一层:Proxy(连接代理)

  • 基于 Rust 实现的连接池和路由层
  • 接收客户端的 PostgreSQL 协议连接
  • 根据项目/分支信息路由到正确的 Compute 节点
  • 处理 TLS 终止、连接复用、负载均衡

第二层:Compute(计算节点)

  • 标准的 PostgreSQL 进程,但经过了修改
  • 通过 neon 扩展与 Pageserver 通信
  • WAL 不写本地磁盘,而是通过网络发送给 Safekeeper
  • 从 Pageserver 按需拉取页面(lazy loading)

第三层:Safekeeper(WAL 持久化层)

  • 接收 Compute 节点发送的 WAL 段
  • 持久化到本地磁盘和/或对象存储
  • 实现 quorum 复制(通常 3 节点中 2 节点确认)
  • 管理 WAL 的保留策略和 GC

第四层:Pageserver(页面存储引擎)

  • Neon 最核心的组件,用 Rust 编写
  • 将 WAL 流转换为页面快照(Materialized Page)
  • 支持 Copy-on-Write 分支(核心创新点)
  • 实现懒加载:只在需要时才从对象存储拉取页面
  • 管理 Compaction(合并 LSM-tree 层级)

第五层:Storage Controller(存储控制器)

  • 管理 Pageserver 之间的负载均衡
  • 处理租户(Tenant)的迁移和分裂
  • 监控存储层的健康状态

第六层:Cloud Storage(对象存储)

  • 最终的持久化层(S3、Azure Blob)
  • Pageserver 的本地磁盘作为缓存
  • WAL 段最终也会归档到对象存储

第二章:存储计算分离的核心——WAL 流转路径重设计

2.1 传统 PostgreSQL 的 WAL 路径

在标准 PostgreSQL 中,WAL 的流转路径是:

事务写入 → WAL Buffer → walwriter → 本地磁盘 WAL 文件
                                        ↓
                              checkpoint → 数据文件

关键点:WAL 和数据文件在同一个文件系统中,由同一个 PostgreSQL 实例管理。

2.2 Neon 的 WAL 路径

Neon 完全重构了这条路径:

事务写入 → WAL Buffer → WAL Sender → Safekeeper (网络传输)
                                        ↓
                              持久化到 Safekeeper 本地磁盘
                                        ↓
                              归档到对象存储
                                        ↓
                              Pageserver 消费 WAL → 更新页面快照

关键设计决策

  1. WAL 不落本地盘:Compute 节点的本地磁盘上没有 WAL 文件,这意味着 Compute 可以随时被销毁和重建
  2. Safekeeper 是 WAL 的唯一持久化点:WAL 通过网络发送给 Safekeeper,由 Safekeeper 负责持久化
  3. Pageserver 是 WAL 的消费者:Pageserver 订阅 Safekeeper 的 WAL 流,将其转换为页面快照

2.3 为什么不能直接把 WAL 写到 S3?

你可能会问:为什么不直接把 WAL 写到 S3,为什么需要 Safekeeper?

原因有三:

  1. 延迟:S3 的写入延迟在 50-200ms,而 WAL 写入需要在事务提交前完成。Safekeeper 使用本地 SSD,写入延迟在 1ms 以内
  2. 部分写入:S3 不支持追加写入(append),而 WAL 是顺序追加流。Safekeeper 可以高效地处理 WAL 段的追加
  3. 仲裁复制:Safekeeper 实现了 quorum 复制,可以在不依赖 S3 的情况下保证 WAL 的持久性

第三章:Pageserver 深度拆解——LSM-tree 与 Copy-on-Write 分支

3.1 Pageserver 的核心职责

Pageserver 是 Neon 架构中最复杂的组件,它需要同时解决三个问题:

  1. WAL 重放:将 WAL 流转换为页面快照
  2. 懒加载:不在内存中保留所有页面,按需从对象存储加载
  3. 分支管理:支持 Git 式的数据库分支,零拷贝创建测试环境

3.2 LSM-tree 存储引擎

Pageserver 的存储引擎基于 LSM-tree(Log-Structured Merge-tree)设计,与 LevelDB/RocksDB 的思路类似,但针对 PostgreSQL 的页面访问模式做了优化:

内存层(MemTable)
    ↓ flush
L0 层(最近的 WAL 段转换结果)
    ↓ compaction
L1 层(合并后的页面快照)
    ↓ compaction
L2 层(更老的页面快照)
    ↓ ...
对象存储(最终归档)

关键区别

  • 传统 LSM-tree 的 key 是用户定义的 key,Pageserver 的 key 是 (tenant_id, timeline_id, block_number)
  • 每个 key 的 value 是整个 8KB 的 PostgreSQL 页面
  • Compaction 的目的是合并多个版本的同一页面,只保留最新的

3.3 Copy-on-Write 分支:数据库的 Git

这是 Neon 最核心的创新。当你执行以下操作时:

-- 在 Neon 中创建分支
-- 等价于 git branch feature-x
SELECT neon.create_branch('feature-x', 'main');

Pageserver 的处理方式:

  1. 不复制任何数据:分支创建的开销是 O(1),只创建一个新的 timeline 元数据记录
  2. 共享父分支的页面:子分支和父分支共享相同的基础页面
  3. 写时复制:当子分支修改某个页面时,Pageserver 才会为子分支创建该页面的新版本

这与 Git 的分支模型完全一致:

  • git branch = neon.create_branch()(O(1),不复制数据)
  • git checkout = 挂载分支到 Compute 节点(毫秒级)
  • git merge = 合并两个分支的修改(Pageserver 处理冲突)

3.4 懒加载机制

Pageserver 不会一次性加载所有页面到内存。它的策略是:

  1. 首次访问:当 Compute 节点请求某个页面时,Pageserver 从对象存储下载该页面所在的 layer 文件
  2. 缓存:下载的页面缓存在 Pageserver 的本地 SSD 上
  3. 驱逐:当缓存满时,LRU 驱逐最久未访问的页面
  4. 预取:对于顺序扫描,Pageserver 可以预取后续页面

这种设计让 Pageserver 可以管理远超本地磁盘容量的数据集。一个 Pageserver 节点可以管理数 TB 的数据,但本地 SSD 只需要几百 GB 作为热数据缓存。


第四章:手写一个最小化的存储计算分离引擎

让我们从零开始实现一个最小化的「存储计算分离」Postgres 分支引擎。这个引擎将演示 Neon 的核心概念:

4.1 核心数据结构

use std::collections::BTreeMap;
use std::sync::Arc;
use tokio::sync::RwLock;

/// 页面编号(PostgreSQL 使用 8KB 页面)
type BlockNumber = u32;

/// LSN(Log Sequence Number)
type LSN = u64;

/// WAL 记录:记录对某个页面的修改
#[derive(Clone, Debug)]
struct WALRecord {
    lsn: LSN,
    block_number: BlockNumber,
    /// 页面修改后的完整内容(简化版,实际应该是 delta)
    page_data: Vec<u8>,
}

/// Timeline:一个分支的完整历史
#[derive(Debug)]
struct Timeline {
    id: String,
    /// 父分支 ID(如果是根分支则为 None)
    parent_id: Option<String>,
    /// 创建时的 LSN
    start_lsn: LSN,
    /// WAL 记录(按 LSN 排序)
    wal_records: Vec<WALRecord>,
}

/// Tenant:一个项目的所有分支
#[derive(Debug)]
struct Tenant {
    id: String,
    timelines: BTreeMap<String, Timeline>,
}

/// Pageserver:核心存储引擎
struct Pageserver {
    tenants: Arc<RwLock<BTreeMap<String, Tenant>>>,
    /// 对象存储客户端(简化为本地文件系统)
    storage_path: String,
}

4.2 WAL 消费与页面物化

impl Pageserver {
    /// 消费 WAL 记录并更新页面快照
    async fn consume_wal(&self, tenant_id: &str, timeline_id: &str, record: WALRecord) {
        let mut tenants = self.tenants.write().await;
        let tenant = tenants.get_mut(tenant_id).expect("tenant not found");
        let timeline = tenant.timelines.get_mut(timeline_id).expect("timeline not found");
        
        // 追加 WAL 记录
        timeline.wal_records.push(record.clone());
        
        // 物化页面快照(将 WAL 转换为页面的最终状态)
        self.materialize_page(tenant_id, timeline_id, record.block_number).await;
    }
    
    /// 物化单个页面:重放所有涉及该页面的 WAL 记录
    async fn materialize_page(&self, tenant_id: &str, timeline_id: &str, block_number: BlockNumber) {
        let tenants = self.tenants.read().await;
        let tenant = tenants.get(tenant_id).unwrap();
        let timeline = tenant.timelines.get(timeline_id).unwrap();
        
        // 重放所有涉及该页面的 WAL 记录
        let mut page_data = vec![0u8; 8192]; // PostgreSQL 默认 8KB 页面
        for record in &timeline.wal_records {
            if record.block_number == block_number {
                page_data.copy_from_slice(&record.page_data);
            }
        }
        
        // 写入物化后的页面到本地缓存
        let page_path = format!(
            "{}/{}/{}/pages/{}.page",
            self.storage_path, tenant_id, timeline_id, block_number
        );
        tokio::fs::write(&page_path, &page_data).await.unwrap();
    }
}

4.3 Copy-on-Write 分支实现

impl Pageserver {
    /// 创建分支:O(1) 操作,不复制任何数据
    async fn create_branch(
        &self,
        tenant_id: &str,
        new_timeline_id: &str,
        source_timeline_id: &str,
        start_lsn: LSN,
    ) {
        let mut tenants = self.tenants.write().await;
        let tenant = tenants.get_mut(tenant_id).expect("tenant not found");
        
        // 获取源分支的当前状态
        let source = tenant.timelines.get(source_timeline_id)
            .expect("source timeline not found")
            .clone();
        
        // 创建新分支:只复制元数据,不复制页面数据
        let new_timeline = Timeline {
            id: new_timeline_id.to_string(),
            parent_id: Some(source_timeline_id.to_string()),
            start_lsn,
            wal_records: Vec::new(), // 空的!新分支从零开始记录 WAL
        };
        
        tenant.timelines.insert(new_timeline_id.to_string(), new_timeline);
        
        println!(
            "[Pageserver] Branch '{}' created from '{}' at LSN {} (O(1) operation)",
            new_timeline_id, source_timeline_id, start_lsn
        );
    }
    
    /// 读取页面:实现 CoW 语义
    async fn read_page(
        &self,
        tenant_id: &str,
        timeline_id: &str,
        block_number: BlockNumber,
    ) -> Option<Vec<u8>> {
        let tenants = self.tenants.read().await;
        let tenant = tenants.get(tenant_id).unwrap();
        
        // 递归查找页面:先在当前分支找,找不到就去父分支找
        let mut current_timeline_id = timeline_id;
        loop {
            let timeline = tenant.timelines.get(current_timeline_id)?;
            
            // 尝试从物化缓存读取
            let page_path = format!(
                "{}/{}/{}/pages/{}.page",
                self.storage_path, tenant_id, current_timeline_id, block_number
            );
            
            if let Ok(data) = tokio::fs::read(&page_path).await {
                return Some(data);
            }
            
            // 当前分支没有这个页面,去父分支找
            match &timeline.parent_id {
                Some(parent_id) => {
                    current_timeline_id = parent_id;
                    println!(
                        "[Pageserver] Page {} not in timeline '{}', falling back to parent '{}'",
                        block_number, timeline_id, parent_id
                    );
                }
                None => return None, // 到达根分支仍然没有
            }
        }
    }
}

4.4 Safekeeper 实现

use std::collections::VecDeque;

/// Safekeeper:WAL 持久化层
struct Safekeeper {
    /// 项目 ID
    tenant_id: String,
    /// 分支 ID
    timeline_id: String,
    /// WAL 段队列
    wal_segments: VecDeque<WALSegment>,
    /// 最小保留 LSN(低于此 LSN 的 WAL 可以被 GC)
    min_retain_lsn: LSN,
    /// 已确认的 commit_lsn
    commit_lsn: LSN,
}

#[derive(Debug)]
struct WALSegment {
    start_lsn: LSN,
    data: Vec<u8>,
}

impl Safekeeper {
    /// 接收来自 Compute 节点的 WAL 记录
    async fn receive_wal(&mut self, record: WALRecord) {
        // 持久化到本地磁盘
        let segment_path = format!(
            "/data/wal/{}/{}.segment",
            self.timeline_id,
            record.lsn / (16 * 1024 * 1024) // 16MB WAL 段
        );
        
        // 追加写入 WAL 段
        let mut segment_data = tokio::fs::read(&segment_path).await.unwrap_or_default();
        segment_data.extend_from_slice(&record.page_data);
        tokio::fs::write(&segment_path, &segment_data).await.unwrap();
        
        // 更新 commit_lsn(假设单节点场景,无需仲裁)
        self.commit_lsn = self.commit_lsn.max(record.lsn);
        
        println!(
            "[Safekeeper] WAL record at LSN {} persisted, commit_lsn = {}",
            record.lsn, self.commit_lsn
        );
    }
    
    /// WAL GC:删除不再需要的 WAL 段
    async fn gc_wal(&mut self) {
        self.wal_segments.retain(|seg| {
            seg.start_lsn + (16 * 1024 * 1024) as u64 > self.min_retain_lsn
        });
    }
}

4.5 完整的使用示例

#[tokio::main]
async fn main() {
    // 初始化 Pageserver
    let pageserver = Pageserver {
        tenants: Arc::new(RwLock::new(BTreeMap::new())),
        storage_path: "/tmp/neon-demo".to_string(),
    };
    
    // 创建项目和分支
    {
        let mut tenants = pageserver.tenants.write().await;
        let mut timelines = BTreeMap::new();
        timelines.insert("main".to_string(), Timeline {
            id: "main".to_string(),
            parent_id: None,
            start_lsn: 0,
            wal_records: Vec::new(),
        });
        tenants.insert("my-project".to_string(), Tenant {
            id: "my-project".to_string(),
            timelines,
        });
    }
    
    // 模拟写入:向 main 分支写入数据
    for i in 0..100 {
        let record = WALRecord {
            lsn: i * 8192,
            block_number: i % 10, // 写入 10 个页面
            page_data: vec![i as u8; 8192],
        };
        pageserver.consume_wal("my-project", "main", record).await;
    }
    
    // 创建分支(O(1) 操作)
    pageserver.create_branch("my-project", "feature-x", "main", 50 * 8192).await;
    
    // 读取分支页面(CoW 语义)
    let page = pageserver.read_page("my-project", "feature-x", 0).await;
    println!("[Client] Read page 0 from feature-x: {} bytes", page.unwrap().len());
    
    // 向 feature-x 分支写入新数据(不影响 main)
    let record = WALRecord {
        lsn: 100 * 8192,
        block_number: 0,
        page_data: vec![42u8; 8192],
    };
    pageserver.consume_wal("my-project", "feature-x", record).await;
    
    // 验证:main 分支的页面没有被修改
    let main_page = pageserver.read_page("my-project", "main", 0).await;
    let feature_page = pageserver.read_page("my-project", "feature-x", 0).await;
    
    println!("[Client] main page[0]: {}", main_page.unwrap()[0]);
    println!("[Client] feature-x page[0]: {}", feature_page.unwrap()[0]);
    // 输出:main page[0]: 0, feature-x page[0]: 42
}

第五章:Neon 的性能优化策略

5.1 计算节点的无状态化

Neon 的 Compute 节点是完全无状态的,这意味着:

  • 可以在秒级启动新的 Compute 节点
  • 可以根据负载自动扩缩容
  • Compute 节点故障时,可以立即在另一台机器上重建

性能数据

  • 冷启动时间:~500ms(从对象存储加载必要的元数据)
  • 热启动时间:~50ms(复用缓存的页面)
  • 传统 PostgreSQL 启动:~5-30s(需要恢复 shared buffers、启动后台进程)

5.2 页面预取与缓存策略

Pageserver 使用多级缓存:

L1: Compute 节点的 shared_buffers(PostgreSQL 原生)
    ↓ miss
L2: Pageserver 的本地 SSD 缓存
    ↓ miss
L3: 对象存储(S3 / Azure Blob)

预取策略

  • 顺序扫描:预取后续 128 个页面(1MB)
  • 随机访问:不预取,按需加载
  • 索引扫描:预取索引层级的页面

5.3 WAL 压缩与归档

Neon 对 WAL 进行压缩后再归档到对象存储:

原始 WAL(16MB/段)
    ↓ LZ4 压缩
压缩后 WAL(~2-4MB/段)
    ↓ 归档
对象存储

压缩率:对于典型的 OLTP 工作负载,压缩率在 4:1 到 8:1 之间。


第六章:Neon vs 传统方案——为什么不是简单加个缓存?

6.1 与 RDS Aurora 的对比

特性NeonAurora
存储计算分离✅ 完全分离⚠️ 部分分离(存储层仍与实例绑定)
Scale-to-Zero✅ 真正的零❌ 最小实例仍然消耗资源
数据库分支✅ Git 式零拷贝分支❌ 需要完整快照
开源✅ Apache 2.0❌ 闭源
存储引擎Rust(自研 LSM-tree)InnoDB(修改版)
冷启动~500ms~30s(最小实例)

6.2 与 PlanetScale(Vitess)的对比

特性NeonPlanetScale
数据库PostgreSQLMySQL(Vitess)
分支模型存储层原生支持依赖 Vitess 的 VReplication
向后兼容完全兼容 PostgreSQLMySQL 兼容但有差异
本地开发✅ CLI 本地运行❌ 仅云端

6.3 与 Supabase 的对比

特性NeonSupabase
核心创新存储计算分离BaaS(后端即服务)
存储引擎自研 Pageserver标准 PostgreSQL
分支功能✅ 原生支持❌ 不支持
实时订阅需要额外配置✅ 内置 Realtime

第七章:生产部署指南

7.1 Neon Cloud 部署(推荐)

# 安装 Neon CLI
curl -fsSL https://cli.neon.tech/install.sh | sh

# 创建项目
neon projects create my-project

# 创建分支
neon branches create --project my-project feature-x

# 获取连接字符串
neon connection-string --project my-project --branch feature-x

7.2 本地自托管部署

# 克隆 Neon 存储引擎
git clone https://github.com/neondatabase/neon.git
cd neon

# 编译
cargo build --release

# 启动本地 Pageserver
./target/release/pageserver \
  --listen-addr 127.0.0.1:6400 \
  --config pageserver.toml

# 启动 Safekeeper
./target/release/safekeeper \
  --listen-addr 127.0.0.1:5454 \
  --config safekeeper.toml

# 启动 Compute 节点(使用 neon 扩展的 PostgreSQL)
./target/release/postgres \
  -D /tmp/neon-data \
  -c neon.storage_controller=127.0.0.1:6400

7.3 监控与调优

-- 查看分支的 WAL 使用量
SELECT * FROM neon.branches WHERE tenant_id = 'my-project';

-- 查看 Pageserver 的缓存命中率
SELECT * FROM neon.pageserver_stats;

-- 查看 Safekeeper 的 WAL 延迟
SELECT * FROM neon.safekeeper_stats;

第八章:Neon 的未来——被 Databricks 收购后会怎样?

2026 年 7 月,Neon 被 Databricks 以 10 亿美元收购,更名为「Lakebase Postgres」。这次收购意味着:

  1. Serverless Postgres 成为数据平台标配:Neon 的存储计算分离架构将成为 Databricks 平台的标准 Postgres 引擎
  2. OLAP + OLTP 统一:Lakehouse 架构(Iceberg + Spark)将与 Serverless Postgres 深度集成
  3. AI Agent 的数据库:Neon 的分支功能非常适合 AI Agent 场景——每个 Agent 可以有自己的数据库分支,互不干扰

总结

Neon 重新定义了 PostgreSQL 的架构边界:

  1. 存储计算分离让数据库变成了「无状态计算 + 有状态存储」的云原生架构
  2. Copy-on-Write 分支让数据库拥有了 Git 般的版本控制能力
  3. 懒加载让 Pageserver 可以管理远超本地磁盘容量的数据集
  4. Rust 实现的存储引擎提供了内存安全和极致性能

对于需要多租户隔离、数据库分支、Scale-to-Zero 的场景,Neon 是目前最成熟的选择。随着被 Databricks 收购,Serverless Postgres 将从一个独立产品变成企业数据平台的核心组件。

核心 takeaway

  • Neon 的核心创新是「把 WAL 从 PostgreSQL 中抽离出来」
  • Pageserver 的 LSM-tree + CoW 分支是整个架构的关键
  • Safekeeper 解决了「WAL 不能直接写 S3」的延迟问题
  • 生产部署推荐使用 Neon Cloud,自托管适合对数据主权有要求的场景

本文代码基于 Neon 存储引擎的概念简化实现,完整源码参考 github.com/neondatabase/neon。生产环境请使用 Neon Cloud 或经过充分测试的自托管方案。

推荐文章

jQuery中向DOM添加元素的多种方法
2024-11-18 23:19:46 +0800 CST
Roop是一款免费开源的AI换脸工具
2024-11-19 08:31:01 +0800 CST
程序员茄子在线接单