让768台服务器看起来像1台
摘要
PlanetScale介绍了如何使用数据库分片将关系型数据库从单台服务器扩展到768台服务器,并解决诸如写入限制和资源争用等瓶颈问题。
暂无内容
查看缓存全文
缓存时间: 2026/07/16 04:47
# 让 768 台服务器看起来像 1 台——PlanetScale
来源:https://planetscale.com/blog/making-768-servers-look-like-1
这就是 768 台服务器。
对一些人来说,这看起来像是很多计算机。
但对于那些管理着拥有数百万客户、每秒执行数百万次查询的应用程序基础设施的人来说,这相当正常。
这种规模的产品通常需要数千台服务器协同工作。
最难扩展的基础设施组件几乎总是数据库。
一台数据库服务器无法处理如此大的需求,因此我们必须通过**数据库分片**将查询和数据分布到多台服务器上。
对于超过几TB数据的Postgres或MySQL数据库,数据库分片是扩展的最佳方式。让我们看看如何从一个小的单节点数据库,扩展到有四个分片、每个存储几百GB的数据库,再一直扩展到在768台服务器上分片并存储PB级数据的系统。
## 成长的烦恼 (https://planetscale.com/blog/making-768-servers-look-like-1#growing-pains)
要理解为什么分片是扩展关系数据库的必要部分,我们必须先了解那些较难扩展的方法的瓶颈。
首先考虑一个简单的应用架构。
你使用过的大多数应用程序都是这样运行的,或者至少在其早期是这样。客户端设备上运行的软件通过互联网连接到应用服务器。这个应用服务器位于数据中心,处理认证、页面加载以及应用程序行为的所有服务器端逻辑。所有持久化数据(如用户账户、帖子、设置和消息)都存储在和检索自数据库服务器(这里的"数据库服务器"通常指Postgres或MySQL,尽管本文重点讨论Postgres)。
即使是大型数据库服务器(数十个CPU核心,数百GB内存),瓶颈也会很快出现。通常要么是由于高查询量导致的CPU限制,要么是由于高读写量导致的I/O限制(IOPS)。
通用可扩展性定律很好地总结了这一点:
简而言之,USL指出资源争用导致可扩展性随资源增加而亚线性增长,并且在某个点上,不一致性导致性能下降。这对于Postgres以及任何试图在更大服务器上跨多个线程或进程扩展的软件系统都成立。
短期内解决这一问题的一种方法是利用只读副本。
在这种配置中,您保留原始服务器作为主库,并添加额外的副本,如上所示。
主库不断向每个副本发送消息流,以确保它们与主库上的数据更改保持同步。写操作(`INSERT`、`UPDATE`、`DELETE`)只能发往主库。如果允许写操作发往任何服务器,可能会导致数据冲突。解决这个问题需要复杂且缓慢的共识算法,虽然可行,但在大多数情况下并不是最优性能的理想选择。
然而,应用服务器可以将读(`SELECT`)查询发送到副本。由于大多数应用的读操作比例远高于写操作,这提供了更大的可扩展性。(副本对于高可用性和数据持久性也是必需的,即使查询流量不需要它们)。
通过增加副本,数据库可以扩展到处理更多流量。一个极端的例子是OpenAI在一个主库上使用了50个副本 (https://openai.com/index/scaling-postgresql/)。
事实证明,垂直扩展服务器(增加CPU/内存)和添加副本只能达到一定限度。有些瓶颈无法通过这种方式解决。
### 1) 写操作仅限于一台服务器 (https://planetscale.com/blog/making-768-servers-look-like-1#1-writes-limited-to-one-server)
在写操作量足够高的情况下,再多的只读副本也无法缓解问题。在Postgres确认已提交的写操作之前,它必须将更改记录到其预写日志(WAL)中,并将该日志刷新到持久化存储。WAL是主库上所有连接共享的资源。这本质上就是整个数据库的单一写入瓶颈,即使你有数十个副本也是如此。
### 2) 副本不会增加数据容量 (https://planetscale.com/blog/making-768-servers-look-like-1#2-replicas-do-not-increase-data-capacity)
副本是主库数据的完整拷贝,包括所有索引。添加副本为我们提供了更多执行读操作的位置,但不会分布数据。
### 3) 备份 (https://planetscale.com/blog/making-768-servers-look-like-1#3-backups)
备份是数据持久性和RPO/RTO保证的重要组成部分。由于节点到存储通信的带宽限制,将大型单体数据库的备份存储到对象存储可能需要数小时甚至数天。对于依赖频繁且经过验证的备份的许多组织来说,这时间长到不可接受。
处理此问题的最成熟方法就是分片。
## Sharding,带"d" (https://planetscale.com/blog/making-768-servers-look-like-1#sharding-with-a-d)
分片通过将数据和查询分布到多个独立的主库上,解决了这三个瓶颈。对于数据来说,这很有用,因为单个节点只能存储这么多数据,并且写入吞吐量有限。对于查询来说,这很有用,因为网络互连和CPU一次只能处理这么多查询。
分片在数据超过几TB的任何规模下都非常有用。例如,对于2TB的数据,我们可以选择设置四个分片,每个分片存储500GB并处理总查询流量的1/4。当我们需要存储PB级数据(一百万GB)时,我们需要更多的分片。在这种情况下,我们可以使用256个分片,每个分片有一个主库加2个副本,每个分片负责存储约4TB。这需要256 \* 3 = 768 台服务器!
如果没有良好的系统,这会大大增加应用后端的复杂性。在如此多事情同时进行的情况下,系统如何……
- 决定哪些数据去往哪台服务器?
- 决定哪些查询去往哪台服务器?
- 处理需要同时与多个分片通信的查询?
- 在这个分布式的数据库上执行备份?
- 监控整个系统的健康状态?
- 响应故障服务器?
针对这些方面有很多可以讨论。但本文要解决的核心问题是:
如何让这768台服务器对应用程序来说看起来就像一个统一的数据库?
我们希望应用服务器从一个复杂的系统(如下图)进行交互:
变成通过一个单一的连接字符串与其交互,使其看起来就像一个大型可扩展的数据库:
而实际上,它利用了数十或数百个分片。针对Postgres的Neki (https://neki.dev/) 和针对MySQL的Vitess (https://vitess.io/) 解决了这个问题。让我们看看它们是如何做到的。
## 代理层 (https://planetscale.com/blog/making-768-servers-look-like-1#the-proxy-layer)
这里几个关键部分中最重要的就是代理层。
代理是位于两个服务之间的中间件服务器。在我们的案例中,这两个服务是应用服务器和数据库服务器。
代理经常与Postgres数据库一起使用。即使没有分片,它们对于连接池和请求排队也很有用。对于普通的(未分片的)Postgres,PgBouncer是一种流行的代理,人们用它来将成千上万个应用连接复用为更少的直接Postgres连接。
PgBouncer的目标很简单。它旨在接受来自许多客户端的大量连接,并通过一个较小的连接池路由它们,该连接池由它持续维护与Postgres的连接。请求排队对于流量激增和数据库故障转移期间非常有用,因此当新的主库上线时,请求可以恢复。我们有一篇关于PgBouncer的博客 (https://planetscale.com/blog/scaling-postgres-connections-with-pgbouncer),如果你想了解更多。
对Postgres进行分片需要一个更复杂的代理。最大的区别在于,除了多路复用和缓冲之外,代理还必须理解数据在服务器之间如何分布,并将SQL查询路由到正确的分片。因此,我们将其称为**路由器**。
在插入数据时,路由器必须知道数据将如何分布。这被称为分片策略 (https://planetscale.com/blog/database-sharding#sharding-strategy)。
一种常见的方法是,基于id列的哈希值对传入的行进行分片。当像这样向数据库插入行时:
``
INSERT INTO users (id, username, email) VALUES
(1, 'ada', '[email protected]'),
(2, 'grace', '[email protected]'),
(3, 'linus', '[email protected]'),
(4, 'margaret', '[email protected]'),
(5, 'dennis', '[email protected]'),
(6, 'barbara', '[email protected]'),
(7, 'donald', '[email protected]'),
(8, 'james', '[email protected]');
``
四个分片各自被分配一个负责存储的ID范围,路由器将插入操作发送到正确的分片。插入操作首先被发送到路由器,路由器计算每个ID的哈希值,然后将其转发到正确的分片。
对于读取操作,有些查询很简单,路由器直接将其传递给单个分片。
``
SELECT email from user where id = 4;
``
在这种情况下,路由器只需拥有一个内部映射,知道哪些用户ID位于哪些服务器上,然后将查询转发出去。根据上面的例子,这将是第一个(顶部)分片。
有些情况更为复杂。
``
SELECT email FROM user
WHERE id BETWEEN 3 AND 5;
``
这些ID范围内的用户分布在多个分片上。路由器必须理解数据拓扑,制定一个将查询分发到所有可能包含匹配结果的分片的计划,在路由器处聚合结果,然后将完整的结果集发送给客户端。
最终,这意味着路由器本身必须内置完整的查询解析器和路由规划器。
路由器必须能够在一个系统中执行查询解析、规划、连接池和缓冲。复杂的软件很难做到完美。
## 它是如何知道的? (https://planetscale.com/blog/making-768-servers-look-like-1#how-does-it-know)
每个数据库都是独特的,拥有自己的模式、表和查询模式。那么,路由器如何通用地知道哪些数据和哪些查询去往哪里呢?
在Neki (https://neki.dev/) 和Vitess (https://vitess.io/docs/reference/features/vschema/) 中,这些都是通过JSON文件指定的,这些文件描述了系统的数据拓扑。Vitess的VSchema和Neki的数据拓扑为工程师提供了极大的灵活性,可以精确描述表和查询应该如何分布。下面是一个简化的例子,说明我们如何指定`user`表的分片方案:
``
{
"shard_indexes": {
"user_hash": {
"type": "hash"
}
},
"tables": {
"user": {
"shard_by": "user_hash",
"column": "id"
}
}
}
``
这些元数据存储在路由器中,告诉它`user`表在其`id`列上使用`user_hash`分片索引进行分片。这个`user_hash`分片索引使用了路由器内置的值哈希功能。对于每一行传入的数据,它对ID进行哈希,并使用此信息将其发送到正确的分片进行存储。
由于这一切都是通过文本和JSON与路由器通信的,AI智能体在这里非常适合进行配置和优化。
## 多个代理,一个数据库 (https://planetscale.com/blog/making-768-servers-look-like-1#many-proxies-one-database)
在256个分片跨越768台服务器、每秒数百万查询的规模下,我们无法通过单个代理路由所有这些流量。我们需要多个!可能是10个,可能是100个,具体取决于流量的形态。
我们仍然希望应用程序将其视为一个单一的服务器。这时网络负载均衡器(NLB)就派上了用场。
NLB的工作很简单:允许通过单个主机/IP进行连接,并将每个连接分配给多个目的地之一。这就是流量如何在路由器之间分布的方式。一旦分配,连接在其生命周期内将保持与同一个代理。
在某些情况下,不需要NLB。消除NLB会给应用服务器的连接逻辑增加一点复杂性,因为它必须知道每个路由器的主机,但消除了一跳网络,使往返延迟最小化。
## 完整图景 (https://planetscale.com/blog/making-768-servers-look-like-1#the-full-picture)
现在所有组件都已就位,使存储1000TB数据的768台服务器在应用程序看来就像一个单一的整体数据库。
1. 应用服务器被告知"连接到数据库`mydb.pscale.com`"
2. 执行DNS查找,返回NLB的IP地址:`123.152.100.4`
3. 应用程序请求连接到数据库`123.152.100.4`
4. 这会将连接先路由到NLB,然后路由到N个代理之一
5. 应用程序开始发送数据库查询,路径为 应用 -> NLB(可选)-> 代理 -> 分片。复杂的路由逻辑对应用程序是隐藏的。(为简单起见,下图未显示NLB)
这个例子展示了扩展到1PB的情况,但分片应该在这个规模之前就开始。具体建议取决于每个数据库的大小、模式和QPS,但我们建议对于超过几TB的数据就对Postgres和MySQL进行分片。那通常是您开始遇到前面描述的那些瓶颈的时候:备份时间长、写瓶颈等等。如果您在扩展关系数据库时遇到挑战,Neki和Vitess就是解决方案。
用于MySQL的Vitess (https://planetscale.com/vitess) 已经使用了十多年来扩展世界上最大的关系数据库。我们在为客户运维大规模分片数据库方面拥有多年经验,并且是Vitess项目的核心维护者。Neki (https://planetscale.com/neki) 由与Vitess相同的专家维护者开发,为Postgres带来了更强大的分片系统。
## 其他方面呢? (https://planetscale.com/blog/making-768-servers-look-like-1#what-about-everything-else)
我们只是触及了像Neki和Vitess这样的分片系统所提供的所有功能的表面。还有许多其他有趣的细节。分片数据的最佳方式是什么?分片数据库如何处理故障?如何更改分片数量?如何同时在256个分片上执行备份?
敬请期待更多内容。关注我们的RSS feed (https://planetscale.com/blog/feed.atom) 或X (https://x.com/planetscale) 以保持关注。
祝你分片愉快。
相似文章
从头构建 PlanetScale:基础设施
作者详细介绍了构建 Homescale 的过程,这是一个类似于 Docker 映像和容器模型的数据库工具,能够从不可变快照中创建可写数据库实例和时间点分支,而无需完整数据复制。
@BenjDicken:分片就是:1)数据库可扩展性的基石 2)架构层面超有趣 想设计数据……
Ben Dicken 强调,分片是构建可扩展数据库和设计数据密集型应用的关键。
扩展Rails:41M请求/小时,8个数据库,disable_joins: true
Aura Frames将其Ruby on Rails应用扩展至每小时4100万请求,通过拆分为8个主数据库并利用Active Record中的disable_joins: true特性。
@PlanetScale:数据库太慢?本地挂载 NVMe 存储带来无上限 IOPS,把数据中心级性能搬进云端……
PlanetScale 推出基于本地 NVMe 存储的高性能云托管方案,为 Vitess 与 Postgres 提供无上限 IOPS 与横向扩展能力。
@motatoeshq: 我们如何将 opencomputer.dev 扩展到 100 万个沙盒
OpenComputer 为 AI 智能体提供长期运行、持久化的云虚拟机,支持有状态、始终在线的计算,并允许动态调整资源,作为临时沙盒的替代方案。