查看原文
其他

Apple 如何构建 iCloud 来存储数十亿个数据库

51CTO技术栈 2024-02-28

         

 

作者丨Leonardo Creed

编译丨诺亚

         

 

在过去的几个月里,我写了关于大型科技公司的各种技术“幕后揭秘”的文章,例如 Meta 的内部无服务器平台、 Google 内部喜爱的代码审查工具等等。不过,苹果的基础设施并不那么公开。我想了解 Apple 是如何构建 iCloud 的,在这篇文章中,我将介绍我所知道的一切。

         

 

Apple 将 FoundationDB 和 Cassandra 用于其云端后端服务 iCloud 和 CloudKit。而且本文的标题并没有弄错:苹果确实在其极端的多租户架构中存储了数十亿个数据库


阅读指南

 

我发现,论文中以及苹果的实践经验与Meta无服务器平台架构的设计原则和教训高度契合。

         

 

1、两者都巧妙地运用了异步处理技术,以实现用户功能的流畅性。Meta在其无服务器架构中,将非面向用户的函数任务利用该技术进行处理,从而避免影响用户体验。而苹果则在Record Layer(在下文将详细解释)的几乎全部功能上采用异步处理方式,目的是隐藏延迟,确保用户感受到的是即时响应。

         

 

2、两者都广泛采用无状态架构设计,鉴于它们都有极高的可扩展性需求。(注:无状态架构意味着服务器不保存任何会话或请求之间的持久化状态信息,从而使得每个请求都能独立处理,且可以根据需要轻松地增加或减少服务器实例以应对流量变化。)

         

 

3、两者都通过逻辑隔离资源来确保可靠性和可用性。

         

 

4、两者都以简化的方式处理各类需求。苹果提到,为存储“小数据”和“大数据”分别配置和运营独立系统是很有诱惑力的做法,但这会增加运维的复杂性。因此,苹果选择用一个抽象层来处理所有类型的数据需求。同样地,Meta在他们的无服务器平台上也采用相同策略,提供了一个统一的抽象层,用于处理各种函数负载。

         

 

5、两者都通过构建抽象层来优化开发者体验,让应用开发者无需过多关注可扩展性需求。这些需求由底层分布式系统工程师在更深层次的架构中处理。   

         

 

6、深知用户需求。无论是Meta还是Apple,它们提供的每一层架构、API设计以及每一个设计决策都是基于对特定技术使用者(无论是应用开发团队还是可观测性团队)清晰理解的基础上制定的。


Cassandra

   

Cassandra 是一种分布式、宽列式NoSQL数据库管理系统,最初由Facebook开发,用于支持Facebook收件箱搜索功能的实现。有趣的是,后来的Meta自身已逐步用ZippyDB替代了大量原本使用Cassandra的地方。

         

 

根据DataStax的信息,iCloud的部分功能由Cassandra提供支持。苹果运营着全球规模最大的Cassandra部署之一。

         

 

他们报告指出:

         

 

1. 超过30万个实例/节点

2. 数据规模达到数百PB(甚至EB级别)

3. 每个集群处理超过2 PB的数据,且拥有数千个这样的集群

4. 每秒处理数百万次查询

5. 支持数千个应用程序

         

 

Cassandra在iCloud中的应用确实彰显了其管理海量数据的能力,达到EB级别。苹果在其服务器上采用多节点Cassandra部署策略,并且团队在设计时非常注重“爆炸半径”(blast radius)控制和数据分片(sharding),以最大程度地减少故障影响范围并优化数据分布与访问性能,从而确保iCloud服务的数据可用性接近100%。

         

 

与此同时,苹果公司内部仍在积极改进Cassandra技术。来自苹果公司的Scott Andreas最近发表了关于Cassandra未来发展的演讲。同时,在苹果的招聘页面上,经常可以看到他们为分布式系统工程师岗位列出熟练使用Cassandra的要求。

         

 

尽管Cassandra在处理大规模分布式存储方面表现出色,但在苹果iCloud的特定场景下,结合使用CloudKit和Cassandra时遇到了两个关键的可扩展性限制,这导致他们采用了 FoundationDB。

         

 

