这篇文章翻译自Stretching Spokes,我加入了一些自己的注解。


GitHub的Spokes系统存储了Git仓库的多个分布式副本。本文讨论了如何把把Spokes为仓库制作的副本分散到相互之间距离很远的的数据中心。

Spokes的背景

GitHub开发了一个名为Spokes的系统来存储我们用户的Git仓库的多个副本,并使副本保持同步。Spokes使用多种策略来确保在大多数情况下每个Git更新都能安全地复制到所有副本,并且在所有情况下至少能够复制到严格多数个副本。Spoke取代了在文件系统块级别进行复制的旧系统,改为在Git应用程序级别进行复制。

Spokes的push操作

每个对Git仓库的push操作都会通过代理,这一代理会透明地将操作复制到多个文件服务器。早期版本的Spoke需要代理与所有副本之间都能够进行低延迟通信,以维持较高的更新速率。因此,副本之间的距离必须比较近。

但是,将仓库副本的位置分离的优点是众所周知的:

  • 副本分散得越开,在影响一片较大地理区域的灾难中(例如飓风、地震和外星人入侵1),就越有可能有副本幸存。
  • 如果多个区域中都有可用的副本,则可以将Git读取请求定向到距离最近的副本,从而减少传输时间。

本文首先解释了为什么延迟会带来问题,我们如何克服问题,使得Git数据能够分布式地存储在整片大陆上,以及这为我们的用户带来了哪些改进。、

副本之间相隔很远。那有什么好大惊小怪的呢?

在开发Spokes之前,我们使用DRBD,对文件系统进行块级复制,以创建仓库副本。该系统对延迟非常敏感,因此我们不得不保证文件服务器副本相互靠近。这显然不够理想,解决这一问题就是最初推动Spokes发展的动力。

自从我们开始运行Spokes之后,我们就开始增加Spokes的仓库副本彼此之间的距离极限。副本之间相距越远,它们之间的延迟就越大。延迟大小限制了Spokes能够为每个仓库维持的Git引用更新(reference update)2的最大速率。

你可能会惊讶,我们居然需要担心这种问题。单个仓库的推送频率不会那么高吧?

嗯,大多数 用户根本不会经常推送。但是如果你托管了近7000万个仓库,你总会发现某些项目使用了你从未预料到的工作流程。我们非常努力,才能保证Github能够为几乎所有的项目提供正常服务,但仍有一些极其荒谬的案例除外。

此外,为了进行内部记录,Github本身也产生了大量的引用更新。例如,每次用户推送一个pull request分支时,我们都必须记录push操作本身,可能需要将该分支同步到目标仓库,为该pull request计算测试merge和测试rebase3,这些操作都会产生引用。如果用户推送到项目的master分支,我们就需要为每一个目标是master的活跃pull request都计算一个测试merge和测试rebase。在某些仓库中,这可能会触发超过一百个引用的更新。

能够对具有高延迟的远程副本进行足够快的引用更新对于Spokes的可用性至关重要。具体来说,我们希望能够支持的每个仓库每秒的更新次数大于1。这意味着每次更新操作的预算只有使用几百毫秒。请记住,无论我们采用何种方法优化写操作,都不能因此减慢读操作的速度,因为读操作和写操作数量之比约为100:1。

减少往返次数

考虑到光速有限之类的烦心事,每次到副本的往返通信都需要时间。例如,横跨美国大陆的一次网络往返通信需要60-80毫秒。多往返几次就会耗尽我们的时间预算。

我们使用三阶段提交4来更新副本,同时将副本作为分布式锁,以确保更新数据库的顺序是正确的。总而言之,远程副本需要四次往返通信;这无疑是昂贵的,但还没有到无法接受的地步。(我们正在计划通过使用更先进的一致性算法来减少往返次数。)

我们尽可能地利用等待网络请求的时间来完成其他的工作。例如,当一个副本获取互斥锁时5,另一个副本可能正在计算校验和,而协调器(coordinator)6可能正在读数据库。

Git引用更新事务

三阶段提交是保持副本同步的关键。为了实现这一协议,我们需要每个副本能够回答“你能执行这些引用更新吗?”这一问题,然后根据协调器的指示提交或回滚事务。为了实现这一目标,我们在开源Git项目中实现了Git引用更新事务(可以通过类似于git update-ref --stdin的命令使用这一特性7</a);为了保证事务执行结果在副本间是确定的(deterministic),我们大量工作8首先,Git获取所有必要的本地引用的锁,然后验证旧值符合预期且新值是有意义的。如果一切正常,则提交这一试探性事务(tentative transaction);否则,它将回滚一切更改。

加速Git引用更新

除了网络延迟之外,我们还必须考虑在单个副本上更新Git引用所需的时间。为此,我们为与引用相关的操作实现一些加速。这些变化也贡献回了开源Git项目。

使用校验和对副本进行比较

我们通过计算副本的所有引用及其值(和一些其他的东西)的校验和来概括副本的状态,称之为“Spokes校验和”。如果两个副本的Spokes校验和相同,则它们肯定拥有相同的逻辑内容。我们在每次更新后计算每个副本的Spokes校验和,作为验证它们保持同步的一项额外检查。

