← 返回信息流
AI 资讯Hacker News·1 小时前

从零构建PlanetScale基础设施

原标题:Let's Build PlanetScale from Scratch: Infrastructure

速览

该文章详细阐述了从零开始构建PlanetScale所需的基础设施组件,包括网络、存储和计算等。内容面向对数据库平台架构感兴趣的开发者,提供了深入的实践指导。本文属于技术分享类,非重大新闻。

AI 深度解读

背景

文章作者多年前在一家数据库工具公司工作,曾构建过一些数据库克隆工具,这些工具虽然不如后来的 PlanetScale 成功,但也有其用途。其核心思想很简单:将存储层与计算层分离,并使用最佳的存储技术来克隆数据库文件。最近,作者在与一支 F1 车队(正在寻找拥有 Ceph 实操经验的 Rust 开发者)的采访相关的推文中,重新想起了这一思路,并开玩笑地告诉朋友要构建一个“Homescale”——即在家自制的 PlanetScale。

核心内容

Homescale 是一个开源项目,托管于 github.com/homescale-dev/homescale。它的目标是:从不可变的快照创建可写的数据库实例和时间点分支,而无需复制整个数据库。

作者借用了 Docker 的镜像和容器模型来表示数据库状态:

  • 数据库镜像(Image):一个不可变的起始点。
  • 数据库容器(Container):从镜像创建的可写克隆。
  • 分支(Branch):从现有容器的当前状态创建的另一个容器。

设想的 CLI 用法如下:

homescale image create --engine postgres postgres-base
homescale container create --image postgres-base dev-db
homescale container connect dev-db
homescale branch create --container dev-db feature-login
homescale container connect feature-login

该系列示例使用 Postgres,但 Homescale 的存储模型与具体数据库无关。镜像、容器和分支模型适用于任何将持久状态保存在块设备文件系统上的数据库引擎,并且该引擎能准备可恢复的快照。Postgres 是计划支持的第一个引擎,在构建存储和编排层时提供了具体实践。

引擎相关的行为通过适配器(adapter)封装。适配器负责初始化镜像、启动数据库进程、暴露连接详情,并在必要时为快照准备数据库。Homescale 则处理卷、不可变状态、可写克隆、工作负载和血缘关系(lineage)等生命周期。

“分支”一词描述了容器之间的关系。执行上述命令后,dev-dbfeature-login 都是可写数据库容器,feature-login 只是从创建分支那一刻 dev-db 的状态启动的。底层血缘关系如下:

flowchart LR
Image[/postgres-base<br/>image/]
Dev[dev-db<br/>writable]
State[/read-only<br/>state/]
Feature[feature-login<br/>writable]
Image -->|clone| Dev
Dev -->|snapshot| State
State -->|clone| Feature

镜像本身是不可变的,因此 Homescale 可以直接从其创建 dev-db。从可写容器分支需要一个中间状态:Homescale 首先捕获 dev-db 的某一时间点状态,然后从该状态创建 feature-login

创建 feature-login 不能逐字节复制 dev-db,否则一个 100 GB 的数据库在分支启动前就需要额外 100 GB 存储。分支应首先与捕获的 dev-db 状态共享数据,只有后续发生变化的数据才需要额外存储。这意味着分支必须发生在数据库进程之下,即存储层。

分离存储与计算

分离数据库存储与计算并非新概念。标准 Amazon RDS 引擎将数据库和日志文件存储在 EBS 卷上;Google Cloud SQL 在带有附加网络块存储的虚拟机中运行数据库进程(使用 Persistent Disk 或 Hyperdisk)。数据库看到的仍然是普通块设备,但其持久数据并不绑定到计算主机的本地磁盘。

Aurora、AlloyDB 和 Azure SQL Hyperscale 更进一步:它们的计算实例连接到专门为数据库设计的共享或分布式存储系统,计算实例可以替换或扩展,而无需为每个实例创建完整的数据副本。

Homescale 更接近第一种模型:数据库进程读写位于块设备上的文件系统。只需要该设备背后的存储具有独立于使用它的进程的生命周期。