1、在Cassandra单一分区内,即使编辑的是不同的记录,同一时间也只能进行一个操作。这意味着对于那些需要多个用户或设备同时处理共享数据的应用程序来说,可能会出现性能瓶颈和并发控制问题。   

         

 

2、在Cassandra中,如果需要在一个原子操作内同时更新多个记录,这些更新操作会受限于单个Cassandra分区。每个分区都有其能够处理的最大数据量限制,随着分区内数据的不断增长,Cassandra的性能往往会随之下降。

         

 

FoundationDB 和 Record Layer 解决了这两个问题。


FoundationDB


苹果对FoundationDB的公开程度要高得多。他们于 2015 年收购了 FoundationDB,此后发表了多篇论文,详细介绍了他们对 FoundationDB 的使用。

         

 

FoundationDB 是一个开源的分布式事务型键值存储系统,旨在处理大规模的数据量,并且在读写混合负载以及写入密集型工作负载方面表现出色。此外,FoundationDB 也符合 ACID(原子性、一致性、隔离性和持久性)原则。

         

 

苹果在CloudKit(其云端后端服务)中广泛使用了FoundationDB Record Layer。


来源:《FoundationDB Record Layer:开源结构化存储》

         

 

从GitHub上的描述来看,Record Layer是一个基于FoundationDB的Java API,它提供了一种面向记录的存储方式,可以大致类比为一个简单的关系型数据库。具体特性包括:

         

 

   

1. 结构化类型:记录以Protocol Buffer(protobuf)消息的形式进行定义和存储,Protocol Buffer是一种最初由Google设计的数据序列化协议。

         

 

2. 索引:Record Layer支持多种索引类型,如值索引(大多数数据库都提供的那种)、排名索引和聚合索引。索引和主键可以通过protobuf选项或程序化方式来定义。

         

 

3. 复杂类型:支持复杂数据类型,例如列表和嵌套记录,并且能够针对这些嵌套结构定义索引。

         

 

4. 查询功能:虽然Record Layer并未提供查询语言,但它提供了API接口,支持对一个或多个记录类型进行扫描、过滤和排序操作,同时还包含一个能够自动选择合适索引的查询规划器。

         

 

5. 多个记录存储与共享模式:Record Layer允许创建并管理多个独立的记录存储实例,所有实例都采用共享(且可动态演变)的模式。举例来说,不同于在一个单一数据库中存储所有用户数据的方式,每个用户可以拥有自己的记录存储,甚至可以根据需要跨不同的FDB集群实例进行分片处理。

         

 

6. 极轻量级:Record Layer被设计用于大型、分布式、无状态环境,旨在实现从打开存储到执行首次查询之间的时间间隔达到毫秒级别。

         

 

7.扩展性强:新的索引类型以及自定义索引键表达式可以动态地融入到记录存储中。

         

 

根据FoundationDB Record Layer论文所述,苹果使用FoundationDB Record Layer为服务数亿用户的大型应用提供强大的抽象层支持。CloudKit 使用Record Layer来托管数十亿个独立的数据库,其中许多数据库共享相同的模式(schema)。


为什么要使用 FoundationDB Record Layer

   

FoundationDB、Record Layer 和 CloudKit 的结构如下所示:


来源:《FoundationDB Record Layer: 开源结构化存储》


  • FoundationDB 负责所有的分布式系统和并发控制工作。

  • Record Layer 作为中间层,充当了关系数据库,以便开发者能够更轻松地与 FoundationDB 进行交互。

  • CloudKit 是最顶层的服务,为应用开发者提供了丰富的功能和API。虽然CloudKit是构建在Record Layer之上的一个典型应用案例,但内部还有其他服务和组件也基于Record Layer构建,比如用于处理JSON文档存储等需要结构化存储的场景。

         

 

Record Layer 使苹果能够在大规模上实现多租户支持。

         

 

实际上,这样的描述可能还显得保守了。

         

 

Record Layer 被用于极端的多租户环境,其中每个应用程序的每个用户都能获得独立的记录存储空间。这意味着Record Layer 托管着数十亿个共享数千种模式的独立数据库。

         

 

