分布式系统学习小记11 —— FARM 与 OCC

“Hi,这是小5自学算力大集群相关知识的第11篇笔记。在之前我们简单认识了Spanner方案,今天我们将对比其与FARM方案的区别,并展开聊聊乐观并发控制。”

摘要

作为分布式方案的一种,FARM 与 Spanner 都实现了复制,并且它们使用两阶段提交来处理事务,但两者所侧重的方向并不一样。


对于 Spanner :

其主要关注点是地理复制,即能够在东西海岸以及不同的数据中心拥有副本,并能够进行涉及多个不同地点数据的高效交易。它最具创新之处在于,为了尝试解决长距离进行两阶段提交所需时间的问题,它为只读事务设置了一条特殊的优化路径并使用同步时间。在Spanner系统中,人们主要担忧的瓶颈是网络速度,即数据中心之间的网络延迟。

对于 FARM :

它假设所有副本都位于同一数据中心。所以,它并非试图解决Spanner所面临的问题,即:如果整个数据中心发生故障,将会发生什么情况?其容错能力主要体现在应对个别故障或尝试在整座数据中心断电并恢复供电后进行恢复。它采用了RDMA技术,但RDMA在某种程度上也限制了方案整体的设计选项。因此,FARM不得不采用乐观并发控制(OCC)。也正是因为采用RDMA技术,其性能远远高于Spanner。在FARM系统中,瓶颈是服务器的CPU时间,因为他们通过将所有副本放置在同一数据中心,在某种程度上忽略了网络延迟的影响。


  • FARM基本架构

数据根据键被分片并分散在多个主、备份对中,如P1、B1;P2、B2;… 这意味着,每次更新数据时,都需要同时在主服务器和备份服务器上进行更新。进行读取操作时,始终需要从主节点读取。此外,每个客户端不仅执行事务,还充当两阶段提交的事务协调者。

  • FARM容错性

只要给定分片的一个副本可用,那么该分片就会保持可用状态。因此只需要一个活跃副本,而非多数。如果在数据中心范围内发生停电,只要系统中每个分片至少有一个副本,它就能恢复。即,拥有F + 1 的副本,那么它们能够容忍该分片最多 F 个故障。

  • FARM如何实现高性能

最主要方式是通过分片(sharding)来实现。在实验中,将数据分片至90台服务器,只要不同分片中的操作彼此或多或少独立,就能自动实现90倍的速度提升,这是因为可以将正在运行的任务并行运行在90台服务器上。

还有一种提高性能的方法是“内核旁路” (kernel bypass) 。其理念是,应用程序不再需要将其所有数据通过复杂的内核代码栈向下发送,而是由内核配置计算机的保护机制允许应用程序直接访问网络接口卡。这意味着应用程序级代码可以直接访问网络接口卡(Network Interface Card, NIC),而无需内核介入。

第三个技巧是确保所有数据都能适配于服务器的RAM中:它们实际上并不将数据存储在磁盘上。之所以可以这么操作是因为使用了某种“非易失性RAM”,确保断电后的数据得以保存。为什么要使用引号呢?让我们来揭晓:当客户端事务更新某条数据时,其真正含义是它向存储该数据的相关服务器发出请求,并促使这些服务器在内存(RAM)中直接修改该事务所涉及的对象。数据中心会在每个机架中放置一个大电池,通过这些电池运行电源供应系统,这样一旦发生电源故障,电池就会自动接管并保持所有机器运行,至少持续到电池耗尽为止。

当电池系统检测到主电源故障时,它会在维持服务器运行的同时,向所有服务器发出警报。通过某种中断或消息告知它们:“注意,电源刚刚失效。你们只剩下10分钟时间,电池也将耗尽。”在这个阶段,FARM服务器上的软件会复制所有排名,停止对FARM的所有处理,然后服务器都会将其全部RAM复制到连接到该服务器的固态驱动器上,这个过程可能需要几分钟。一旦所有RAM数据复制到固态硬盘中,机器便会自行关闭并断电。

如果存在其他原因导致服务器故障,例如硬件出现问题,或者软件中的某个错误引发崩溃,这些崩溃与“非易失性RAM”系统无关。这些崩溃将导致机器重启,并丢失其RAM中的肉容,且无法恢复这些内容。这种NV-RAM方案实质上消除了持久化速率作为系统性能瓶颈的问题,仅留下网络和CPU作为性能瓶颈。

第四个技巧就是前面我们所说的RDMA技术。即,远程直接内存访问(Remote Direct Memory Access)。两台通过支持RDMA网卡连接的服务器甚至可以直接访问、修改对方的内存数据。其本质上是一种精巧的网络接回卡,能够接收指令包。使得网络接口卡能够直接读写服务器内存,而无需中断服务帮。

如何将RDMA与事务处理、分片和复制相结合?因为要构建一个真正实用的数据库系统,分片和事务复制是不可或缺的。如果采用两阶段提交中的事务,服务器上的数据不仅仅是数据,还包括已提交的数据以及已写入但尚未提交的数据。同样,传统上,服务器负责判断最近更新的数据是否已提交,并保护客户端,防止他们看到被锁定或尚未确认已提交的数据。这就需要使用到乐观并发控制。


  • OCC (Optimistic Concurrency Control)