flowchart TD
Database[Database process]
Filesystem[Filesystem]
Device[Block device]
Storage[(Persistent storage)]
Database -->|file reads and writes| Filesystem
Filesystem -->|block I/O| Device
Device --> Storage

这一边界让 Homescale 可以在启动数据库进程之前创建存储,并在进程停止后保留存储。更重要的是,它让分支在存储层发生,而不需要数据库引擎自己实现分支。

两个 Postgres 进程对它们共享的历史一无所知,每个进程看到的都是自己的可写块设备。其他受支持的引擎也会看到相同的存储边界;引擎适配器会改变,但快照、克隆和血缘关系模型不会变。

Homescale 仍然需要一种方法来创建一个与父设备共享未更改数据的可写块设备。

COW 是秘诀

写时复制(Copy-on-write,COW)允许可写克隆与其创建来源的不可变状态共享未更改的数据。当 Homescale 创建 dev-db 时,新容器最初从 postgres-base 读取数据,dev-db 的写入存储在自己的容器中而不改变镜像。创建 feature-login 重复相同过程:Homescale 捕获 dev-db 的当前状态,然后从中创建一个新的可写容器。

捕获的状态 dev-db@feature-login 是只读的,它保留了两个容器分离的时间点。dev-db 可以继续更改,而 feature-login 在该捕获状态之上存储自己的更改。

feature-login 读取它未更改的数据时,存储层沿着链条回溯到其父节点。这个过程可以延续多代,直到找到数据。首次写入共享数据时,存储层将相关的分配单元复制到可写容器中,然后在那里应用更改。

因此,一个 100 GB 数据库的分支表现为一个完整的 100 GB 数据库,而不需要一开始就复制另一个 100 GB。初始成本主要是元数据。存储空间随着 dev-dbfeature-login 的分歧而增长,尽管一次小规模的数据库写入可能在底层导致更大的存储分配。

该模型也会产生依赖关系:feature-login 依赖 dev-db@feature-login 来获取尚未复制到自己容器中的数据。只要分支仍依赖该中间状态,Homescale 就不能删除它。

因此,Homescale 需要持久块设备、不可变快照和可写的 COW 克隆。

Ceph

Ceph 通过 RBD(RADOS Block Device)提供这些操作。数据库看到的只是一个普通块设备。Ceph 在同一个底层系统之上提供对象、文件和块存储。作者只关心块接口 RBD。RBD 镜像是一个虚拟块设备,可以映射到机器、格式化为文件系统,并像普通磁盘一样挂载。

在前面的存储栈中,底部的持久存储就是一个 Ceph RBD 镜像。数据库仍然通过相同的文件系统和块设备接口进行读写。

在 RBD 镜像底层,Ceph 将设备拆分为对象,并跨其对象存储存储这些对象。默认对象大小为 4 MB(可配置)。这就是为什么一次小规模的数据库写入不一定会导致克隆中产生同样小的分配:当克隆首次写入仍与父设备共享的数据时,Ceph 需要在应用写入之前将相应的 RADOS 对象复制到子设备中。

RBD 快照是镜像在某一时间点的只读视图。克隆是一个可写 RBD 镜像,它引用回其父快照。

关键要点

  • 核心思想:将数据库的存储层与计算层分离,使用写时复制(COW)技术实现轻量级克隆和分支,无需完整复制数据。
  • 模型借鉴:借用了 Docker 的镜像(Image)和容器(Container)概念来建模数据库状态:不可变镜像、可写容器、以及从容器快照创建的分支。
  • 存储无关性:Homescale 的镜像-容器-分支模型与具体数据库引擎无关,只要该引擎将持久状态存储在块设备文件系统上并且能准备可恢复快照。Postgres 是首个支持的引擎。
  • 引擎适配器:引擎相关行为通过适配器封装,负责初始化镜像、启动进程、暴露连接等;Homescale 管理生命周期(卷、状态、克隆、工作负载、血缘)。
  • 分支机制:分支操作发生在存储层,而非数据库进程层。创建分支时,新容器最初共享父容器的未更改数据,只有写入变化的数据才占用
查看原文 →onatm.dev