来源:《FoundationDB Record Layer: 开源结构化存储》

         

 

   

Record Layer 能够在如此大规模上成功处理多租户问题,主要归功于其两个核心架构决策

         

 

1.无状态操作:Record Layer 设计为无状态模式,这意味着通过简单地增加更多无状态实例,就可以轻松扩展计算资源。

         

 

这种设计使得负载均衡器和路由器的工作变得更为简化,它们只需关注数据的位置而非计算服务器的具体能力。同时,由于无状态服务器无需维护会话状态等信息,因此分配给客户端的资源集合得以减少。

         

 

2.记录存储抽象化管理:Record Layer 使用记录存储抽象层来高效管理资源分配和可扩展性。这个抽象层代表了整个逻辑数据库,包含了序列化数据、索引以及运行时状态。

         

 

每个记录存储都有特定的键范围分配,确保不同租户的数据在逻辑上保持分离。如有必要迁移某个租户的数据,过程十分直接,只需将分配给该租户的键范围迁移到新的集群中即可,因为管理与使用该记录存储所需的所有信息都包含在这个键范围内。


CloudKit 如何使用 FoundationDB 和Record Layer


来源:《FoundationDB Record Layer: 多租户结构化数据存储系统》

         

 

在CloudKit中,每个应用程序由一个遵循特定模式的“逻辑容器”来表示。这个模式详细定义了必要的记录类型、字段和索引,以实现高效的数据检索和查询功能。应用程序在CloudKit内部将其数据组织到不同的“区域”(zones)中,这样可以按逻辑分组记录,便于与客户端设备进行选择性同步。

         

 

对于每一位用户,CloudKit在FoundationDB中分配一个唯一的子空间。在这个子空间内,针对用户使用的所有应用程序,CloudKit都会为每个应用创建一个记录存储。换言之,CloudKit实际上管理着大量逻辑数据库——即用户数量乘以应用程序数量所得到的数量级,每一个都包含其自身的记录集、索引和元数据,总量高达数十亿个独立数据库。   

         

 

当CloudKit接收到来自客户端设备的请求时,它会通过负载均衡机制将请求导向可用的CloudKit服务进程。该服务进程随后与Record Layer中的相应记录存储进行交互,以完成请求操作。

         

 

CloudKit将定义好的应用程序模式转换为Record Layer中的元数据定义,并将其存储在独立的元数据存储中。此外,CloudKit还会添加特定的系统字段来丰富这些元数据,如记录的创建时间、修改时间以及记录所在的区域信息。为了实现对每个区域内记录的有效访问,区域名称会被作为前缀附加到主键上。除了用户自定义的索引外,CloudKit还管理“系统索引”,例如为了管理存储配额而维护的一种根据记录类型追踪其大小的索引。

         

 

FoundationDB和Record Layer结合使用,共同解决了苹果面临的一些关键问题,这些问题单靠Cassandra或FoundationDB都无法完美解决


已解决的问题


个性化全文搜索


FoundationDB在解决用户个性化全文搜索,以快速访问其数据方面发挥了重要作用。苹果的系统利用了FoundationDB的键顺序特性,能够实现对文本开头(前缀匹配)进行快速搜索,并且无需额外开销即可处理更复杂的搜索需求,如查找相近词或特定顺序排列的词语(邻近搜索和短语搜索)。

         

 

在传统的搜索系统中,通常需要后台运行额外的进程来保持搜索索引的实时更新。而苹果的系统则实现了所有操作的实时性,这意味着一旦数据发生变化,搜索索引会立即得到更新,无需任何额外步骤。这种设计不仅提高了搜索效率,还确保了数据的一致性和时效性,为用户提供更为流畅、准确的搜索体验。


高并发区域


FoundationDB为CloudKit处理同时发生的大量更新提供了更为平滑和一致的方式。

         

 

在以前使用Cassandra时,CloudKit依赖于一个特殊的索引来追踪各个区域内的数据变化以实现跨设备同步。当设备需要更新数据时,会通过检查这个索引来获取最新信息。但这种方法存在一个问题:当多台设备几乎同时进行更新操作时,可能会引发冲突。   

         

 

