Cointime

扫码下载App
iOS & Android

如何构建稳健而高效的分布式系统?这篇文章给你答案!

个人专家

作者:Unmesh Joshi. 编译:Cointime.com QDD

概述

这篇文章主要介绍了分布式系统的模式和实现,以及在主流开源分布式系统中观察到的一些模式。文章开始提到,进行分布式系统研讨会时面临的主要挑战是如何将分布式系统的理论与开源代码库(如Kafka或Cassandra)相结合,同时讨论的内容又足够通用,能够涵盖广泛的解决方案。模式的概念提供了一种解决方法。

模式的结构允许我们专注于特定问题,清楚地说明为什么需要特定的解决方案。然后,解决方案描述使我们能够提供具体的代码结构,既能显示实际解决方案,又足够通用,可以涵盖各种变化。这种模式技术还允许我们将各种模式链接在一起,构建完整的系统。这为讨论分布式系统实现提供了良好的词汇。

接下来是在主流开源分布式系统中观察到的一组模式。希望这些模式对所有开发人员都有用。

分布式系统 - 实现视角

当今的企业架构充斥着本质上是分布式的平台和框架。如果我们看一下典型企业架构中使用的框架和平台的样例列表,可能如下所示:

所有这些本质上都是“分布式”的。一个系统要成为分布式系统,有两个方面:

  • 它们在多台服务器上运行。集群中的服务器数量可以从三台到几千台不等。
  • 它们管理数据。因此,它们是固有的“有状态”系统。

当数据存储在多台服务器上时,可能会出现许多问题。上述提到的所有系统都需要解决这些问题。这些系统的实现对这些问题有一些反复出现的解决方案。了解这些解决方案的一般形式有助于理解这些系统的广泛实现,并且在构建新系统时也可以作为良好的指导。模式派上了用场。

模式

模式是由Christopher Alexander引入的一个概念,在软件界被广泛接受,用于记录用于构建软件系统的设计构造。模式提供了一种有结构的方式来看待问题空间,并提供了多次验证的解决方案。使用模式的一个有趣方式是能够将多个模式链接在一起,形成一种模式序列或模式语言的形式,从而在实现“整个”或完整的系统时提供一些指导。将分布式系统视为一系列模式是一种了解其实现的有用方法。

问题及其重复出现的解决方案。

当数据存储在多台服务器上时,可能会出现许多问题。

进程崩溃

进程可能随时崩溃,可能是由于硬件故障或软件故障引起的。进程可以以多种方式崩溃。

  • 系统管理员可能会将其关闭以进行例行维护。
  • 在进行某些文件IO操作时,可能因为磁盘已满且异常未被正确处理而被终止。
  • 在云环境中,情况可能更加复杂,因为一些无关事件可能会导致服务器崩溃。

最重要的是,如果进程负责存储数据,它们必须被设计为对存储在服务器上的数据提供持久性保证。即使进程突然崩溃,它应该保存所有已成功存储的数据。根据访问模式,不同的存储引擎具有不同的存储结构,从简单的哈希映射到复杂的图形存储。由于将数据刷新到磁盘是最耗时的操作之一,不能将每次插入或更新都刷新到磁盘。因此,大多数数据库具有仅定期刷新到磁盘的内存存储结构。这会导致如果进程突然崩溃,则有可能丢失所有数据。

使用称为“预写式日志(Write-Ahead Log)”的技术来解决这个问题。服务器将每个状态更改作为命令存储在硬盘上的追加文件中。追加文件通常是一个非常快速的操作,因此可以在不影响性能的情况下进行。使用一个追加顺序的单个日志来存储每个更新。在服务器启动时,可以重新播放日志以重新构建内存状态。

这提供了持久性保证。即使服务器突然崩溃然后重新启动,数据也不会丢失。但是,在服务器恢复之前,客户端将无法获取或存储任何数据。因此,在服务器故障的情况下,可用性不足。

