最近我偶然发现了一个优秀的 YouTube 视频,“Pinterest 是如何在只有 6 名工程师的情况下扩展到 1100 万用户”(https://www.youtube.com/watch?si=coeqLRKu5i1nnpbI&v=QRlP6BI1PFA&feature=youtu.be)以及以下参考文章,“Pinterest 的扩展之路 —— 从零到每月数十亿页面浏览量,用时两年”(https://highscalability.com/scaling-pinterest-from-0-to-10s-of-billions-of-page-views-a/)。我认为这两个资源都非常出色,值得大家去了解系统设计。本文将概括我在学习这些资源时发现的最重要的内容。
Pinterest 的进化路线图
Pinterest 的扩张之旅可以分为四个不同的阶段:
- 探索自我的时代:这个阶段以快速制作原型和不断变化的产品需求为特点,由一个小型工程团队管理。
- 实验时代:指数级的用户增长需要快速扩张,导致采纳了众多技术。然而,这导致了一个复杂且脆弱的系统。
- 成熟时期:这个阶段涉及有意识地简化他们的架构,专注于成熟、可扩展的技术,如 MySQL、Memcache 和 Redis。Pinterest 没有增加技术栈,而是把资金投入到令其运转良好的领域。
- 返归时代:通过搭建合适的架构,Pinterest 仅通过水平扩张就得以延续增长轨道,验证了其选择的正确性。
让我们看看为什么这些技术在残酷的重构清洗中仍然屹立不倒。
核心技术:可扩展性的构建基石
Pinterest 优先考虑可靠、易于理解和可轻松扩展以满足不断增长的用户群的技术。让我们深入了解这些技术:
- MySQL:一个健壮成熟的关系数据库管理系统,以其稳定性和广泛的用户群体而闻名。这确保了易维护、易排障和易招聘熟悉该技术的工程师。最重要的是,它是我最喜欢的 f-word:免费。
- 内存缓存(Memcache):这是一个简单、高性能的系统,用于缓存频繁访问的数据。Memcache 的简单性和可靠性使其非常适合卸载数据库读取。而且免费。
- Redis:一个可处理各种数据结构并提供持久性和复制灵活性的多功能数据存储。这使 Pinterest 能够根据数据敏感性定制持久化策略。正如您所猜测的,这也是免费的。
- 选择 Solr 是因为它可以快速使用。此外,团队"尝试了 Elastic Search,但在他们的规模下,它在处理大量小型文档和大量查询方面遇到了困难。"
聚类 vs 分片:如何扩展数据库
随着数据量的急剧增加,Pinterest 面临着一个关键选择:如何分发其数据库以应对工作负载?出现了两种主要方法,每种方法都有自己的优缺点。
什么是数据库集群?
数据库集群是将多个单独的数据库实例或服务器连接到您的系统的过程。在大多数常见的数据库集群中,多个数据库实例通常由一个名为主服务器的单一数据库服务器管理。在系统设计领域,实施这种设计可能是必要的,尤其是在大型系统(Web 或移动应用)中,因为单个数据库服务器无法处理所有客户请求。为了解决这个问题,将引入多个并行工作的数据库服务器的使用。
不言而喻,采用这种技术可为我们的系统带来众多好处,如处理更多用户和克服系统故障。这种实施的主要缺点之一是引入了额外的复杂性。为了处理额外的复杂性,应由一个更高级别的服务器管理多个数据库服务器,监控整个系统的数据流。
如上图所示,多个数据库服务器通过 SAN 设备连接在一起。SAN 是存储区域网络的缩写,是一种计算机网络设备,提供对集中化的块级数据存储的访问。SAN 主要用于从服务器访问数据存储设备,如磁盘阵列和磁带库,使这些设备在操作系统中显示为直接连接的存储。尽管您仍然可以构建自己的数据库集群,但最近,公司为客户提供第三方云数据库存储即服务。使用此类服务,客户可以节省维护和监控自己的数据库服务器或集群的成本。
在本文中,我们将解释两种最常见的集群架构类型。接下来我们将为您提供一些数据库集群的优点。
数据库集群架构
无共享架构
建立一个共享无关数据库体系结构,每个数据库服务器都必须独立于所有其他节点。这意味着每个节点都有自己的数据库服务器来存储和访问数据。在这种体系结构中,没有单一的数据库服务器是主服务器。这意味着没有一个中央数据库节点来监控和控制系统中数据的访问。请注意,共享无关体系结构提供了出色的水平可扩展性,因为节点或数据库服务器之间没有共享任何资源。
共享磁盘体系结构
另一方面,我们有共享磁盘架构。在这种架构中,所有节点(CPU)共享对所有可用数据库服务器的访问权限,从而可以访问系统的所有数据。与共享无架构不同,互连网络层位于 CPU 和数据库服务器之间,允许多个数据库服务器访问。值得注意的是,与共享无架构相比,共享磁盘集群在可扩展性方面并不能提供太多优势,因为如果所有节点共享访问同一数据,则需要一个控制节点来监控系统中的数据流。问题在于,在从属节点数量超过一定限度后,主节点将无法有效地监控和控制所有从属节点。
数据库集群的优势
- 系统负载均衡
负载均衡是将一定数量的任务分配到多个不同的资源上的过程。这种任务的目标是使整个系统的处理效率大大提高。进行负载均衡的主要原因是为了防止任何系统过载导致突然系统故障的可能性。
虽然小型应用程序可能不需要多个数据库,但随着应用程序的发展,引入更多服务器的需求将是必要的。虽然对公司来说,用更高效的服务器取代数据库服务器仍然适用,但单个服务器能处理的请求数量是有限的。为了解决这个问题,多个数据库服务器被引入到系统中,并有一个主节点平均分配用户请求。这个想法是不要过度占用单个服务器,而让其他服务器空闲。
- 触及更多客户
公司投资数据库集群的主要原因之一是可扩展性。通过增加更多数据库服务器,公司可以处理来自世界各地的大量用户。
请注意,在不同地理位置实施多个数据库服务器将使数据库服务器更接近客户的地理位置,从而实现更快的客户互动。对于全球用户使用的应用程序,如 Facebook、YouTube 和 Google,这将是必需的。
- 系统中的数据冗余
数据冗余是在两个或多个不同的存储空间中存储数据的过程。在数据库群集的情况下,尽管可能有多个数据库服务器,但所有服务器必须保存完全相同的数据。数据冗余非常重要,因为如果一个数据库服务器被破坏(数据丢失或更改),我们仍然可以在另一个数据库服务器中拥有数据的副本。在某个系统的数据库出现问题的情况下,数据冗余可以成为
- 克服应用程序失败的风险
通过让多个数据库服务器并行工作,数据库工程师可以克服单点故障问题。如果一个应用程序只有一个数据库服务器,而这个服务器出现故障或关闭,则可以认为系统已经停止运行。为了解决这种问题,必须有其他数据库服务器待命。您可能永远无法知道数据库服务器何时会停机,因此始终保持其他服务器可用是更好的选择。请注意,数据冗余与应用程序故障的区别在于,在数据冗余的情况下,所有数据都会丢失。而在单点故障的情况下,数据库服务器只是暂时停机,预计将再次恢复运行,因此系统仍然可以运作,只是暂时无法存储或检索数据。
什么是数据库分片?
分片是一种数据库架构模式,与水平分区有关,即将一个表的行分割成多个不同的表,这些表称为分区。每个分区都有相同的模式和列,但行信息完全不同。同样,每个分区中保存的数据都是独立和独特的,与其他分区中的数据无关。
分片的优势
分片数据库的主要吸引力在于,它可以帮助实现水平扩展,也称为向外扩展。水平扩展是指向现有的基础架构添加更多机器来分散负载,从而支持更多流量和更快的处理速度。这通常与垂直扩展(也称为向上扩展)形成对比,后者涉及升级现有服务器的硬件,通常是添加更多内存或 CPU。
提升绩效
另一个选择分片数据库架构的原因是为了加快查询响应时间。当您在一个没有被分片的数据库上提交查询时,它可能需要搜索表中的每一行才能找到您要查找的结果集。对于一个拥有大型、单一数据库的应用程序来说,查询可能会变得非常缓慢。通过将一个表分片到多个表中,查询只需要遍历更少的行,结果集也能更快地返回。
可靠性
分片也可以通过减轻停机的影响来提高应用程序的可靠性。如果您的应用程序或网站依赖于未分片的数据库,停机可能会使整个应用程序无法使用。但是使用分片数据库,停机只会影响到单个分片。尽管这可能会导致应用程序或网站的某些部分无法为某些用户使用,但总体影响仍然小于整个数据库崩溃的情况。
更易管理
生产数据库必须全面管理,以进行定期备份、数据库优化和其他常见任务。对于单一大型数据库,这些例行任务的完成可能非常困难,仅从所需的时间窗口来看就是如此。例行的表和索引优化可能需要从几小时到几天不等,在某些情况下,定期维护变得不可行。通过采用分片方法,每个独立的"分片"可以独立维护,提供了一个更易管理的场景,并能并行执行此类维护任务。
降低成本
大多数数据库分片实现利用了低成本的开源数据库和普通数据库。这种技术也可以充分利用许多商业数据库的价格相对合理的"工作组"版本。分片技术与普通的多核服务器硬件配合很好,相比于高端的多 CPU 服务器和昂贵的存储区域网络(SAN),这类服务器硬件成本要低得多。在某些情况下,由于许可费、软件维护和硬件投资的节省,整体成本降低了 70%,与传统解决方案相比有大幅下降。
分片类型
按键范围分片
它是如何工作的?
一种分区方法是为每个分区分配一个连续的键范围(从某个最小值到某个最大值),就像纸质百科全书的卷册一样。如果你知道范围之间的边界,你就可以轻松确定包含给定键的分区。如果你还知道哪个分区是分配到哪个节点,然后您可以直接向适当的节点发出请求(或者在百科全书的情况下,从架子上取下正确的书)。
在每个分区中,我们可以将键保持有序排列。这样做的优点是,范围扫描很容易,您可以将键视为连接索引,以便一次性获取多个相关记录查询。
例如,考虑一个应用程序,它存储来自传感器网络的数据,其中关键是测量时间戳(年-月-日-时-分-秒)。范围扫描在这种情况下非常有用,因为它让您可以轻松获取某个特定月份的所有读数。
主键范围分区的弊端(热点)
然而,关键范围分区的缺点是某些访问模式可能导致热点。如果键是时间戳,那么分区对应于时间范围,每个分区对应一天。不幸的是,由于我们在测量发生时将数据从传感器写入数据库,所有写入最终都落在同一个分区(即今天的分区),因此该分区可能会因写入过多而过载。
为了避免传感器数据库中出现这个问题,您需要使用时间戳以外的其他元素作为键的第一个元素。例如,您可以在每个时间戳前面添加传感器名称,这样分区首先按传感器名称,然后按时间进行。假设您同时有许多传感器处于活动状态,写入负载将更加平均地分布在分区上。现在,当您需要获取一个时间范围内多个传感器的值时,您需要为每个传感器名称执行单独的范围查询。
键的分片散列
它是如何工作的?
由于这种歪斜和热点的风险,许多分布式数据存储使用哈希函数来确定给定键的分区。一个好的哈希函数将倾斜的数据转换为均匀分布。假设你有一个 32 位哈希函数,它接受一个字符串。每当你给它一个新的字符串,它都会返回一个看似随机的数字,范围在 0 到 2^32 之间。即使输入字符串非常相似,它们的哈希值也会均匀分布在该数字范围内。
在分区目的中,散列函数不需要是加密安全的:例如,Cassandra 和 MongoDB 使用 MD5,Voldemort 使用 FowlerNoll–Vo 函数。许多编程语言都内置有简单的散列函数(因为它们用于哈希表),但它们可能不适合分区:例如,在 Java 的 Object.hashCode()和 Ruby 的 Object#hash 中,同一个键在不同进程中可能有不同的哈希值。一旦您拥有适合键的散列函数,您就可以为每个分区分配一个散列范围(而不是键范围),并且每个键的散列落在分区范围内的键将存储在该分区中。
这种技术擅长于在分区之间公平地分配密钥。分区边界可以均匀分布,也可以伪随机选择(在这种情况下,这种技术有时被称为一致哈希)。
一致性哈希
一致性哈希是在内容交付网络(CDN)等互联网范围广泛系统的缓存中均匀分散负载的一种方式。它使用随机选择的分区边界来避免对中央控制或分布式共识的需求。请注意,这里的一致性与副本一致性或 ACID 一致性无关,而是描述了一种特定的再平衡方法。正如我们将在"再平衡难题"中看到的,这种特定的方法实际上对数据库来说并不太好使,所以在实践中很少使用(一些数据库的文档仍然提到一致性哈希,但通常是不准确的)。
由于这确实令人困惑,最好避免使用"一致性哈希"这个术语,并将其称为哈希分区。
重新平衡分片
随着时间的推移,数据库中的事物会发生变化:
- 查询吞吐量增加,因此您想添加更多 CPU 来处理负载。
- 数据集大小增加,所以您想添加更多磁盘和内存来存储它。
- 机器故障,其他机器需要接管失灵机器的责任.
这些变化都需要将数据和请求从一个节点移动到另一个节点,从集群中的一个节点将负载移动到另一个节点的过程称为再平衡。
不管使用哪种分区方案,重新平衡通常都需要满足一些最低要求:
- 经过重新平衡后,负载(数据存储、读取和写入请求)应该在集群中的各个节点之间公平共享。
- 在重新平衡的过程中,数据库应该继续接受读写操作。
- 节点之间只应该移动必要的数据,以使重新平衡快速并最小化网络和磁盘 I/O 负载。
平衡策略
有几种不同的方法来分配分区到节点,让我们逐一简单地讨论一下。
如何不进行(哈希 mod N)
当通过键的哈希值进行分区时,我们之前说过最好是将可能的哈希值划分为范围,并将每个范围分配到一个分区(例如,如果 0 ≤ hash(key) < b,则将键分配给分区 0;如果 b0 ≤ hash(key) < b1,则将其分配给分区 1,依此类推)。
也许您想知道为什么我们不直接使用 mod(许多编程语言中的%运算符)。例如,hash(key) mod 10 将返回一个 0 到 9 之间的数字(如果我们将哈希写为十进制数,哈希 mod 10 将是最后一位数)。
使用mod N 的方法的问题在于,如果节点数 N 发生变化,大部分键都需要从一个节点移动到另一个节点。例如,假设 hash(key) = 123456。如果最初有 10 个节点,那么该键最初位于节点 6 (因为 123456 mod 10 = 6)。当增加到 11 个节点时,该键需要移动到节点 3 (123456 mod 11 = 3),当增加到 12 个节点时,它需要移动到节点 0 (123456 mod 12 = 0)。如此频繁的移动会导致重新平衡变得过于昂贵,所以我们需要一种不会造成过多数据迁移的方法。
幸运的是,有一个相当简单的解决方案:创建比节点数多得多的分区,并将几个分区分配给每个节点。例如,在一个由 10 个节点组成的集群上运行的数据库可以一开始就分成 1,000 个分区,这样每个节点大约会被分配 100 个分区。现在,如果向集群添加一个节点,新节点可以从每个现有节点偷取一些分区,直到分区再次公平分布。如果从集群中删除一个节点,过程也会相反发生。
只有整个分区在节点之间移动。分区的数量不会改变,分区对键的分配也不会改变。唯一改变的是分区到节点的分配。这种分配的变更并非立即生效,需要一些时间通过网络传输大量数据,因此在传输过程中任何读写操作都会使用旧的分区分配。
动态分区
对于使用键范围分区的数据库,固定数量的分区和固定边界将非常不方便。如果边界设置错误,您可能会将所有数据都放在一个分区中,而其他分区都是空的。
手动重新配置分区边界会非常繁琐。出于这个原因,主键范围分区数据库,如 HBase 和 RethinkDB,动态创建分区。
当分区增长到超过配置的大小(在 HBase 中,默认值为 10 GB)时,它会被拆分为两个分区,使得大约一半的数据最终位于分割的两侧。相反,如果大量数据被删除,分区收缩到某个阈值以下,它可以与相邻的分区合并。这个过程类似于 B 树顶层发生的情况。
每个分区都分配给一个节点,每个节点都可以处理多个分区,就像固定数量的分区一样。在大分区被拆分后,其中一个半部分可以转移到另一个节点,以平衡负载。
在 HBase 的情况下,分区文件的传输是通过 底层分布式文件系统HDFS 完成的。
动态分区的一个优点是分区数量可以根据总数据量进行调整。如果数据量较小,则只需少量分区,开销较小;如果数据量巨大,每个分区的大小可以设置为可配置的最大值。然而,需要注意的是,由于没有预先了解如何划分分区边界,空数据库最初会有一个单一分区。在数据集较小时,直到首个分区分裂为止,所有写入操作都必须由单个节点处理,而其他节点则处于空闲状态。
为了缓解这个问题,HBase 和 MongoDB 允许在一个空数据库上配置初始分区集(这被称为预切分)。在键范围分区的情况下,预切分需要您事先了解键分布的样子。动态分区不仅适用于键范围分区的数据,也同样适用于哈希分区的数据。自版本以来,MongoDB 都支持键范围和哈希分区,并且在任何一种情况下都能动态拆分分区。
分片(Sharding)的缺点
增加了系统的复杂性
正确实施分片数据库架构是一项复杂的任务。如果没有正确操作,分片过程存在丢失数据或表损坏的重大风险。分片也会对您团队的工作流程产生重大影响。用户必须管理分布在多个分片位置的数据,而不是从单一入口点管理和访问数据,这可能对某些团队造成干扰。
数据平衡
在分片数据库架构中,有时候一个分片会超越其他分片并变得失衡,这也被称为数据库热点。在这种情况下,分片数据库的任何优势都会被抵消。数据库可能需要进行再次分片以实现更均衡的数据分布。从一开始就必须内置再平衡,否则在进行再分片时,从一个分片移动数据到另一个分片需要大量的停机时间。
从多个分片中合并数据
为了实现某些复杂的功能,我们可能需要从分散在多个分片中的不同源拉取大量数据。我们不能发出一个查询并从多个分片获取数据。我们需要向不同的分片发出多个查询,获取所有响应并将它们合并。
没有原生支持
分片并不是每个数据库引擎都原生支持的。因此,分片通常需要"自己动手"。这意味着很难找到关于分片或排查问题的技巧的文档。
为什么 Pinterest 选择分片
Pinterest 采用分片而非集群的原因是相对简单性,以及他们在"实验时代"中集群方法的负面经历。他们面临的问题有:
- 集群管理问题:集群管理算法中的漏洞导致多次中断,难以排查。
- 数据再平衡问题:自动再平衡可能会导致性能瓶颈和数据不一致性问题。
- 数据所有权混淆:发生了某一次级节点错误地假定了主节点角色,导致数据丢失。“在一个案例中,他们引入了一个新的次级节点。大约 80%的时候,次级节点称自己是主节点,而主节点变成了次级节点,因此丢失了 20%的数据。丢失 20%的数据比丢失全部还要糟糕,因为你不知道丢失了什么。”
分片提供了一种更可预测和易管理的方法。他们愿意以增加应用程序级别的控制和简单性为代价,放弃一些数据库级别的功能,如连接和事务。
向分片架构迁移
切分数据库的过程并非一蹴而就。Pinterest 采取了分阶段的方法,在功能冻结期间精心执行,以最大程度地减少对用户的影响:
- 消除连接:所有 MySQL 连接都被删除,需要数据反规范化并增加对缓存的依赖以维持性能。积极的缓存有助于弥补失去连接和需要查询多个分片的性能影响。
- 基于 ID 的分片:这个最终阶段涉及基于 64 位 ID 进行分片。这个 ID 嵌入了分片位置,消除了对独立查找表的需求,简化了数据路由。“一个用户的所有数据(pins、boards 等)都位于同一个分片上。这是一个巨大的优势。渲染用户个人资料,例如,不需要进行多个跨分片查询。它很快。”
这种分阶段的方法允许在每个阶段进行增量实施和全面验证。
分段存储的代价:缺点和解决方案
虽然分片提供了更可管理的方法,但它也带来了 Pinterest 需要解决的挑战:
- 迁移脚本:将大量数据转移到分片基础架构中比预期耗时更长,突显了对强大的脚本工具和流程的需求。
- 应用程序逻辑:缺乏数据库级联接和事务要求开发人员在应用程序层实现维护数据一致性和完整性的逻辑。
- 模式修改:仔细计划并在所有分片上应用更改是需要更改数据库模式的前提。
- 报告障碍:跨多个分片生成报告需要额外的步骤来汇总每个分片的结果。
经验收获:来自 Pinterest 之旅的关键启示
Pinterest 的扩张之路为任何架构增长系统的人提供了宝贵的经验教训:
- 简单是关键:选择简单、易懂的技术可简化故障排查并降低意外问题的风险。
- 优先考虑可扩展性:在快速增长的环境中,应该有意愿牺牲一些数据库功能以换取可扩展性。
- 设计水平增长:选择一种架构,可以在您的用户群扩大时添加更多资源。
通过拥抱简单性、强调可扩展性和从经验中学习,Pinterest 成功应对了爆炸式增长的挑战。他们的故事可作为建立和扩展高性能分布式系统的宝贵案例研究。