而采用FoundationDB后,CloudKit利用了一种特殊类型的索引,它可以精确地跟踪每一次更改的顺序,而且不会导致冲突。这种机制是通过为每次变更分配一个唯一的“版本号”来实现的,当CloudKit需要进行同步时,它会根据这些版本号来确定设备错过了哪些更新内容。

         

 

然而,在将数据从一个存储集群转移到另一个存储集群(可能是为了更均匀地分布负载)时,情况变得复杂起来,因为每个集群都有自己独立的、不匹配的版本号。为解决这一问题,CloudKit为每位用户的每份数据赋予了一个称为“化身”的“迁移计数”,每当用户的数据被迁移到新集群时,“化身”值就会递增。每个记录更新都会包含用户当前的“化身”号码,从而确保即使在迁移之后,CloudKit仍可以根据化身号和版本号来正确判断出更新的顺序。

         

 

当CloudKit切换到这个新系统时,面临的挑战之一是如何处理那些尚未带有版本号的老数据。他们巧妙地解决了这个问题,通过运用一种特殊函数,该函数可以先按照旧系统的方式对老的更新进行排序,然后再加入新系统的更新。这意味着无需对应用程序进行复杂的改动或遗留过时代码。此函数综合考虑了化身、版本以及旧的更新计数器值,确保了记录顺序的准确性。


高延迟查询

   

FoundationDB 是为高并发设计的,而非针对低延迟。这意味着它能够同时处理大量任务,而不是专注于单个任务的速度。


来源:《FoundationDB Record Layer: 开源结构化存储》

         

 

为了充分利用这种设计,Record Layer在处理任务时采用了大量的异步操作方式——它会将任务排队等待未来完成,期间可以继续进行其他工作。这种方法有助于掩盖这些任务执行过程中可能出现的延迟。

         

 

   

然而,FoundationDB用于与数据库通信的工具最初是采用单线程模式设计,一次只做一件事并使用一个网络线程。在早期版本中,这种设置导致了系统内部的拥堵,因为所有任务都在等待轮到自己在这条网络线程上执行。Record Layer也沿用了这种单线程处理方法,这导致了性能瓶颈。

         

 

为了解决这一问题,苹果公司着手减轻这条网络线程的工作负载。现在,通过让系统同时从多个角度与数据库协同工作,而非形成单一的任务队列,使得复杂的任务看上去执行速度更快。这样一来,由于系统无需等待一个任务完成后再开始另一个任务,所以延迟或所谓的“缓慢感”被有效地隐藏起来。


冲突事务


在FoundationDB中,如果一个事务正在读取某些键值,而另一个事务在同一时刻修改了这些相同的键值,则会导致“事务冲突”。FoundationDB通过提供对可能导致冲突的键集合进行精确控制的能力,从而允许精细管理这些冲突。

         

 

避免不必要的冲突的一个常见方法是对一组键执行一种特殊的、不会引发冲突的读取操作,即所谓的“快照”读取。如果这种读取发现重要键值,那么事务只会针对那些特定键标记潜在冲突,而不是整个键范围。这样可以确保事务只受到与其结果真正相关的更改影响。

         

 

Record Layer采用了这一策略来高效地管理其排名索引系统中的一部分结构——跳表(skip list)。然而,手动设置这些冲突范围可能较为复杂,并且可能导致难以识别的错误,特别是当它们与应用程序的主要逻辑混合在一起时。因此,建议构建于FoundationDB之上的系统创建更高级别的工具,如自定义索引,以处理这些模式。这种方法有助于避免将放宽冲突规则的责任留给每个客户端应用,否则可能会导致错误和一致性问题的发生。


参考链接:

https://read.engineerscodex.com/p/how-apple-built-icloud-to-store-billions

https://www.foundationdb.org/files/record-layer-paper.pdf


——好文推荐——

大模型“藏毒”:“后门”触发,猝不及防!

加码Copilot!微软高歌猛进,个人用户享有GPT-4 Turbo的优先访问权


继续滑动看下一个

Apple 如何构建 iCloud 来存储数十亿个数据库

向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存