一个明显的解决方案之一是将数据存储在多台服务器上。因此,我们可以在多台服务器上复制预写式日志。

涉及多个服务器时,需要考虑许多更多的故障场景。

网络延迟

在TCP/IP协议栈中,传输消息的延迟没有上限。它可能会因网络负载而变化。例如,一个1 Gbps的网络连接可能会因触发的大数据作业而被淹没,填充网络缓冲区,从而导致某些消息的任意延迟到达服务器。

在典型的数据中心中,服务器被集中放置在机架中,并且有多个机架通过顶端交换机连接。可能存在一棵连接数据中心中一部分和另一部分的交换机树。在某些情况下,一组服务器可以彼此通信,但与另一组服务器断开连接。这种情况被称为网络分区。在服务器通过网络进行通信的情况下,如何知道特定服务器是否已崩溃是一个基本问题。

在这里需要解决两个问题。

  • 一个特定的服务器不能无限期地等待另一个服务器是否崩溃。
  • 不应该有两组服务器,每组服务器都认为另一组服务器已经失败,因此继续为不同的客户提供服务。这称为分裂大脑(split brain)。

为了解决第一个问题,每个服务器定期向其他服务器发送一个心跳(heat beat)消息。如果错过一个心跳,发送心跳的服务器将被视为崩溃。心跳间隔足够小,以确保检测服务器故障不会花费太长时间。如下所述,在最坏的情况下,服务器可能正在运行,但群集作为一个整体可以继续进行,将服务器视为正在失败。这确保了向客户端提供的服务不会中断。

第二个问题是分裂大脑。对于分裂大脑,如果两组服务器独立接受更新,则不同的客户端可能会获取和设置不同的数据,并且一旦分裂大脑解决,就无法自动解决冲突。

为了解决分裂大脑问题,必须确保两组服务器之间断开连接的情况下不能独立进行进展。为了确保这一点,服务器采取的每个操作仅在大多数服务器可以确认该操作的情况下被视为成功。如果服务器无法获得多数派,它们将无法提供所需的服务,并且某些客户组可能无法接收服务,但群集中的服务器始终处于一致状态。构成多数派的服务器数量称为法定人数。如何决定法定人数?这取决于群集可以容忍的故障数。因此,如果我们有一个由五个节点组成的群集,我们需要三个法定人数。通常情况下,如果我们想要容忍f个故障,我们需要一个大小为2f + 1的群集。

法定人数确保我们有足够的数据副本来承受一些服务器故障。但是,这还不足以为客户端提供强一致性保证。假设客户端在法定人数上发起写操作,但写操作只在一个服务器上成功。法定人数中的其他服务器仍然具有旧值。当客户端从法定人数读取值时,如果具有最新值的服务器可用,它可能会获取最新值。但是,如果在客户端开始读取值时,具有最新值的服务器不可用,它也可能获得旧值。为了避免这种情况,需要有人跟踪法定人数是否就某个操作达成一致,并且只向客户端发送已保证在所有服务器上都可用的值。这种情况下使用Leader and Followers。其中一个服务器被选举为领导者,其他服务器充当跟随者。领导者负责协调和控制跟随者的复制。现在,领导者需要决定对客户端可见的更改。使用High-Water Mark来跟踪已成功复制到法定人数的跟随者的写入预写式日志中的条目。所有高于高水位线的条目都对客户端可见。领导者还将高水位标记传播给跟随者。因此,如果领导者失败,并且一个跟随者成为新的领导者,客户端所看到的内容不会存在不一致性。

进程暂停

