作者 | Engineer's Codex
译者 | 刘雅梦
策划 | Tina
在过去的几个月里,我写了各种关于大型科技公司“幕后”技术的文章,比如 Meta 的内部无服务器(serverless)平台和谷歌内部喜爱的代码审查工具。
然而,苹果的基础设施并不公开。我想知道苹果是如何构建 iCloud 的,这篇文章涵盖了我所知道的一切。
苹果将 FoundationDB 和 Cassandra 用于其云后端服务 iCloud 和 CloudKit。是的,标题并没有错:苹果确实在其极端的多租户架构中存储了数十亿个数据库。
现实世界中永恒的教训
在开始阅读之前,先看下这些适用的经验教训和指导方针。
我发现,本文将要讨论的苹果的许多经验教训与 Meta 无服务器平台架构 的经验教训非常相似。
Cassandra 是一个宽列 NoSQL 数据库管理系统。它最初是由 Facebook 开发,用于支持 Facebook 收件箱的搜索功能。有趣的是,Meta 自己已经用 ZippyDB 取代了大部分 Cassandra 的使用。
iCloud 部分是由 Cassandra 提供支持的。DataStax 的数据显示,苹果拥有世界上最大的 Cassandra 部署之一。
报告显示:
来源 (https://twitter.com/erickramirezau/status/1578063811495477248)
iCloud 中 Cassandra 的其他分片显示,它管理着 EB 级的数据。每台服务器有多个 Cassandra 节点,苹果的团队在控制爆炸半径和分片方面非常聪明。这确保了 iCloud 数据的可用性接近 100%。
苹果仍在积极改进 Cassandra。苹果的 Scott Andreas 上个月就 Cassandra 的未来做了一次演讲。在苹果的招聘页面上,当招聘分布式系统工程师时,他们通常会提到 Cassandra。
然而,CloudKit + Cassandra 遇到了两个可扩展性限制,这导致他们采用了 FoundationDB。
FoundationDB 和 Record Layer 解决了这两个问题。
FoundationDB
苹果对 FoundationDB 的公开程度要高得多。他们于 2015 年收购了 FoundationDB,并发表了多篇论文,详细介绍了他们对 FoundationDB 的使用情况。
FoundationDB 是一个开源、分布式、事务性的键值对存储。它旨在处理大量的数据,并适用于读 / 写工作负载和写入密集型工作负载。它也符合 ACID。
苹果在 CloudKit(苹果的云后端服务)中广泛使用了 FoundationDB Record Layer。
来源:FoundationDB Record Layer:开源结构化存储 (https://www.youtube.com/watch?v=HLE8chgw6LI)
根据 GitHub 的介绍:
Record Layer 是一个 Java API,它在 FoundationDB 之上提供了面向记录的存储,(非常)大致相当于一个简单的关系数据库,其特点是: 结构化类型——记录是根据 protobuf(Protocol Buffer)消息定义和存储的。Protocol Buffer 最初是由谷歌设计的。 索引——Record Layer 支持各种不同的索引类型,包括值索引(大多数数据库提供的类型)、排序索引和聚簇索引。索引和主键可以通过 protobuf 选项定义,也可以通过编程方式定义。 复杂类型——支持复杂类型,如列表和嵌套记录,包括针对此类嵌套结构定义索引的能力。 查询——Record Layer 不提供查询语言,但它提供了查询 API,该 API 能够扫描、过滤和排序一种或多种记录类型,以及能够自动选择索引的查询规划器。 多记录存储,共享模式——Record Layer 提供了支持许多离散记录存储实例的能力,所有实例都具有共享(和不断发展的)模式。例如,与其为存储所有用户数据的单个数据库建模,不如为每个用户提供自己的记录存储,也许可以在不同的 FDB 集群实例中进行分片。 非常轻量级——Record layer 旨在用于大型、分布式、无状态的环境。打开存储和第一次查询之间的时间以毫秒计。 可拓展——新的索引类型和自定义索引键表达式可以动态地合并到记录存储中。
在 FoundationDB Record Layer 的论文中,他们写道:
“[FoundationDB Record Layer 用于] 为服务于数亿用户的应用程序提供强大的抽象。CloudKit 使用 Record Layer 来承载数十亿个独立的数据库,其中许多数据库具有通用模式。”
为什么使用 FoundationDB Record Layer?
FoundationDB、Record Layer 和 CloudKit 的结构如下所示:
来源:FoundationDB Record Layer:开源的结构化存储
Record Layer 允许苹果大规模支持多租户。
事实上,这有点低估了它。
Record Layer 用于极端多租户,其中每个应用程序的每个用户都可以获得独立的记录存储。这意味着 Record Layer 承载着数十亿个独立的数据库,共享数千个模式。
那就更好了!而且更令人印象深刻。
来源:FoundationDB Record Layer:开源的结构化存储
由于两个基本的架构决策,Record Layer 被设计用于处理如此大规模的多租户。
这是一个很好的切入点,可以让我们粗略地了解一下苹果是如何构建 iCloud 的。
如果你对 CloudKit、FoundationDB 和 Record Layer 的相关技术感兴趣,请继续阅读。
CloudKit 如何使 FoundationDB 和 Record Layer
来源:FoundationDB Record Layer:多租户结构化数据存储
在 CloudKit 中,应用程序由“逻辑容器”表示,该容器遵循已定义的模式。该模式概述了必要的记录类型、字段和索引,以实现高效的数据检索和查询。应用程序将其数据组织到 CloudKit 内的“区域”中,这允许对记录进行逻辑分组,以便与客户端设备进行选择性同步。
对于每个用户,CloudKit 在 FoundationDB 中指定一个唯一的子空间。在这个子空间中,它为用户与之交互的每个应用程序创建一个记录存储。从本质上讲,CloudKit 管理着大量的逻辑数据库(将用户数量乘以应用程序数量),每个数据库都包含自己的一组记录、索引和元数据,总计数十亿个数据库。
当 CloudKit 收到来自客户端设备的请求时,它会通过负载平衡将该请求定向到可用的 CloudKit 服务进程。然后,该进程与特定的 Record Layer 记录存储进行交互来满足请求。
CloudKit 将定义的应用程序模式转换为 Record Layer 内的元数据定义,该元数据定义存储在单独的元数据存储中。此元数据通过特定于 CloudKit 的系统字段来进行扩充,这些字段跟踪记录的创建、修改时间以及存储记录的区域。区域名称以主键为前缀,以便能够有效地访问每个区域内的记录。除了用户定义的索引外,CloudKit 还管理着用于内部目的的“系统索引”,例如通过保留按记录类型跟踪记录大小的索引来管理存储配额。
FoundationDB 和 Record Layer 一起为苹果解决了 4 个关键问题,这些问题是单独使用 Cassandra 或单独使用 FoundationDB 无法解决的。
已解决的问题:个性化全文搜索
FoundationDB 帮助用户解决了个性化全文搜索的问题,让用户能够快速访问数据。
他们的系统利用 FoundationDB 的键顺序,可以快速搜索文本的开头(前缀匹配),也可以进行更复杂的搜索(例如查找靠近或按特定顺序排列的单词——邻近度和短语搜索),而无需额外的开销。
在传统的搜索系统中,你通常需要在后台运行额外的进程来保持搜索索引的最新状态,但苹果的系统会实时执行所有操作,这意味着一旦数据发生变化,搜索索引就会立即更新,不需要额外的步骤。
已解决的问题:高并发区域
借助 FoundationDB,CloudKit 可以顺利地处理同时发生的许多更新。
之前,在使用 Cassandra 时,CloudKit 曾经依赖一个特殊的索引来跟踪每个区域中的更新,从而在设备之间同步数据。当设备需要更新其数据时,它会检查该索引以查看新内容。但这个系统有一个缺点:当多个更新同时发生时,它可能会导致冲突。
但借助 FoundationDB,CloudKit 使用了一种特殊的索引来跟踪每次更新的确切顺序,而不会导致冲突。这是通过为每个更新分配一个唯一的“版本”来完成的,当 CloudKit 需要同步时,它会查看这些版本,以找出设备错过了哪些更新。
然而,当 CloudKit 需要在不同的存储集群之间移动数据时(也许是为了更均匀地分配负载),事情就变得棘手了,因为每个集群都有自己的版本号,而这些版本号并不匹配。为了解决这个问题,CloudKit 为每个用户的数据提供了一个“移动计数”(称为“化身”),每当他们的数据被转移到一个新的集群时,移动计数就会增加。每个记录更新都包括用户当前的“化身”编号,确保即使在移动后,CloudKit 仍然可以通过查看化身号和版本号来确定正确的更新顺序。
当他们切换到这个新系统时,CloudKit 面临着处理不包含这些版本号的旧数据的挑战。他们巧妙地克服了这一点,通过使用一个特殊的函数,在新的更新之前使用以前的系统对旧的更新进行排序。这意味着不会对应用程序进行复杂的更改,也不会留下过时的代码。该函数考虑了化身、版本和旧的更新计数器值,以维护记录的正确顺序。
已解决的问题:高延迟查询
FoundationDB 是为高并发而非低延迟而设计的。这意味着它可以同时处理很多任务,而不是关注单个任务的速度。
来源:FoundationDB Record Layer:开源的结构化存储
为了充分利用这种设计,Record Layer 的许多工作都是“异步”完成的——它将待完成的任务排在队列中,允许其他工作在此期间完成。这种方法有助于掩盖在这些任务中可能出现的任何延迟。
然而,FoundationDB 用于与其数据库通信的工具被设计为使用单个线程进行网络连接,每次只做一件事。在早期版本中,这种设置会导致系统中的流量堵塞,因为这个网络线程中的所有东西都在等待被轮询。Record Layer 一直在使用这种单线程方式,这导致了瓶颈。
为了改善这一点,苹果减少了该网络线程的工作负载。现在,复杂的任务似乎更快了,因为系统同时在多个前端处理数据库,而不是形成队列。通过这种方式,延迟或明显的缓慢被隐藏起来了,因为系统不会等到一个任务完成后再开始另一个任务。
已解决的问题:冲突的事务
在 FoundationDB 中,如果一个事务正在读取某些键,而另一个事务同时在修改这些键,就会导致“事务冲突”。FoundationDB 通过提供对读写时可能导致这些冲突的键集控制,允许对这些冲突进行精确地管理。
避免不必要冲突的一种常见方法是对一系列键执行一种不会引起冲突的特殊读取,称为“快照”读取。如果这个读取找到了重要的键,则事务将只标记这些特定的键是否存在潜在冲突,而不是标记整个范围。这确保了事务只受对其结果真正重要的更改的影响。
Record Layer 使用这种策略来有效地管理一个被称为跳表的结构,该结构是其排序索引系统的一部分。然而,手动设置这些冲突范围可能很棘手,并可能导致难以识别的错误,尤其是当它们与应用程序的主要逻辑混合在一起时。因此,建议构建在 FoundationDB 之上的系统创建更高级别的工具,比如自定义索引,来处理这些模式。这种方法有助于避免将放宽冲突规则的责任留给每个客户端应用程序,这可能会导致错误和不一致。
原文链接:
https://read.engineerscodex.com/p/how-apple-built-icloud-to-store-billions
声明:本文为 InfoQ 翻译,未经许可禁止转载。