【引子】周末阅读时光,一篇好的论文(https://cacm.acm.org/magazines/2023/6/273229-foundationdb-a-distributed-key-value-store/fulltext),开阔了眼界,支持事务语义的NoSQL应该放到软件系统架构备选方案之中。
FoundationDB是一个开源的事务性键值存储系统,是最早将NoSQL架构的灵活性和可扩展性与ACID事务的强大性能相结合的系统之一。FoundationDB架构解耦成一个内存中的事务管理系统、一个分布式存储系统和一个内置的分布式配置系统。每个子系统都可以独立地进行配置,以实现可扩展性、高可用性和容错性。
FoundationDB还包括了一个确定性仿真框架,用于在可能的故障情况下测试新的功能。这种严格的测试使FoundationDB更加稳定,并允许开发人员以快速的节奏引入和发布新功能。
同时,FoundationDB提供了一个最小的、精心挑选的功能集,可以在FoundationDB上构建不同的系统。其强大的数据一致性、健壮性和可用性,使之成为苹果、Snowflake和其他公司云基础设施的基础,用于存储用户数据、系统元数据和配置以及其他关键信息。
1. 背景信息
1.1 当前NoSQL解决与面临的问题
许多云服务依赖于可扩展的分布式存储后端来持久化应用程序状态。这种存储系统必须具有容错性和高可用性,并且同时提供足够强的语义和灵活的数据模型,以便快速进行应用程序开发。这些服务必须支持能够扩展到数十亿用户,存储的数据量为PB或EB,每秒处理数百万个请求。
NoSQL系统的出现,提供了应用程序开发的简便性,使得扩展和操作存储系统变得简单,并提供了容错性,并支持各种数据模型。为了可扩展性,这些NoSQL系统牺牲了事务语义,而提供了数据的最终一致性,迫使应用程序开发人员考虑并发操作的数据更新问题。
1.2 FoundationDB的由来与特点
FoundationDB是在2009年创建的,希望成为构建高级分布式系统所需的基础系统。它是一个有序的、事务性的、键值存储,本地支持其整个键空间的多键严格序列化事务。它提供了一个高度可扩展的、事务性的存储引擎,具有精心选择的最少功能集。它不提供结构化语义、查询语言、数据模型、二级索引或许多其他在事务性数据库中通常找到的功能。
NoSQL模型为应用程序开发人员提供了很大的灵活性。应用程序可以将数据存储为简单的键值对,但需要实现更高级的功能,例如一致性二级索引和引用完整性检查。FoundationDB默认为严格可序列化事务,但允许在细粒度控制下放松这些语义,以适应不需要这种事务的应用程序。
FoundationDB的流行和日益增长的开源社区之一的原因是它专注于数据库的"下半部分",将其余部分留给上面的无状态应用程序来提供各种数据模型和其他功能。在FoundationDB上构建的各种层证明了这种不寻常设计的有用性。例如,FoundationDB记录层添加了用户从关系数据库中期望的大部分内容,图数据库JanusGraph提供了一个基于FoundationDB层的实现。CouchDB正在重新构建为FoundationDB的一个层。因此,传统上的应用程序可以同样使用FoundationDB。
测试和调试分布式系统与构建一样困难。意外的进程和网络故障、消息重新排序和其他非确定性的来源可能会暴露出隐含的假设,这些假设在现实中会被破坏,这些错误非常难以重现或调试。这些错误对于明确的数据库系统往往是致命的。此外,数据库系统的有状态性质意味着任何这样的错误都可能导致数据损坏,但或许可能需要几个月才能发现。模型检查技术可以验证分布式协议的正确性,但往往无法检查实际实现。深层次的漏洞,只有在特定顺序的多个崩溃时才会发生,对端到端测试构成了挑战。
FoundationDB采取了一种激进的方法------在构建数据库本身之前,构建了一个确定性的数据库仿真框架,可以模拟相互作用的进程网络和各种磁盘、进程、网络和请求级故障和恢复,所有这些都在一个物理进程内完成。专门为此目的创建了C++的语法扩展Flow。这种在模拟中的严格测试使得FoundationDB非常稳定,并允许其开发人员以快速的节奏引入新的功能和发布。
FoundationDB的松耦合架构由控制平面和数据平面组成。控制平面管理集群元数据,并使用Active Disk Paxos来实现高可用性。数据平面由事务管理系统和分布式存储层组成,前者负责处理更新,后者负责提供读取;两者可以独立扩展。FoundationDB通过乐观并发控制和多版本并发控制的组合实现了严格的串行化。
FoundationDB与其他分布式数据库不同的一个特点是其处理故障的方法。FoundationDB不依赖于仲裁机制,而是尝试通过重新配置系统来积极检测和恢复故障。这使得我们可以在资源更少的情况下实现相同级别的容错性:FoundationDB可以容忍n个故障,而只需要n+ 1(而不是2n + 1)个副本。这种方法适合于本地或大区部署。对于广域网部署提供了一种新颖的策略,避免跨区域写入延迟,同时在区域之间提供自动故障转移,而不会丢失数据。
2. 设计原则与系统架构
FoundationDB的主要设计原则是分而治之、面向故障的设计和仿真测试。
FoundationDB将事务管理系统(写)与分布式存储系统(读)解耦,并独立地扩展它们。在事务管理系统中,进程被分配为代表事务管理的不同方面的各种角色。此外,集群范围内的编排任务,如过载控制和负载平衡,也被划分并由其他不同的角色提供服务。
对于分布式系统而言,故障是一种必然而非例外。为了应对事务管理系统中的故障,需要恢复处理所有故障:当检测到故障时,事务系统主动关闭。因此,所有故障处理都被简化为单个恢复操作,这成为了常见的、经过充分测试的代码路径。为了提高可用性,FoundationDB努力将平均恢复时间(MTTR)最小化。在我们的生产集群中,总时间通常不超过五秒。
FoundationDB依赖于一种随机、确定性的模拟测试框架,用于测试其分布式数据库的正确性。模拟测试框架不仅暴露深层次的错误,而且提高了开发人员的生产力和FoundationDB的代码质量。
2.1. 架构
FoundationDB集群具有用于管理关键系统元数据和群集范围编排的控制面板,以及用于事务处理和数据存储的数据面板,如下图所示。
控制平面
控制平面负责将关键系统元数据(即事务系统配置)持久化在协调器上。这些协调器形成一个Paxos组,并选举出一个集群控制器。集群控制器监控集群中的所有服务器,并维护三个进程:序列器、数据分发器和速率控制器。如果它们失败或崩溃,则这些进程会重新启动。数据分发器负责监控故障并平衡存储服务器之间的数据。速率控制器为集群提供过载保护。
数据平面
FoundationDB适用于读多写少、每个事务读写少量关键字、需要可扩展性的OLTP工作负载。分布式事务管理系统由序列器、代理和分区范围解析器组成,所有这些都是无状态进程。日志系统存储TS的写前日志,而单独的分布式存储系统用于存储数据和提供读取服务。日志系统包含一组日志服务器,而分布式存储系统具有多个存储服务器。序列器为每个事务分配读取和提交版本。代理为客户端提供多版本读取并协调事务提交。解析器检查事务之间的冲突。日志服务器充当复制、分片和分布式持久队列,每个队列存储一个存储服务器的WAL数据。分布式存储系统由多个存储服务器组成,每个存储服务器存储一组数据分片,即连续的键范围,并提供客户端读取。存储服务器是系统中大部分进程,并且它们一起形成分布式B树。每个存储服务器上的存储引擎是SQLite的增强版本,其中增强使范围清除更快,将删除推迟到后台任务,并添加了异步编程支持。
2.1.1 读写分离和扩展
上述进程被分配为不同的角色,通过为每个角色添加新的进程来进行扩展。客户端从分片的存储服务器中读取,因此读取随着存储服务器的数量线性扩展。通过添加更多的代理、解析器和日志服务器来扩展写入。控制平面的单例进程(例如集群控制器和序列器)和协调器不是性能瓶颈;它们只执行有限的元数据操作。
2.1.2 引导启动
FoundationDB没有对外部协调服务的依赖。所有用户数据和大部分系统元数据都存储在存储服务器中。有关存储服务器的元数据存储在日志服务器中,并且日志服务器的配置数据存储在所有协调器中。协调器是一个磁盘Paxos组;如果不存在集群控制器,则服务器会尝试成为集群控制器。新选举的集群控制器从协调器中读取旧的LS配置,并生成新的事务服务器和日志服务器。代理从旧的LS中恢复系统元数据,包括有关所有存储服务器的信息。序列器等待新的事务服务器完成恢复,然后将新的日志服务器配置写入所有协调器。新的事务系统随后准备好接收客户端事务。
2.1.3 重新配置
序列器进程监视代理,解析器和日志服务器的健康状况。每当日志服务器或日志服务器出现故障,或数据库配置更改时,序列器将终止。集群控制器检测到序列器故障,然后启动并引导新的事务服务器和日志服务器。通过这种方式,事务处理被分为各个时期,每个时期代表一个具有自己序列器的事务管理系统的生成。
2.2. 事务管理
2.2.1 端到端的事务处理
客户端事务首先通过联系其中一个代理来获取读版本(即时间戳)。代理然后请求序列器 生成至少与先前发出的所有事务提交版本一样的读版本,并将此读版本发送回客户端。然后,客户端可以向存储服务器发出读取并在特定读版本下获取值。客户端写入被本地缓存而不与群集联系,事务的数据库查找结果与未提交的写入组合以保留读取。在提交时,客户端将事务数据发送到其中一个代理,并等待提交或中止响应。如果事务无法提交,客户端可以选择重新启动它。
代理以三个步骤提交客户端事务。首先,它联系序列器 以获得大于任何现有读版本或提交版本的提交版本。序列器通过每秒最高100万个版本的速率选择提交版本。然后,代理将事务信息发送到分区范围解析器,后者通过检查读写冲突来实现FoundationDB的乐观并发控制。如果所有解析器都没有冲突,则事务可以进入最终提交阶段。否则,代理将事务标记为已中止。最后,提交的事务被发送到一组日志服务器进行持久化。在所有指定的日志服务器都回复代理之后,事务被视为已提交,代理将提交的版本报告给序列器然后回复客户端。存储服务器不断地从日志服务器拉取变异日志,并将已提交的更新应用到磁盘上。
除了上述的读写事务 ,FoundationDB还支持只读事务 和快照读取,其中的只读事务既可以串行化(在读取版本时发生)又高效,客户端可以在不与数据库联系的情况下本地提交这些事务。FoundationDB中的快照读取通过减少冲突来选择性地放宽事务的隔离属性,即并发写入不会与快照读取冲突。
2.2.2 严格串行化
FoundationDB通过将优化并发控制与多版本控制相结合来实现可串行化快照隔离。回想一下,事务Tx 从序列器获取它的读取版本和提交版本,其中读取版本号保证不小于Tx 启动时的任何提交版本,而提交版本大于任何现有的读取或提交版本号。这个提交版本定义了事务的串行历史,并用作日志序列号(LSN)。因为Tx观察到了所有先前提交的事务的结果,FoundationDB实现了严格的串行化。为了确保日志序列号之间没有间隙,序列器在每个提交版本中返回前一个提交版本。代理将LSN和前一个LSN发送给解析器和日志服务器,以便它们可以按照LSN的顺序串行处理事务。
类似地,存储服务器按增加的LSN顺序从日志服务器提取日志数据。分区范围解析器使用类似于写入快照隔离的无锁冲突检测算法,不同之处在于在FoundationDB中选择提交版本之前进行冲突检测。这使得FoundationDB可以高效地批量处理版本分配和冲突检测。整个键空间被划分在分区范围解析器之间,允许并行执行冲突检测。只有当所有的分区范围解析器都承认事务时,事务才能提交。否则,事务将被中止。有可能一个被中止的事务被一部分分区范围解析器承认,并且它们已经更新了可能提交的事务的历史记录,这可能导致其他事务发生冲突(即假阳性)。
在实践中,这对于生产环境的工作负载来说并不是问题,因为事务的键范围通常属于一个分区范围解析器。此外,由于修改后的键在多版本控制窗口后会过期,因此这样的假阳性只会在短暂的多版本控制窗口时间内发生(即5秒)。FoundationDB的优化并发控制设计机制避免了获取和释放锁的复杂逻辑,极大地简化了事务服务和存储服务之间的交互。代价是被中止的事务会浪费工作。
在多租户的生产负载中,事务冲突率非常低(小于1%),优化并发控制运行良好。如果发生冲突,客户端可以简单地重新启动事务。
2.2.3 日志协议
在代理决定提交事务后,向所有日志服务器发送消息:变更被发送到负责修改键范围的日志服务器,而其他日志服务器接收一个空消息体。日志消息头包括从序列器获得的当前和先前的LSN以及此代理的最大已知提交版本。日志服务器使日志数据持久化后,会回复给代理,如果所有副本日志服务器都已回复,并且此LSN大于当前KCV,则代理会将其KCV更新为LSN,并将重做日志从LS发送到存储服务器不是提交路径的一部分,而是在后台执行的。
在FoundationDB中,存储服务器将非持久化的重做日志从日志服务器应用到内存索引中。通常情况下,这发生在任何反映提交的读版本被分配给客户端之前,允许服务多版本读取非常低的延迟。因此,当客户端读取请求到达存储服务器时,请求的版本(即最新提交的数据)通常已经可用。如果在存储服务器副本上没有可读的新数据,则客户端会等待数据可用,或者在另一个副本上重新发出请求。
如果两者都超时,客户端可以简单地重新启动事务。由于日志数据已经在日志服务器上持久化,存储服务器可以在内存中缓冲更新,并定期将数据批量持久化到磁盘上,从而提高I / O效率。
2.2.4 事务系统恢复
传统的数据库系统通常采用ARIES恢复协议。在恢复过程中,系统通过将重做日志记录重新应用于相关数据页面来处理从上一个检查点开始的日志记录。这使数据库达到一致的状态;在崩溃期间进行的事务可以通过执行撤销日志记录来回滚。在FoundationDB中,恢复被故意地设计得非常便宜 - 没有必要应用撤销日志条目。这是由于一个极其简化的设计选择:重做日志处理与正常的日志前进路径相同。
在FoundationDB中,存储服务器从日志服务器拉取日志并在后台应用它们。恢复过程从检测故障并招募新的事务系统开始。在旧的日志服务器中的所有数据被处理之前,新的TS可以接受事务。恢复只需要找到重做日志的结尾:在该点(与正常的正向操作相同),存储服务器异步重放日志。
对于每个时期,集群控制器在几个步骤中执行恢复。首先,它从协调器中读取先前的TS配置,并锁定此信息以防止另一个并发恢复。接下来,它恢复先前的TS系统状态,包括有关旧日志服务器的信息,停止它们接受事务,并招募一组新的序列器,代理,解析器和日志服务器。在先前的日志服务器停止并启动新的事务服务器之后,集群控制器将新的事务服务器信息写入协调器。因为代理和解析程序是无状态的,它们的恢复没有额外的工作。相反,日志服务器保存已提交事务的日志,我们需要确保所有这些事务都是持久性的,并且可以由存储服务器检索。恢复旧日志服务器的本质是确定重做日志的结尾,即恢复版本(RV)。滚动撤销日志本质上是在旧的日志服务器和存储服务器中丢弃RV之后的任何数据。图2说明了如何由序列器确定RV。
代理请求日志服务器会搭载其KCV,即此代理已提交的最大LSN,以及当前事务的LSN。每个日志服务器保留收到的最大KCV和持久的版本,它是LogServer持久的最大LSN。在恢复过程中,序列器尝试停止所有m个旧日志服务器,每个响应都包含该日志服务器上的DV和KCV。
假设日志服务器的复制度为k。一旦序列器收到超过m-k个回复,序列器就知道上一个时期已提交的事务达到了所有KCV的最大值,这成为上一个时期的结束版本(PEV)。所有此版本之前的数据都已完全复制。对于当前时期,其起始版本为PEV +1,序列器选择所有DV的最小值作为RV。在[PEV + 1,RV]范围内的日志从上一个时期的日志服务器复制到当前时期的日志服务器,以在日志服务器故障的情况下修复复制度。复制此范围的开销非常小,因为它只包含几秒钟的日志数据。
当序列器接受新事务时,第一个事务是一个特殊的恢复事务,它会通知存储服务器当前RV的值,以便它们可以回滚任何大于RV的数据。当前的FoundationDB存储引擎由一个未版本化的SQLite B树和内存中的多版本重做日志数据组成。只有离开多版本控制窗口(即已提交的数据)的变异才会写入SQLite。回滚只是在存储服务器中丢弃内存中的多版本数据。然后,存储服务器从新的日志服务器中拉取任何大于版本PEV的数据。
2.3. 复制
FoundationDB使用各种复制策略来容忍不同数据的失败。
2.3.1 元数据复制
控制平面的系统元数据存储在协调器上,使用Active Disk Paxos。只要协调器的多数(即大多数)处于活动状态,就可以恢复此元数据。
2.3.2 日志复制
当代理将日志写入日志服务器时,每个分片的日志记录都会同步复制到k = f + 1个日志服务器上。只有当所有k都回复成功持久性后,代理才能向客户端发送提交响应。日志服务器故障会导致事务系统恢复。
2.3.3 存储复制
每个分片(即关键字范围)都异步复制到k = f + 1个存储服务器,称为team。存储服务器通常托管多个分片,以使其数据均匀分布在许多团队中。存储服务器故障会触发数据分配器将数据从包含失败进程的团队移动到其他健康team中。请注意,存储team抽象比Copysets更为复杂。
为了减少由于同时故障而导致的数据丢失的概率,FoundationDB确保在副本组中最多只放置一个进程位于故障域,例如主机、机架或可用区。每个团队都保证至少有一个进程处于活动状态,如果任何一个相应的故障域仍然可用,则不会丢失数据。
2.4 仿真测试
测试和调试分布式系统是一项具有挑战性且效率低下的工作。对于FoundationDB来说,这个问题尤为严重------它的强并发控制合约的任何故障都可以在其上层系统中产生几乎任意的损坏。因此,从一开始就采用了一种雄心勃勃的端到端测试方法:在确定性的离散事件模拟中运行真实的数据库软件,连同随机生成的合成工作负载和故障注入。严酷的模拟环境很快会引发数据库中的错误,并且确定性保证每个这样的错误都可以被重现和调查。
2.4.1 确定性模拟器
FoundationDB从一开始就建立了这种测试方法。所有数据库代码都是确定性的,并且避免多线程并发(相反,每个核心部署一个数据库节点)。下图说明了FoundationDB的模拟器过程,其中抽象了所有的非确定性和通信源,包括网络、磁盘、时间和伪随机数生成器。FoundationDB是用Flow编写的,这是一种新颖的C++语法扩展,添加了类似async/await的并发原语和自动取消,允许高并发代码以确定性方式执行。Flow提供了Actor编程模型,它将FoundationDB服务器进程的各种操作抽象成多个由Flow运行时库调度的actor。模拟器进程能够生成多个FoundationDB服务器,在单个离散事件模拟中通过模拟的网络相互通信。生产实现是到相关系统调用的简单桥接。
模拟器运行多个工作负载,通过模拟网络与模拟的 FoundationDB 服务器通信。这些工作负载包括故障注入指令、模拟应用程序、数据库配置更改和内部数据库功能调用。工作负载是可组合的,以测试各种功能,并被重复使用以构建全面的测试用例。
2.4.2 测试代理
FoundationDB 使用各种测试代理来检测模拟中的故障。大多数合成工作负载内置了断言来验证数据库的合同和属性,例如通过检查数据中的不变量来验证其只能通过事务原子性和隔离性来维护。断言在整个代码库中用于检查可以"本地"验证的属性。像可恢复性(最终可用性)这样的属性可以通过将建模的硬件环境(在足以破坏数据库可用性的故障集之后)返回到应该可能恢复的状态,并验证集群最终恢复来检查。
2.4.3 故障注入
模拟注入机器、机架和数据中心故障和重启、各种网络故障、分区和延迟问题、磁盘行为(例如机器重启时未同步写入的损坏)以及随机化事件。这种故障注入的多样性既测试了数据库对特定故障的弹性,又增加了模拟中状态的多样性。故障注入分布经过精心调整,以避免过高的故障率导致系统进入小状态空间。FoundationDB本身通过一种高级故障注入技术与模拟器合作,使得罕见的状态和事件更加常见,这种技术非正式地称为"buggification"。
在其代码库的许多地方,模拟器允许注入一些不寻常(但不违反契约的)行为,例如在通常成功的操作中不必要地返回错误,注入通常很快的操作的延迟,或选择一个异常值的调整参数等。这与网络和硬件层面的故障注入相辅相成。调整参数的随机化也确保特定的性能调整值不会意外地变得必要以确保正确性。Swarm测试广泛用于最大化模拟运行的多样性。每次运行都使用随机的群集大小和配置、随机的工作负载、随机的故障注入参数、随机的调整参数,并启用和禁用随机子集的buggification点。
我们已经开源了FoundationDB的Swarm测试框架。条件覆盖宏用于评估和调整模拟的有效性。例如,一个开发人员担心新的代码可能很少使用完整的缓冲区,可以添加一行 TEST(buffer.is_full());模拟结果的分析将告诉他们有多少个不同的模拟运行达到了该条件。如果数量过低或为零,他们可以添加buggification、工作负载或故障注入功能,以确保该场景得到充分测试。
2.4.4 发现错误的延迟
快速发现错误对于在生产之前在测试中遇到它们以及提高工程生产力都非常重要,在单个提交中立即发现的错误可以轻松地追溯到该提交。如果模拟器内部的CPU利用率低,则离散事件模拟可以以任意快的速度运行,因为模拟器可以将时钟快进到下一个事件。许多分布式系统错误需要时间才能发现,并且在具有长时间低利用率的模拟中运行可以比"真实世界"端到端测试每个核心发现更多此类错误。此外,随机测试具有令人尴尬的并行性,FoundationDB开发人员可以和确实会在主要发布之前"爆发"测试的数量,以期捕获到迄今为止逃避测试过程的异常稀有的错误。由于搜索空间实际上是无限的,运行更多的测试会导致覆盖更多的代码并发现更多的潜在错误,与脚本化的功能或系统测试形成对比。
2.4.5 仿真测试的局限
仿真无法可靠地检测性能问题,例如不完美的负载均衡算法。它也无法测试第三方库或依赖项,甚至无法测试在Flow中未实现的一方代码。因此,我们大多避免了对外部系统的依赖。最后,关键依赖系统(例如文件系统或操作系统)中的错误或对其约定的误解可能导致FoundationDB中的错误。例如,一些错误是由于真正的操作系统约定比预期的要弱而导致的。
4. 评估方法
使用合成工作负载来评估FoundationDB的性能。具体而言,有四种类型:(1) 盲写,更新配置的随机键的数量;(2) 区间读取,从随机键开始获取配置的连续键的数量;(3) 点读取,获取n个随机键;和(4) 点写入,获取m个随机键并更新另外m个随机键。通过盲写和区间读取来评估写入和读取性能,点读取和点写入一起用来评估混合读写性能。确保数据集无法完全缓存在StorageServers的内存中。
在最大写入吞吐量下,日志服务器的CPU利用率达到饱和状态。对于读取和写入操作,增加事务中的操作数可以提高吞吐量。然而,进一步增加操作数不会带来显著的改变,解析器和代理的CPU利用率也可达到饱和状态。提交请求涉及多个跳和持久化到三个日志服务器,因此延迟比读取和读版本高。批处理有助于保持吞吐量,但由于饱和,提交延迟会激增。
由于面向客户的性质,短暂的重新配置时间对于这些集群的高可用性至关重要。这些短暂的恢复时间是由于它们不受数据或事务日志大小的限制,只与系统元数据大小相关。在恢复过程中,读写事务被临时阻塞,并在超时后重试。然而,客户端读取不会受到影响。导致这些重新配置的原因包括软件或硬件故障的自动故障恢复、软件升级、数据库配置更改以及对生产问题的手动处理。
5. FoundationDB的核心特性
5.1. 架构设计
分而治之的设计原则在实现云部署时起到了重要的作用,使数据库既具备可扩展性又能保持性能优良。
首先,将事务系统与存储层分离使得计算和存储资源能够更加灵活地独立部署和扩展。此外,日志服务器的引入类似于验证副本,在一些多区域生产部署中,日志服务器显著减少了实现高可用性所需的存储服务器(完全副本)的数量。运营人员还可以自由地将FoundationDB的不同角色部署在不同类型的服务器实例上,以优化性能和成本。
其次,这种松耦合的设计使得可以扩展数据库的功能,例如可以用RocksDB替换现有的SQLite引擎。
最后,许多性能改进可以通过将功能专门化为独立的角色来实现的,例如将数据分配器和流频控与序列器分离,添加存储缓存,将代理分为读版本Proxy和提交Proxy。这种设计模式实现了频繁添加新功能和扩展能力的目标。
5.2. 仿真测试
仿真测试使FoundationDB能够以小团队保持高开发速度。这是通过缩短引入错误和发现错误之间的延迟时间,以及允许确定性重现问题来实现的。例如,添加额外的日志不会影响事件的确定性顺序,因此可以确保精确重现。这种调试方法的生产力要比正常的生产环境调试高得多。在极少数情况下,在真实环境中首次发现的错误,调试过程通常会先改进模拟的能力或准确性,直到问题在模拟中可以被重现,然后才开始正常的调试流程。通过模拟进行严格的正确性测试使FoundationDB变得极其可靠。
仿真测试不断地推动可模拟性测试的边界,通过消除依赖并在Flow中来实现。例如,早期版本的FoundationDB依赖于Apache Zookeeper进行协调,已被在Flow中自行实现的全新Paxos替代。
5.3. 快速恢复
快速恢复不仅有助于提高可用性,还极大地简化了软件升级和配置更改,并使其更快速。传统的分布式系统升级方法是进行滚动升级,以便在出现问题时可以回滚。滚动升级的持续时间可能会持续几个小时到几天。相比之下,FoundationDB可以通过同时重新启动所有进程来执行升级,通常在几秒钟内完成。此外,这种升级路径还简化了不同版本之间的协议兼容性问题,无需确保不同软件版本之间的RPC协议兼容性。另外,快速恢复有时可以自动修复潜在的错误,这类似于软件复活技术。
5.4. 五秒的MVCC窗口
FoundationDB选择了一个5秒的多版本并发控制窗口来限制事务系统和存储服务器的内存使用,因为多版本数据存储在解析器和存储服务器的内存中,这限制了事务的大小。这个5秒的窗口对于大多数在线事务处理的使用场景已经足够长了。因此,超过时间限制通常会暴露出应用程序中的低效问题。
对于一些可能超过5秒的事务,很多可以分成更小的事务来处理。例如,FoundationDB的持续备份过程会扫描整个键空间并创建键范围的快照。由于5秒的限制,扫描过程被分成了许多小的范围,以便每个范围可以在5秒内完成。实际上,这是一个常见的模式:一个事务创建了多个任务,每个任务可以进一步划分或在一个事务中执行。FoundationDB在一个叫做"任务桶(TaskBucket)"的抽象中实现了这样的模式,而备份系统在很大程度上依赖于它。
6.小结
FoundationDB是一个为了OLTP云服务而设计的分布式键值存储。其主要思想是将事务处理与日志记录和存储分离。这种解耦的架构使得读写处理的分离和水平扩展成为可能。事务系统结合了乐观并发控制(OCC)和多版本并发控制(MVCC),以确保严格的串行化。日志记录的解耦和事务顺序的确定性极大简化了恢复过程,从而实现了异常快速的恢复时间和提高了可用性。最后,确定性和随机模拟确保了数据库实现的正确性。
【参考资料与关联阅读】
-
FoundationDB. https://github.com/apple/foundationdb.
-
Flow. https://github.com/apple/foundationdb/tree/master/flow.
-
FoundationDB Joshua. https://github.com/FoundationDB/FoundationDB-joshua.
-
Foundationdb storage adapter for janusgraph. https://github.com/JanusGraph/janusgraph-foundationdb.
-
Rocksdb. https://rocksdb.org/.
-
SQLite. https://www.sqlite.org/index.html.
-
CouchDB. https://couchdb.apache.org/.