在具有大量引用的繁忙仓库中,从头开始计算Spokes校验和是比较昂贵的,并且会限制引用更新的最大速率。因此,我们会尽可能用逐步的方法来计算Spokes校验和。我们将该值定义为所有(refname, value)对的hash值的异或。因此,在更新引用时,我们可以通过下式来更新校验和的这一部分:

1
new_checksum = old_checksum XOR hash(refname, oldvalue) XOR hash(refname, newvalue)

在我们知道旧Spokes校验和的情况下,计算新Spokes校验和的代价就很小了。

优先考虑用户发起的更新

即使进行了所有这些优化,一次参考更新仍然需要大约三分之一秒。这在大多数情况下都足够了。但是在我们之前提到的情况下,对master进行一次更新可能会导致上百次内部记录的引用更新(bookkeeping reference update),处理这些更新可能会使仓库在30秒内都处于忙状态。如果这些更新会在如此长的时间内阻止用户发起引用更新,则用户请求将被高度延迟,甚至会超时。

为了解决这一问题,我们将一些内部记录更新合并为几个事务,并且令用户发起的更新优先于内部记录更新(因为它们不需要立即被执行)。

GitHub.com和GitHub Enterprise的地理复制

Spokes为GitHub用户带来的最切实的好处是,可以通过地理位置较近的Spokes副本提供Git读取操作(fetch和clone)。由于Spokes可以快速找出哪些副本是最新的,它可以将读操作发送到距离最近的最新副本。Spokes已经在这方面加速了GitHub.com的许多用户的传输速度,而且,随着我们在更多地理区域增加副本,传输速度还会进一步提高。

GitHub Enterprise是GitHub的企业本地版,通过相同的底层Spokes技术,它现在也支持地理复制(Geo-replication)9了。甚至当用户远离中心GHE主机(main GHE host)10时,靠近这样的副本的用户也可以享受更快的Git传输速度。这些副本被配置为无投票权的(non-voting)11,因此,即使被地理复制的主机暂时无法访问,对中心GHE主机的Git推送也能继续进行。

结论

通过对Spokes的精心设计,以及对分布式引用更新的性能的仔细优化,Spokes现在能够在更长的距离范围内复制Git仓库了。这提高了GitHub.com和GitHub Enterprise的健壮性、速度和灵活性。

注释

1虽然我觉得,一旦外星人真的入侵,应该也没有时间考虑这个问题了。

2我不太明白“reference update”这一术语指代的是什么。Git文档似乎表明,这是用于保存commit对应的SHA-1值的文件:

我们可以借助类似于git log 1a410e这样的命令来浏览完整的提交历史,但为了能遍历那段历史从而找到所有相关对象,你仍须记住1a410e是最后一个提交。我们需要一个文件来保存SHA-1值,并给文件起一个简单的名字,然后用这个名字指针来替代原始的SHA-1值。
在 Git 里,这样的文件被称为“引用(references,或缩写为 refs)”;你可以在.git/refs目录下找到这类含有 SHA-1 值的文件。