即使使用了法定人数和领导者和跟随者,仍然存在一个需要解决的棘手问题。领导者进程可以任意暂停。进程可能会暂停的原因有很多。对于支持垃圾回收的语言,可能会存在长时间的垃圾回收暂停。具有长时间垃圾回收暂停的领导者可能会与跟随者断开连接,并在暂停结束后继续向跟随者发送消息。同时,由于跟随者没有收到来自领导者的心跳,它们可能已经选举出一个新的领导者并接受了来自客户端的更新。如果旧领导者的请求按原样处理,它们可能会覆盖一些更新。因此,我们需要一种机制来检测来自过时领导者的请求。这时候使用“生成式时钟(Generation Clock)”来标记和检测来自旧领导者的请求。生成时钟是一个递增的数字。

不同步的时钟和事件排序问题

检测旧领导者消息和新消息之间的问题是维护消息排序的问题。可能会认为可以使用系统时间戳来对一组消息进行排序,但事实并非如此。无法使用系统时钟的主要原因是服务器之间的系统时钟无法保证同步。计算机中的时钟由石英晶体管理,并根据晶体的振荡测量时间。

这种机制容易出错,因为晶体的振荡速度可能快或慢,因此不同的服务器可能具有非常不同的时间。一组服务器的时钟通过一项名为NTP的服务进行同步。该服务定期检查一组全球时间服务器,并相应地调整计算机时钟。

由于这是通过网络进行通信,并且网络延迟可能因上述部分中讨论的原因而延迟,时钟同步可能会因网络问题而延迟。这可能导致服务器时钟彼此偏离,并且在NTP同步发生后,甚至可以向后移动时间。由于计算机时钟存在这些问题,一般不使用一天中的时间来对事件排序。而是使用一种简单的技术称为Lamport时钟Generation Clock就是Lamport时钟的一个示例。Lamport时钟只是简单的数字,只有在系统中发生某个事件时才会递增。在数据库中,事件涉及写入和读取值,因此Lamport时钟只在写入值时递增。Lamport时钟的数值也会传递给发送给其他进程的消息中。接收进程可以选择两个数字中较大的数字,即它在消息中接收到的数字和自己维护的数字。这样,Lamport时钟还可以跟踪相互通信的进程之间的事件先后关系。其中一个示例是参与事务的服务器。虽然Lamport时钟可以对事件进行排序,但它与一天中的时钟无关。为了弥合这个差距,使用了一种称为Hybrid Clock的变体。Hybrid Clock使用系统时间以及一个独立的数字,以确保值以单调递增的方式增加,并且可以像Lamport时钟一样使用。

Lamport时钟允许确定一组相互通信的服务器上事件的顺序。但它无法检测在一组副本中发生的对同一值的并发更新。Version Vector用于检测一组副本之间的冲突。

Lamport时钟或Version Vector需要与存储的值关联起来,以检测哪些值在其他值之后存储或是否存在冲突。因此,服务器将值存储为Versioned Value

将所有内容整合起来-模式序列

我们可以看到,理解这些模式如何帮助我们从零开始构建一个完整的系统。我们将以共识实现为例。

容错共识

分布式共识是分布式系统实现的特殊情况,它提供最强的一致性保证。在流行的企业系统中常见的示例包括ZookeeperetcdConsul。它们实现了诸如zab和Raft等共识算法,以提供复制和强一致性。还有其他流行的共识算法,如用于Google的Chubby锁定服务的multi-paxos、view stamp replication和virtual-synchrony。简而言之,共识是指一组服务器就存储的数据、存储数据的顺序以及何时将该数据对客户端可见达成一致。

假设一种崩溃故障模型,即假设当发生任何故障时,集群节点停止工作并崩溃,实现单个值的共识的基本技术是使用Paxos。Paxos描述了一些简单的规则,使用两阶段执行、Quorum和Generation Clock来实现集群节点集之间的共识,即使在进程崩溃、网络延迟和时钟不同步的情况下。

当数据被复制到集群节点时,仅实现单个值的共识是不够的。所有副本都需要就所有数据达成一致。这需要在保持严格顺序的同时多次执行PaxosReplicated Log描述了如何扩展基本的Paxos以实现这一点。