在乐观并发控制中可以无锁地进行读取操作。同时也不会直接写入数据,而是将其缓存,即在客户端本地缓存写入操作,直至事务最终结束。

当交易最终完成,并尝试提交时,会经历一个被称为验证阶段的环节,在此阶段,交易处理系统会试图确定执行的实际读取和写入操作是否与可串行性保持一致。如果而非一致则不提交对应事务,反之则进行提交。如果在读取或写入数据时,其他事务也在同时修改该数据,那么乐观方案会在此时中止,已经读取了本不应读取的损坏数据。

在执行阶段,因为写入新内容前,都需要先进行读取。因此,不论是写操作还是读取操作,在执行阶段都需要进行读取。读取操作会获取版本号,即初始版本号。当交易调用tx-commit以表明其完全完成时,客户端上的tx-commit调用中的库充当事务协调器。

在提交阶段

第一阶段,事务协调器发送一系列锁定消息并等待其回复。随后发送验证消息并等待所有回复。即,提交协议的第一个阶段是锁定阶段。

对于客户端所编写的每个对象,需要将该更新后的对象发送给相应的主节点。因此,它将更新后的对象发送给主节点,并作为该客户端在主节点日志中的一个新的目志条目。客户端利用RDMA技术向主节点的日志进行追加操作。其附加的内容,是客户端想要写入的对象ID、客户端最初读取该对象时获取的版本号,以及新值。这些信息需要提交给每个分片的主节点登录,以便在每个分片中写入一个对象。当这一步骤完成后,当事务协调器收到“好的,没问题”的回复时,这些新的日志记录位于主节点的日志中。

主节点必须主动处理这些日志条目,因为它需要进行一系列验证检查,以确定该主节点在事务中的部分是否允许提交。此时,需等待每个主节点在其内存中轮询该客户端的日志,发现新的目志条目,并处理该新条目。随后发送一个“是”或“否”的投票,以表明其是否愿意执行交易的一部分。

当主节点在其轮询循环中检测到来自客户端的某个传入锁日志条目时。若该对象(具有对象ID)当前已被锁定,则主节点将拒绝此锁定消息,并通过RDMA向客户端发送消息,告知“不,此事务无法获准进行”。如果该对象未被锁定,那么主节点接下来要做的是检查版本号,确保客户端发送的版本号(即客户端最初读取的版本号)未发生更改。如果版本号发生了变化,那就意味着在事务读取和写入之间有其他人在版本号变化时对该对象进行了写操作。如果版本号相同且锁位未设置,那么主节点将设置锁并返回一个肯定的响应给客户端。

由于主节点是多线程运行在多个CPU上。旦可能存在其他事务,因此可能会有其他CPU同时从同 一主节点上的其他客户端读取其他传入的日志队列。不同事务之间或尝试修改同一对象的不同事务的锁记录处理之间可能存在竞态条件。因此,主节点实际上使用了一条原子指令,即比较并交换指令,以原子操作的方式同时检查版本号、锁定并设置该版本号上的锁定位。这便是为何锁位必须位于版本号的高位,以便单一指令即可执行版本号与锁位的比较和交换操作。

如果对象已经被锁定,则不会发生阻塞。主节点仅在其他某个事务已锁定该资源时返回一个否定响应。

第二阶段,客户端作为事务协调者。它等待来自所有主节点的响应,对于事务修改的每个对象,它都会从分片的主节点获取响应。如果其中任何一个节点拒绝该事务,那么事务协调器将终止整个事务,并向所有主节点发送消息,表明其改变了主意。如果所有主节点都表示同意,那么交易协调器就会认为该交易可以提交,接着事务协调器通知所有的主节点:所有主要节点都投了赞成票。

一旦事务协调器确认所有主节点都已知晓事务提交,便可通知所有主节点,它们可以丢弃该事务的所有日志条目。

关于验证(validate)阶段

在提交的两阶段中间其实还有一个验证阶段。验证阶段是对仅被事务读取而未被写入的对象进行优化处理的一种机制。事务协调者能够通过极其迅速的单边读取执行验证,而无需将信息写入日志并等待主节点查看日志条目并进行思考。其实质上取代仅用于读取对象的锁,并且速度将大大提升。验证过程涉及事务协调器重新获取对象头信息。在提交时,它并非发送锁定消息,而是重新获取对象头部,并检查当前版本号是否与首次读取该对象时的版本号相同。同时,它还会检查锁位是否为清空状态。

乐观并发控制能提供可串行化的本质是在检查实际发生的执行是否与一次一个事务的执行相同。如果没有冲突的交易,那么版本号和锁位就不会发生变化,将看到事务结束时的版本号与首次读取对象时所见的版本号相同。如果在读取对象与尝试提交更改之间存在冲突事务。并且该冲突事务修改了某些内容,那么一旦它开始提交,我们将会看到一个新的版本号或锁定位被设置。因此,在首次读取对象与最终提交之间,对版本号和锁位进行比较。可以判断在使用这些对象期间,是否有其他对该对象的提交悄悄介入。

这种乐观方案允许我们在首次使用数据时无需检查锁,从而能够利用极快的单边RDMA读取方式来获取数据,实现高性能。


以上内容来自对 MIT 《分布式系统 6.824》的学习笔记,在此特别声明。