3我感觉自己对merge和rebase的区别一无所知,也无从理解为什么要做这样的计算。大概是为了显示差异比较?(5.1 代码合并:Merge、Rebase 的选择

4三阶段提交

三阶段提交(英语:Three-phase commit),也叫三阶段提交协议(英语:Three-phase commit protocol),是在计算机网络及数据库的范畴下,使得一个分布式系统内的所有节点能够执行事务的提交的一种分布式算法。三阶段提交是为解决两阶段提交协议的缺点而设计的。
与两阶段提交不同的是,三阶段提交是“非阻塞”协议。三阶段提交在两阶段提交的第一阶段与第二阶段之间插入了一个准备阶段,使得原先在两阶段提交中,参与者在投票之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。

5并不理解具体是获取什么锁。

6我猜测这里的协调器指的应该是分布式事务处理协调器(distributed transaction coordinator),这种技术能够使得分布式计算的事务是可靠的。(参考了What is MSDTC and why do I need to care about it?,虽然不知道和数据库有什么关系。分布式系统真有趣!)

7我之前从未听说过update-ref这个命令。于是查阅手册,得知这是一个用于安全地更新引用中存储的对象名称的命令。

8为了说明他们确实做了很多工作,作者在这一句话里插入了9个链接,全是指向对应的commit和merge记录的。真是辛苦了……

9简单来说,“地理复制”(Geo-replication)这个东西指的就是,通过多个活跃副本完成来自地理区域不同的数据中心的请求。不过好像还有一些其他的细节。

10“GHE”是“Github Enterprise”的缩写。

11“投票权”说的应该是在Spokes中实现弹性这篇文章中提到的实现持久性的方法:保证多数一致。在这里,我猜失去投票权的意思是,无论它的写入结果如何,都不参与投票(不需要获得对于它的独占锁定),如果发生不一致,再进行更新。

这篇文章翻译自Building resilience in Spokes,我加入了一些自己的注解。


Spokes是我们的文件服务器的复制系统,我们在里面存储了超过3800万个Git仓库和超过3600万个gists。它至少存储了每个仓库和每个gist的三个副本,这样,即使服务器和网络出现故障,我们也可以提供持久且高可用的内容访问。Spokes使用Git和rsync1的组合来对存储库进行复制,修复和重新平衡。

Spokes是什么?

在我们进入这一主题——如何实现弹性——之前,我们需要声明一个新的名字:DGit现在改名为Spokes了。

今年早些时候,我们宣布了我们的应用级Git复制系统,名为“DGit”(“Distributed Git”)。我们得到的反馈表明,“DGit”这个名字的区分度不高,可能会导致与Git项目本身混淆。所以我们决定重命名这个系统为Spokes

“弹性”的定义

在任何系统或服务中,有两种衡量弹性的关键方法:可用性(availability)和持久性(durability)。系统的可用性指的是系统提供它应当提供的服务所需的运行时间。它可以提供内容吗?它能接受写操作吗?可用性可能是部分的,完整的或退化的:每个仓库都可用吗?是否有一些仓库——或者整个服务器——的访问很缓慢?

系统的持久性指的是它对永久性数据丢失的抵抗能力。一旦系统接受了一个写操作——推送,合并,通过网站进行的编辑,创建新仓库等——它就应该永远不会破坏或回退该内容到之前的状态。这里的关键问题出现在系统接受写入时:需要存储多少副本,以及在哪里存储?显然,必须存储足够数量的副本,才能保证写操作不丢失的可能性足够高。

系统可以是持久但不可用的。例如,如果系统能够为当前写操作制造的副本数量不能超过最低要求,则系统可能会拒绝接受写操作。这样的系统对于写操作是暂时不可用的,不过它同时能够保证不会丢失数据。当然,系统也可以是不持久但可用的。例如,接收任何写入,无论它们是否可以安全地提交,。

读者可能会意识到这与CAP定理2有关。简而言之,系统最多可以满足以下三个特性中的两个:

  • 一致性(consistency):所有节点都读到相同的数据
  • 可用性(availability):系统可以满足读写请求
  • 分区容错性(partition tolerance):即使节点关闭或无法通信,系统也能正常工作

Spokes将一致性和分区容错性放在首位。在最坏的情况下,它将拒绝接受一些写入,对于这些写入,它不能同步提交至至少两个副本。

可用性

Spokes的可用性取决于底层服务器和网络的可用性,以及我们检测和绕过服务器和网络问题的能力。

单个服务器经常会变得不可用。自从今年春天开始试用Spokes以来,由于内核死锁和RAM芯片故障,我们的一些服务器崩溃了。有时,由于较轻的硬件故障或较高的系统负载,服务器能够提供的服务退化了。在所有这些情况下,Spokes都必须快速检测出问题并绕过它。每个存储库都复制在三台服务器上,因此即使一台服务器处于脱机状态,也基本总会有一个最新的可用副本可以访问。不过,Spokes可不只是它的每个单独的容错部分的总和。3

快速检测问题只是第一步。Spokes同时使用心跳服务(heartbeat)4和实际应用程序流量的组合来确定文件服务器何时停止工作。使用实际应用流量很关键,原因如下。首先,心跳服务的学习和反应速度很慢。我们的每个文件服务器每秒需要处理超过100个请求。如果心跳每秒发生一次,则只有在一百个请求都已经失败后才能发现故障。其次,心跳测试只能覆盖服务器功能的一个子集:例如,服务器是否可以接受TCP连接并响应无操作请求。但是如果失败的情形更微妙呢?如果Git二进制文件已损坏怎么办?如果磁盘访问停止了怎么办?如果所有经过身份验证的操作都失败怎么办?当真正的流量失败时,有时无操作服务仍然能够成功。

因此,Spokes会在处理实际应用程序流量时监视失败情况,如果有太多请求失败,它会将节点标记为脱机。当然,实际请求在正常情况下有时也会失败。例如,有人会尝试读取已经删除的分支,或尝试推送到他们无权访问的分支。因此,Spoke仅仅在三个请求连续失败时才将节点标记为脱机。这有时会导致完全健康的节点脱机——在正常情况下,三个请求也可能会连续失败——但这种情况很少发生,并且导致的代价并不大。

Spokes也使用心跳服务,但不是作为主要的故障检测机制。相反的是,心跳有两个目的:轮询系统负载,并在节点被标记为脱机后提供全清信号(all-clear signal)5。一旦心跳成功,该节点将再次标记为在线。如果在服务器出现问题的情况下心跳成功(检索系统负载几乎是无操作),则在三次失败的请求之后,节点将再次标记为脱机。

因此,Spokes在节点大约发生三次失败之后就会检测到故障。但是连续三次失败的操作仍然太多了!对于干净的故障——连接被拒绝或超时——所有操作都知道如何尝试下一个主机。请记住,Spokes对于每个仓库都维护了三个或更多的副本。对仓库的路由查询不只返回一个服务器,而是返回按优先顺序排序的三个(大约)最新副本的列表。如果对首选副本上的操作失败,则通常还有至少两个个副本可以尝试。

从一个服务器故障转移到另一个服务器的操作图(此处为远程过程调用(Remote Procedure Call,RPC)6)清楚地显示了服务器何时脱机。在下图中,有一个服务器在约1.5小时的时间内不可用;在此期间,数千个RPC操作被重定向到其他服务器。这样的图表是Spokes团队发现出错服务器的最佳检测方法。

一台服务器停机

Spokes的节点离线检测只是建议性的——这只是一种优化。连续三次失败的节点只会被移动到所有读操作的优先顺序列表的末尾,而不是直接从副本列表中移除。尝试一个可能离线的副本还是比根本不进行尝试要好的。

这个故障检测器对服务器故障很有效:当服务器过载或脱机时,对它的操作将失败。Spokes检测到这些故障,并暂时停止将流量定向到故障服务器,直到心跳成功为止。但是,网络和应用程序(Rails)服务器的故障更加混乱。给定的文件服务器可能只对应用程序服务器的一个子集处于脱机状态,而一个出错的应用程序服务器可能会看到每个文件服务器都处于脱机状态的假象。因此,Spokess的故障检测实际上是MxN7的:每个应用程序服务器都保留自己的脱机文件服务器列表。如果我们发现许多应用程序服务器都将某个文件服务器标记为脱机,那么它可能确实脱机了。而如果我们发现某一个应用程序服务器将许多文件服务器标记为脱机,则发现了一个应用程序服务器的错误。

下图说明了故障检测的MxN特性,并以红色显示,如果文件服务器dfs4处于脱机状态,哪些故障检测器会发现错误。8

MxN故障检测

在最近的一次事件中,开发环境中的一个前端应用程序服务器发生了无法解析文件服务器的DNS名称的错误。因为它无法到达文件服务器以向它们发送RPC操作或心跳,所以它得出结论,每个文件服务器都处于脱机状态。但是,只有那台应用程序服务器发生了这些错误;所有其他应用服务器都在正常工作。因此,这台坏掉的应用程序服务器在RPC故障转移图中变得非常明显,并没有没有生产流量因此受到影响。9

持久性

有时,服务器会失效。磁盘可能会失效;RAID控制器可能会失败;甚至整个服务器或整个机架上的全部机器都可能出现故障。即使面对这种逆境,Spokes也为仓库数据提供了持久性。

就像可用性那样,持久性的的实现基础是复制。Spokes至少保留了每个存储库,wiki和gist的三个副本,且这些副本位于不同的机架中。除非严格多数个副本可以应用更改并获得相同的结果,否则不接受对仓库的更新——推送,重命名,编辑维基等。

Spokes只需要一个额外的副本即可避免单节点故障。那么,为什么需要严格多数呢?存储库很可能在大致相同的时间被多次写入,这种情况是很常见的。这些写入可能会发生冲突:例如,一个用户可能会删除一个分支,而另一个用户将新的提交推送到同一个分支。冲突的写入必须被序列化——也就是说,必须在每个副本上以相同的顺序应用(或拒绝)这些写入,这样每个副本才能得到相同的结果。Spokes将写入序列化的方式是确保每次写入都获得对大多数副本的独占锁定。两个写入不可能同时获得多数锁定,因此Spokes通过完全消除并发写入来避免冲突。

如果一个仓库恰好有三个副本上,则在两个副本上的成功写入既保证了持久性,也保证了多数。如果仓库有四个或五个副本,则成功写入需要三个副本。

在很多其他的复制和共识协议(consensus protocols)中,写入到主副本的顺序是官方顺序,所有其他副本都必须按该顺序进行写入。主副本通常需要手动指定,或使用选举协议(election protocol)自动指定。Spokes简单地跳过这一步骤,并将每次写操作都视为一次选举——选择出胜出的顺序之后,可以直接得到写入结果,而不是得到一个能够指示写入顺序的获胜服务器。

无法在多数副本上以相同方式应用的任何写操作都会被Spokes从它被应用的副本上回退。实质上,每个写入操作都需要经过一个投票协议,投票失败方的任何副本都被标记为不健康——不可读取或写入——直到它们被修复。维修是自动和快速的。由于需要多数副本同意接受或回滚更新,在修复不健康的副本时,仍然至少有两个副本可以继续接受读取和写入。

需要明确的一点是,分歧和修复发生的概率是很小的。GitHub每天接受数百万次仓库写入操作。在典型的一天里,几十次写入才会导致一次非一致投票,通常是因为一个副本特别繁忙,到它的连接超时了,而其他副本在没有它的情况下投票成功,继续前进。落后的副本几乎总是在一两分钟内恢复,不会对仓库的可用性造成用户可见的影响。

整个磁盘和整个服务器的故障更罕见,但它们确实会发生。当我们必须移除整个服务器时,突然有数十万个仓库只剩下有两个副本了,而不是三个。这一状况也是可修复的。Spokes会定期检查每个仓库是否具有所需数量的副本;如果没有,则创建更多副本。可以在任何地方创建新副本,并且可以通过每个仓库剩余的两个副本中的任何一个进行复制。因此,服务器故障后的修复是N对N的。文件服务器的集群越大,从单节点故障中恢复的速度就越快。

正常关机

如上所述,Spokes可以快速透明地处理服务器脱机和永久失效。那么,我们可以将这一方法直接用于需要计划维护时对服务器的重启或移除吗?是,也不是。

我们的确可以通过sudo reboot重新启动服务器,也可以通过直接把服务器拔掉来移除它。但这样做有一些微妙的缺点,因此我们需要设计一种更谨慎的机制,重用一些用于应对崩溃和故障的相同逻辑。

简单地重新启动服务器不会影响未来的读写操作,这些操作将被透明地指向其他副本。它也不影响正在进行的写入操作,因为这些操作发生在所有副本上,而其他两个副本可以直接投票成功并继续写入,不需要我们正在重新启动的服务器上的副本。但重启确实会打断正在进行的读取操作。大多数读取操作——例如,获取README以显示在仓库的主页上——速度都很快,能够在服务器正常关闭之前完成。但有些读取,特别是大型仓库的克隆,取决于最终用户的网速,需要几分钟或几小时才能完成。直接打断这些操作是非常粗鲁的。可以在另一个副本上重新启动这些操作,但到目前为止的所有进度都将丢失。

因此,在Spokes中,为了主动重启一台服务器,我们需要先将它置于静默期(quiescing period)。当服务器处于静默状态时,它对于新的读取操作被标记为脱机,但允许现有的读取操作(包括克隆)完成。静默期可能会持续几秒到几个小时,具体取决于被重启的服务器上哪些读取操作处于活动状态。

可能会令人惊讶的是,写操作像往常一样被发送到服务器,即使它们静默也是如此。这是因为写操作需要在所有副本上运行,因此单个副本可以随时丢弃,不会发生用户可见的影响。此外,如果副本在静默时没有接收到任何写入操作,那么该副本将大大落后于其他副本,当它最终完全重新上线时,时会产生大量的追赶负载(catch-up load)。

我们不在Spokes文件服务器上执行“混乱猴子”(chaos monkey)测试10,原因与我们在重新启动它们之前要将它们置为静默状态的原因相同:避免中断需要长时间运行的读取操作。也就是说,我们不会仅仅为了确认突发的单节点故障仍然(在大多数情况下)是无害的而随机重启文件服务器。

虽然我们不执行“混乱猴子”测试,我们仍然会按需要对服务器轮流进行重启,这实现了大致相同的测试目标。当我们需要进行一些需要重启的更改时——比如更改内核或文件系统参数,或更改BIOS设置——我们会将这些服务器置于静默状态并重启它们。我们将机架作为可用性区域11,因此我们一次将整个机架置于静默状态。当给定机架中的服务器结束静默状态——即完成所有未完成的读取操作——我们分批重启这些服务器,每次最多五个。整个机架重启结束后,我们继续前进到下一个机架。

下图显示了在轮流重启期间失败的RPC操作。用不同的颜色标记每个服务器。值是堆叠的,因此在最高的峰值表示的时刻中,八个服务器在同时重启。浅红色块表示一台服务器未能正常重启,因此离线了大约两个小时。

滚动重启

用直接插拔的方法移除服务器的弊端与计划外重启的弊端类似。除了会中断任何正在进行的读取操作外,这种行为还会为在这台服务器上托管的所有仓库带来几个小时的额外风险。当一台服务器突然消失时,之前存储在里面的所有仓库现在都只剩下两个副本了。两个副本足够执行任何读取或写入操作,但两个副本无法承受额外的故障。换句话说,在没有警告的情况下就删除服务器,这样会增加在同一天晚些时候写入操作被拒绝的概率。我们的目标是将这种可能性保持在最低水平。

因此,在准备移除一台服务器之前,我们不再把它存储的仓库副本作为任何仓库的活跃副本。Spokes仍然可以使用该服务器进行读写操作。但当它询问是否所有仓库都有足够的副本时,其中一些仓库——有副本位于将被移除的服务器上的那些 ——将声称不够,然后创建更多的副本。修复这一问题的过程类似于服务器直接消失后的修复过程,不过,区别在于,现在服务器仍然可用,以防其他服务器出现故障。

结论

可用性是很重要的,而持久性甚至更为重要。可用性衡量的是服务响应请求的时间长度12。持久性衡量的是,服务能够可信地存储输入数据中的多少。

为了提供可用性和持久性,Spokes为每个仓库至少保留了三个副本。三个副本意味着,即使一个服务器失效,也不会对用户产生可见的影响。如果两个服务器都发生了故障,Spokes仍然可以为大部分仓库提供完全的访问权限,并为那些恰好有两个副本存储在这两个故障服务器上的存储库提供只读访问。

Spokes只在大多数副本——一般至少为两个——能够提交写入并得到相同的仓库状态时才接受对仓库的写入,这一要求通过确保所有副本上的写入顺序相同提供了一致性。通过在至少两个位置存储每个已提交的写入,它也可以在单个​​服务器发生故障时提供持久性。

Spokes的故障检测器通过监视实时应用程序流量,确定服务器何时脱机并绕过该问题。最后,Spokes具有自动修复功能,可在磁盘或服务器发生永久性故障时快速恢复。

注解

1关于rsync

rsync是Unix下的一款应用软件,它能同步更新两处计算机的文件与目录,并适当利用差分编码以减少数据传输量。rsync中的一项同类软件不常见的重要特性是每个目标的镜像只需发送一次。rsync可以拷贝/显示目录内容,以及拷贝文件,并可选压缩以及递归拷贝。
在常驻模式(daemon mode)下,rsync默认监听TCP端口873,以原生rsync传输协议或者通过远程shell如RSH或者SSH提供文件。SSH模式下,rsync客户端运行程序必须同时在本地和远程机器上安装。
rsync是以GNU通用公共许可证发行的自由软件。

2CAP定理

在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:

  • 一致性(Consistence) (等同于所有节点访问同一份最新的数据副本)
  • 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
  • 分区容错性(P artition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。)
    根据定理,分布式系统只能满足三项中的两项而不可能满足全部三项。理解CAP理论的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会导致数据不一致,即丧失了C性质。如果为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了A性质。除非两个节点可以互相通信,才能既保证C又保证A,这又会导致丧失P性质。

3不知道这个地方在说什么。是说Spokes能提供的功能远多于冗余性吗?

4heartbeat的定义(http://blog.51cto.com/hoolee/1408615):

Heartbeat 项目是Linux-HA工程的一个组成部分,它实现了一个高可用集群系统。心跳服务和集群通信是高可用集群的两个关键组件,在 Heartbeat 项目里,由 heartbeat 模块实现了这两个功能。
heartbeat(Linux-HA)的工作原理:heartbeat最核心的包括两个部分,心跳监测部分和资源接管部分,心跳监测可以通过网络链路和串口进行,而且支持冗 余链路,它们之间相互发送报文来告诉对方自己当前的状态,如果在指定的时间内未收到对方发送的报文,那么就认为对方失效,这时需启动资源接管模块来接管运行在对方主机上的资源或者服务。

5所谓“All clear”是一种防空警报,它的含义是空袭已经结束,民众可以离开防空洞。

6远程过程调用

远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用,例:Java RMI。

7我没有完全理解M*N是什么意思。不过我猜测,这指的是错误是可以双向检测的。

8这张图大概显示了3个fetch请求,一个远端git worker请求和一个web api请求。

9此处令人思考的是,这一故障检测是如何实现的。我们需要了解请求是否会失败,以及一些能够进行请求的非文件服务器记录下的请求失败情况。通过其他服务器的请求情况,实际上,我们可以确定这些服务器的实际运行状况。而RPC图大概是需要通过收集所有请求的实际状况来绘制的。绘制完成之后,Spokes系统可能会根据某种策略自动进行结点状况判断,也可以绘制成实时状态图,让人类判断里面发生错误的结点。

10Chaos monkey

Chaos Monkey是一种识别系统组并随机终止组中某个系统的服务。该服务在受控时间(不在周末和假日运行)和间隔(仅在工作时间运行)运行。在大多数情况下,我们将应用程序设计为在对等设备脱机时继续工作,但在这些特殊情况下,我们希望确保周围有人来解决和学习任何问题。考虑到这一点,Chaos Monkey只在工作时间运行,其目的是让工程师保持警觉并能够做出响应。

(其实就是在系统里自动造成随机失败,增加工程师的警觉性)

11(原注)将机架作为可用性区域处理意味着我们放置仓库副本的时候需要保证同一机架中不会存储同一存储库的两个副本。这样就可以保证,即使丢失了整个服务器机架,也不会影响托管在里面的任何仓库的可用性或持久性。我们选择机架作为可用性区域,是因为几种重要的故障模式(failure mode),特别是与电源和网络相关的故障模式,可能会同时影响整个服务器机架。

“可用性区域”(Availability Zone)由独立的数据中心构成,每个地区都是一个数据中心集群,这些数据中心之间距离足够近,这样可以保证数据库等应用的延迟足够低,但又足够远,这样可以防止出现意外时同时宕机,例如亚马逊在日本的数据中心就分布在不同的地震区。(香港:亚马逊云计算全球化布局下一站?

12所以这个是单次请求所需的时间长度,和服务器自己的运行时间没有关系?那么,看来前面搞错了……

这篇文章翻译自Introducing DGit,我加入了一些自己的注解。(大部分翻译来自谷歌翻译,水平比我更高,只有少数地方译错,我感觉翻译行业的前景很不乐观。)


Edit:DGit现在改名叫Spokes了

GitHub在数百台服务器1上托管了超过3500万个存储库和超过3000万个Gists。在过去的一年中,我们构建了DGit,这是一种新的分布式存储系统,可显著提高对Git内容的提取和存储的可用性(availability),可靠性(reliability)和性能(performance)。

DGit是“Distributed Git”的缩写。正如许多读者已经知道的那样,Git本身是分布式的——Git存储库的任何副本都包含项目整个历史记录中的每个文件,分支和提交。DGit使用Git的这个属性在三个不同的服务器上保存每个存储库的三个副本。即使其中一个服务器出现故障,DGit的设计也能够使存储库的可用性不会发生中断。即使在极端情况下,存储库的两个副本同时不可用,存储库仍然可读(readable);即,提取(fetch),克隆(clone)和大多数Web UI仍然能够继续工作。

DGit在应用程序层执行复制,而不是在磁盘层执行复制。不妨把这些副本看做是三个通过Git协议保持同步的松散耦合的Git仓库,而不是相同的充满了仓库的磁盘映像。此设计使我们能够以极大的灵活性确定仓库副本的存储位置以及用哪个副本进行读取操作。

如果需要使文件服务器脱机,则DGit会自动确定哪些仓库的副本少于三个,并在其他文件服务器上创建这些存储库的新副本。此“修复”过程将所有剩余的服务器用作源和目标。由于“修复”过程的吞吐量是N-N2,因此速度非常快。所有这一切都没有任何停机时间。

DGit只使用普通的Git

大多数最终用户将其Git仓库作为对象、包文件和引用存储在单个.git目录中。他们使用Git命令行客户端、GitHub Desktop等图形客户端或Visual Studio等IDE中内置的Git支持来访问仓库。可能会令人惊讶的是,GitHub的仓库存储层DGit是使用相同的技术构建的。为什么不使用SAN3,一个分布式文件系统?或者其他的能够将持久存储数据的问题抽象化的神奇的云技术?

答案很简单:它速度快,而且很健壮。

Git对延迟非常敏感。一个简单的git log或者git blame命令可能需要顺序加载和遍历数千个Git对象。如果这些低级磁盘访问存在任何延迟,则性能会受到严重影响。因此,将仓库存储在分布式文件系统中是不可行的。Git是为访问高速磁盘而优化的,因此DGit文件服务器将仓库存储在本地高速SSD上。

在更高的层次上,Git还经过优化,可以在通过协议在Git仓库之间之间高效更新(例如,推送和提取)。因此,我们使用这些协议来保持DGit副本同步。

Git是一种成熟且经过良好测试的技术。为什么有一级方程式赛车可用时要重新发明轮子?

GitHub的理念始终是,通过尽可能接近用户使用Git的方式,在我们的服务器上使用Git4。DGit延续了这一传统。如果我们发现性能瓶颈或其他问题,我们有几个核心的Git和libgit2贡献者会解决这些问题,并将补丁提交到人人可用的开源项目。我们在Git方面的经验和专业知识使其成为用于DGit复制操作的的最优选择。

使用DGit前后的GitHub架构

之前,我们使用现有的磁盘层复制技术(即RAID和DRBD5)保存了仓库数据的副本。我们将文件服务器成对组织起来,每个活动文件服务器都有一个通过交叉电缆连接的专用在线备份服务器。每个磁盘有四个副本:主文件服务器上通过RAID保存两个副本,另外两个副本使用DRBD保存在该文件服务器的热备份上。如果文件服务器出现任何问题——例如,硬件故障,软件崩溃或过载情况——一名人类将确认故障并命令备用服务器接管。因此,冗余级别是良好的,但故障转移过程需要手动干预,并且不可避免地导致故障服务器上的仓库在一段时间内不可用。为了尽量减少此类事件,我们始终将仓库存储在专用且高度可靠的服务器上。

现在改为使用DGit,每个仓库都存储在三个服务器上,这三个服务器独立地分布在我们的大群文件服务器中。DGit自动选择托管每个仓库的服务器,使这些副本保持同步,并选择处理每个传入的读取请求最的佳服务器。写操作会同时被导入到三个副本中,仅仅当至少两个副本确认写入成功时才会提交。

文件服务器复制

现在GitHub把仓库存储在一个名为github-dfs的集群中——dfs是“DGit file server”的缩写。这些仓库存储在这些文件服务器上的本地磁盘中,并通过Git和libgit2进行服务。此集群的客户端包括Web前端和与用户的Git客户端通信的代理。

GitHub架构

DGit的优势

DGit为GitHub用户和内部GitHub基础架构团队都提供了许多优势。它也是实现更多即将到来的创新的关键基础。

  • 文件服务器不再必须作为成对的相同服务器部署,彼此靠近,并通过交叉电缆一对一连接。我们现在可以在任何空间配置中使用异构的文件服务器池。
  • 在开始使用DGit之前,当整个服务器发生故障时,需要尽快将其替换,因为其备份服务器运行时没有备用服务器。两个服务器一起中断可能会使数十万个仓库无法访问。现在,当服务器出现故障时,DGit会快速制作其托管的仓库的新副本,并在整个集群中自动分发它们。
  • 路由故障的破坏性大大减小了。我们不必重新启动并重新同步整个服务器,只需停止到服务器的路由流量,直到它恢复。现在可以安全地重启生产服务器,没有过渡期。由于服务器中断对DGit的破坏性较小,我们不再需要等待人类确认中断; 我们可以立即绕过它。
  • 我们不再需要保留大多数时间都在闲置的热备用文件服务器。在DGit中,每个CPU和所有内存都可用于处理用户流量。虽然像push这样的写操作必须转到仓库的每个副本,但是任何副本都可以提供读操作。由于读操作的数量远远超过写操作,因此每个仓库现在可以处理的流量几乎是以前的三倍。下图显示了git处理在旧文件服务器(蓝色)和DGit服务器(绿色)上分别引起的CPU负载。蓝线标识活动服务器的平均值; 它们的热备件不包括在内。DGit服务器上的负载较低:峰值时大约低三倍,而低谷时大约低两倍。由于所有文件服务器都有无法在副本之间分配的后台维护任务,因此低谷性能的改进没有三倍那么多。

DGit减少了CPU负载

  • DGit可以自动平衡磁盘和CPU热点。添加服务器根本不需要计划:DGit只是随机地将现有仓库移动到新服务器,直到磁盘空间和CPU负载恢复平衡。随着现有仓库的扩展或缩小,DGit会移动它们以保持磁盘空间平衡。随着仓库受欢迎程度的增加或降低,DGit会转移负载以缓解CPU和内存热点。在下图中,一个DGit服务器集群(以红色显示)大部分已满,直到我们添加了一个新的服务器集群(以蓝色显示),其中包含更大的磁盘,以减轻磁盘空间压力。第三个集群(绿色)有两个服务器接收仓库,一个服务器放弃它们。继续移动存储库,直到所有服务器的磁盘空闲空间比例相近。

磁盘平衡图

  • DGit减少了存储库之间的命运共享(fate sharing)6。在使用DGit之前,一组固定的仓库一起存储在单个服务器上和该服务器的备用服务器上。如果一个存储库太大,代价太昂贵或太受欢迎,那么该文件服务器上的其他仓库可能会变慢。在使用DGit之后,可以通过其他副本服务其他的仓库,这些副本不太可能与繁忙仓库的其他副本位于相同的服务器上。
  • 副本的分离意味着我们可以将存储库的副本放在不同的可用区域中,甚至可以放在不同的数据中心中。可用性得到改善,我们(终于)可以通过在地理上靠近用户的服务器为用户提供内容。

DGit的试运行

DGit带来的变化是巨大的,所以我们一直在逐步推广它。DGit最复杂的特性是,复制不再是透明的:现在,每个存储库都显式存储在三台服务器上,而不是一台有自动同步热备份的服务器上。因此,DGit不能再依靠DRBD和RAID控制器来保持副本同步,必须实现自己的序列化处理(serializability)7,锁定(locking),故障检测(failure detection)和二次同步(resynchronization)8。我们将在以后的帖子中探讨这些内容丰富的主题。这些足以说明,在依赖DGit存储客户数据之前,我们需要彻底测试这些功能。我们的部署经过多个步骤:

  • 我们首先移动了DGit开发人员的个人仓库。
  • 我们移动了一些私人的,GitHub拥有的仓库,这些仓库不属于运行网站的一部分。我们首先在每个存储库中打开一个issue,请求我们的同事的允许。这既是一个礼貌的预先通知,也是一种开始向GitHub其余部分解释DGit的方法。
  • 我们移动了GitHub的其余大部分私有仓库。
  • 我们停止移动存仓库大约三个月,同时我们进行了大量测试,对DGit相关流程进行自动化,为DGit撰写了操作级别的文档,并且(咳咳)修复了偶尔发生的错误。
  • 经过三个月的稳定运行后,我们移动了大多数GitHub拥有的公共存储库,以及外部用户拥有的那些存储库的fork。例如,Linguist的拥有者是GitHub,但其大约1,500个fork属于外部用户。托管公共仓库测试了DGit处理大型仓库的网络和更高流量负载的能力。
  • 我们开始移动不属于GitHub的公共仓库。我们立即从GitHub的showcases流行仓库中找出一些有很多分支的繁忙的仓库:包括RubyRailsBootstrapD3等等,并且搬运了它们。我们的目标是在DGit中尽可能多地获取流量和不同的使用模式,同时仍然手工采集一小部分仓库的数据。
  • 距离我们第一次移动自己的仓库六个月后,令人满意的是,DGit能够很好地托管网站,于是我们开始批量移动仓库。

DGit中存储库的百分比

在试运行阶段,我们不断尝试关闭服务器,有时会同时关闭几个服务器,此时它们正在提供实时生产流量。用户操作并没有受到影响。

在撰写本文时,58%的存储库和96%的Gists(占Git操作的67%)都迁移到了DGit中。我们正在尽快将剩余的文件服务器对转换为DGit服务器。

结论

GitHub始终致力于快速可靠地获取,推送和查看仓库。在未来几年内,我们将使用DGit作为我们的仓库存储层以实现这些目标,同时进行横向扩展并提高容错能力。

在接下来的一个月里,我们将发布更多对DGit背后的技术进行深入研究的帖子。

总结

我之前曾经在网上看到一篇相关的评论帖子,但是现在找不到了,我明天再找……

注解

1这个服务器的数量比我想象的要少。(或者谷歌翻译错了,应该是成百上千台。)

2大概是因为源可以有多个,目标也可以有多个。不过这个吞吐量可能翻译错了,我也不懂“N-by-N”具体是什么意思。

3关于SAN(摘自维基百科):

存储区域网络(英语:storage area network,缩写作 SAN)是一种连接外接存储设备和服务器的架构。人们采用包括光纤通道技术、磁盘阵列、磁带柜、光盘柜的各种技术进行实现。该架构的特点是,连接到服务器的存储设备,将被操作系统视为直接连接的存储设备。除针对大型企业的企业级存储方案外,随着在2000年后价格和复杂度的降低,越来越多的中小型企业也在逐步采用该项技术。

它访问的是磁盘设备,而不是文件。
4虽然我现在并不理解这个传统的存在意义和价值。

5DRBD=Distributed Replicated Block Device,一个基于软件在不同宿主之间创建块设备(硬盘、翻去、逻辑分区等)的镜像的存储复制服务。功能特性包括:

  • 实时
  • 透明
  • 同步或异步

https://docs.linbit.com/docs/users-guide-9.0/#p-intro

6关于命运共享:

命运共享(Fate Sharing)建议将所有必要的状态放在通信端点,这些状态用于维护一个互动的通信关联(例如虚拟连接)。由于这个原因,导致通信失效的情况也会导致一个或更多端点失效,这样显然会导致整个通信的失败。命运共享是一种通过虚拟连接(例如,由TCP实现的连接)维持活动的设计理念,即便网络在一段时间内失效。命运共享也支持一种“带智能终端主机的哑网络”模型。

端到端原则与命运共享原则;还是没太看懂)

7可串行化(Database Conflict Serializability [数据库冲突可串行化]):

多个事务[Transaction]的并发执行是正确的,当且仅当其结果与按某一次串行地执行这些事务时的结果相同,称这种调度策略为可串行化的调度。–数据库系统概论第四版

8查了之后,发现一堆心脏病疗法,仍然无法很好地认识什么是resynchronization。