这种技术也被称为状态机复制以实现容错。在状态机复制中,存储服务(如键值存储)在所有服务器上进行复制,并且用户输入在每个服务器上按相同的顺序执行。用于实现这一点的关键实施技术是在所有服务器上复制Write-Ahead Log以获得复制日志

实现复制日志的模式序列

我们可以将这些模式组合起来实现复制的Write-Ahead Log,具体如下。

要提供持久性保证,可以使用Write-Ahead Log模式。Write Ahead Log被分成多个段,使用Segmented Log。这有助于处理日志清理,由Low-Water Mark处理。通过在多个服务器上复制写前日志来提供容错能力。服务器之间的复制由Leader and Followers模式管理,使用Quorum来更新High-Water Mark以决定哪些值对客户端可见。所有请求都按严格顺序处理,使用Singular Update Queue。在从领导者发送请求到跟随者时,保持顺序使用Single Socket Channel。为了在单个套接字通道上优化吞吐量和延迟,可以使用Request Pipeline。跟随者通过从领导者接收的HeartBeat来了解领导者的可用性。如果领导者因网络分区而暂时与集群断开连接,则可以使用Generation Clock检测到。如果所有请求仅由领导者提供服务,它可能会过载。当客户端是只读且可以容忍读取旧值时,它们可以由跟随者服务器提供服务。Follower Reads允许处理来自跟随者服务器的读取请求。

原子提交

当多个集群节点都存储相同数据时,共识算法是有用的。通常,数据的大小太大,无法在单个节点上存储和处理。因此,使用各种分区方案(如Fixed partitionsKey-Range Partitions)将数据分区到一组节点上实现容错性。

有时需要将一组分区上的数据作为一个原子操作存储。如果存储分区的进程崩溃,或者存在网络延迟或进程暂停,可能会发生数据在一些分区上复制成功而在另一些分区上失败的情况。为了保持原子性,数据需要在所有分区上存储并可访问,或者全部不存储。使用Two Phase Commit来保证跨一组分区的原子性。为了保证原子性,两阶段提交通常需要锁定所涉及的数据项。这可能严重影响吞吐量,特别是在持有锁的长时间只读操作存在时。为了在不使用冲突锁的情况下获得更好的吞吐量,两阶段提交的实现通常使用基于Versioned Value的存储。

Kubernetes或Kafka控制平面

KubernetesKafka这样的产品的架构都是围绕一个强一致性的元数据存储构建的。我们可以将其视为模式序列。Consistent Core用作强一致性、容错性的元数据存储。Lease用于实现集群节点的组成员资格和故障检测。集群节点使用State Watch在任何集群节点失败或更新其元数据时获得通知。Consistent Core实现使用Idempotent Receiver在网络故障重试的情况下忽略群集节点发送的重复请求。Consistent Core是建立在上述章节中描述的“Replicated Wal”上的。

逻辑时间戳的使用

可以将各种类型的logical timestamps使用视为模式序列。各种产品使用Gossip Dissemination或Consistent Core来实现集群节点的组成员资格和故障检测。数据存储使用Versioned Value来确定哪些值是最新的。如果单个服务器负责更新值,或者使用Leader and Followers,那么可以使用Lamport时钟作为Versioned Value中的版本。当时间戳值需要从每天的时间派生时,会使用Hybrid Clock而不是简单的Lamport时钟。如果允许多个服务器处理更新同一值的客户端请求,则使用Version Vector来检测不同集群节点上的并发写入。

通过理解问题及其一般形式中的重复解决方案,有助于理解完整系统的构建块。

下一步

分布式系统是一个广阔的主题。在这里涵盖的模式集合只是其中的一小部分,涵盖了解决任何分布式系统中的以下问题类别的不同问题解决方案:

  • 群组成员资格和故障检测
  • 分区
  • 复制和一致性
  • 存储
  • 处理
评论

所有评论

推荐阅读