让 768 台服务器看起来像 1 台这里有 768 台服务器。对一些人来说这是一大堆计算机。但对管理拥有数百万客户、每秒执行数百万次查询的应用程序基础设施的人而言这很正常。这种规模的产品通常需数千台服务器协同工作。几乎在所有情况下最难扩展的基础设施组件是数据库。单台数据库服务器无法应对高需求因此需通过数据库分片技术将查询和数据分散到多台服务器上。对于处理几 TB 以上数据的 Postgres 或 MySQL 数据库数据库分片是最佳扩展方式。下面看看如何从小型单节点数据库逐步扩展到将几 TB 数据分散存储在四个分片中最终扩展到在 768 台服务器上进行分片存储 PB 级数据。成长的烦恼要理解为何分片是扩展关系型数据库的必要手段需先了解可扩展性较差的方法存在的瓶颈。先看一个简单的应用程序架构。大多数应用程序至少在发展初期是这样运行的客户端设备上的软件通过互联网连接到位于数据中心的应用服务器该服务器负责处理身份验证、页面加载以及应用程序的所有服务器端逻辑。所有持久化数据如用户账户、帖子、设置和消息等都存储在数据库服务器中并从中读取这里的 数据库服务器 通常指 Postgres 或 MySQL本文主要关注 Postgres。即便使用大型数据库服务器拥有数十个 CPU 核心、数百 GB 的 RAM瓶颈也会很快出现通常是由于高查询量导致 CPU 受限或者是由于大量读写操作导致 I/O 受限IOPS。通用可扩展性定律很好地总结了这一现象该定律指出资源 争用 会使可扩展性随着资源增加呈非线性增长并且在某个临界点不一致性 会导致性能下降。对于 Postgres 以及任何试图在大型服务器上通过多线程或多进程进行扩展的软件系统来说都是如此。至少在短期内一种解决方法是利用只读副本。在这种配置中将原始服务器作为 主服务器并添加额外的 副本。主服务器会持续向每个副本发送消息流以确保它们与主服务器上的数据更改保持同步。写入操作INSERT、UPDATE、DELETE只能发送到主服务器若允许向任何服务器进行写入操作可能会出现数据冲突解决这个问题需要复杂且缓慢的共识算法虽可行但在大多数情况下为实现最佳性能这种做法并不理想。不过应用服务器可以将读取SELECT查询发送到副本。由于大多数应用程序的读取请求比例远高于写入请求因此这大大提升了可扩展性。即使查询流量不需要副本为实现高可用性和数据持久性副本也是必不可少的。通过添加副本数据库可以扩展以处理更多流量如 OpenAI 在单个主服务器上使用 50 个副本。但事实证明垂直扩展服务器增加 CPU/RAM和添加副本的方法只能解决部分问题有些瓶颈无法通过这些方法解决1) 写入操作仅限于一台服务器当写入量足够大时再多的只读副本也无法解决问题。在 Postgres 确认写入操作已提交之前它必须将更改记录到预写日志WAL中并将该日志刷新到持久存储中。WAL 是主服务器上所有连接共享的资源这实际上就是整个数据库的单一写入瓶颈即使有数十个副本也无济于事。2) 副本不会增加数据容量副本是主服务器数据的完整副本包括所有索引。添加副本可以增加读取数据的位置但并不能分散数据。3) 备份备份是数据持久性以及恢复点目标RPO/恢复时间目标RTO保障的重要组成部分。由于节点与存储之间的通信带宽有限将大型单体数据库备份到对象存储可能需要数小时甚至数天时间对于许多依赖频繁且有效的备份的组织来说这是无法接受的。解决这些问题最有效的方法就是分片。分片的作用分片通过将数据和查询分散到多个不同的主服务器解决了上述三个瓶颈。对于数据而言由于单个节点的存储容量和写入吞吐量有限分片尤为重要对于查询来说由于网络互连和 CPU 一次能处理的查询数量有限分片也能发挥作用。当数据规模超过几 TB 时分片就会很有用。例如有 2 TB 的数据可设置四个分片每个分片存储 500 GB 数据并处理总查询流量的 1/4。当需要存储 1 PB即 100 万 GB的数据时就需要更多的分片可使用 256 个分片每个分片配备一个主服务器和两个副本每个分片负责存储约 4 TB 数据这样一来总共就需要 256 * 3 768 台服务器如果没有一个完善的系统这将大大增加应用程序后端的复杂性。在如此复杂的情况下系统该如何决定哪些数据存储到哪台服务器决定哪些查询发送到哪台服务器处理需要同时与多个分片通信的查询对分散的数据库进行备份监控整个系统的健康状况应对服务器故障要解决上述每一个问题都有很多内容可以探讨。但本文要解决的问题是如何让这 768 台服务器对我们的应用程序来说看起来就像一个统一的数据库我们希望应用服务器从与一个复杂的系统交互转变为通过单个连接字符串与之交互就好像它在与一个大型的、可扩展的数据库进行交互一样而实际上它利用了数十或数百个分片。用于 Postgres 的 Neki 和用于 MySQL 的 Vitess 可以解决这个问题下面看看它们是如何做到的。代理层在几个关键组件中最重要的是代理层。代理是位于两个服务之间的中间件服务器在我们的场景中这两个服务分别是应用服务器和数据库服务器。代理在 Postgres 数据库中经常被使用即使不进行分片它们也可用于连接池管理和请求排队。对于普通未分片的 PostgresPgBouncer 是一个常用的代理人们用它将数千个应用程序连接多路复用到较少的直接 Postgres 连接上。PgBouncer 的目标很简单它旨在接受来自多个客户端的大量连接并将这些连接通过一个较小的连接池进行路由该连接池会与 Postgres 持续保持连接。查询排队功能在流量高峰和数据库故障转移期间非常有用这样当新的主服务器上线时请求可以继续执行。对 Postgres 进行分片需要一个更复杂的代理。最大的区别在于除了多路复用和缓冲之外代理还必须了解数据在服务器之间的分布方式并将 SQL 查询路由到正确的分片因此将其称为 路由器。在插入数据时路由器必须知道数据的分布方式这就是所谓的 [分片策略](/blog/database-sharding#sharding-strategy)。一种常见的方法是根据 ID 列的哈希值对传入的行进行分片。例如当向数据库中插入如下行时INSERT INTO users (id, username, email) VALUES(1, ada, adaexample.com),(2, grace, graceexample.com),(3, linus, linusexample.com),(4, margaret, margaretexample.com),(5, dennis, dennisexample.com),(6, barbara, barbaraexample.com),(7, donald, donaldexample.com),(8, james, jamesexample.com);四个分片中的每个分片都会被分配一个负责存储的 ID 范围路由器会将插入操作发送到正确的分片。插入操作首先会发送到路由器路由器会计算每个 ID 的哈希值然后将其转发到正确的分片。对于读取操作有些查询比较简单路由器只需将其转发到单个分片即可。例如SELECT email from user where id 4;在这种情况下路由器只需在内部记录每个用户 ID 所在的服务器并将查询转发到相应位置。根据上述示例这个查询会被发送到第一个顶部分片。但有些情况会更复杂例如SELECT email FROM userWHERE id BETWEEN 3 AND 5;具有该 ID 范围的用户分散在多个分片中。路由器必须了解数据拓扑结构制定一个将查询分发到所有可能包含匹配结果的分片的计划在路由器端聚合结果然后将完整的结果集发送给客户端。这意味着路由器本身必须内置一个完整的查询解析器和路由规划器。路由器必须能够在一个系统内完成查询解析、规划、连接池管理和缓冲等任务开发这样复杂的软件并非易事。它如何知晓每个数据库都是独一无二的有自己的模式、表和查询模式。那么路由器如何通用地知道哪些数据和查询应该发送到哪里呢在 Neki 和 Vitess 中这些信息都是通过表示系统数据拓扑的 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 代理非常适合进行配置和优化。多个代理一个数据库当有 256 个分片分布在 768 台服务器上并且每秒要处理数百万次查询时无法通过单个代理来路由所有流量需要多个代理具体数量可能是 10 个也可能是 100 个这取决于流量情况。我们仍然希望应用程序将其视为一台单一的服务器这时网络负载均衡器NLB就能发挥作用。NLB 的工作很简单允许通过单个主机/IP 进行连接并将每个连接分配到多个目标之一这就是流量在路由器之间分配的方式。一旦分配完成连接在其生命周期内将始终与同一个代理保持连接。在某些情况下NLB 并不是必需的。去掉 NLB 会使应用服务器的连接逻辑稍微复杂一些因为它需要知道每个路由器的主机信息但这样可以减少一次网络跳数将往返延迟降至最低。全貌现在所有组件都已就位可以让存储 1000 TB 数据的 768 台服务器对我们的应用程序来说看起来就像一个单一的、整体的数据库。应用服务器收到 连接到 mydb.pscale.com 上的数据库 的指令。进行 DNS 查找返回 NLB 的 IP 地址123.152.100.4。应用程序请求连接到 123.152.100.4 上的数据库。该连接首先通过 NLB 进行路由然后到达 N 个代理之一。应用程序开始发送数据库查询查询路径为应用程序 - NLB可选 - 代理 - 分片。复杂的路由逻辑对应用程序是透明的。为简单起见下面的示例中未显示 NLB。这个示例展示了如何扩展到 1 PB 的规模但分片应该在达到这个规模之前就开始进行。具体的建议取决于每个数据库的大小、模式和每秒查询量QPS但我们建议对于几 TB 以上的数据对 Postgres 和 MySQL 进行分片。通常在这个阶段会开始遇到前面提到的瓶颈长时间的备份、写入瓶颈等。如果你在扩展关系型数据库方面遇到挑战Neki 和 Vitess 是很好的解决方案。用于 MySQL 的 Vitess 已经在扩展全球大型关系型数据库方面使用了十多年。我们在为客户运营大型分片数据库方面拥有多年经验并且是 Vitess 项目的核心维护者。Neki 是由 Vitess 的同一批专家维护者开发的它为 Postgres 带来了更强大的分片系统。其他方面呢我们只是触及了像 Neki 和 Vitess 这样的分片系统所提供功能的皮毛还有很多其他有趣的细节值得探索。例如分片数据的最佳方法是什么分片数据库如何处理故障如何更改分片数量如何同时对 256 个分片进行备份请继续关注我们的更多内容。公司信息关于我们 品牌介绍 博客 更新日志 招聘信息 活动信息产品相关案例研究 企业版 定价 性能基准测试资源中心文档 迁移指南 技术支持 服务状态 信任中心课程学习面向开发者的 MySQL 数据库扩展 学习 Vitess开源项目Vitess Vitess 社区 GitHub隐私政策 | 使用条款 | Cookie 政策 | 专利信息 | 请勿分享我的个人信息