分布式系统:问题及其可复用的解决方案

java达人

    作者:  Unmesh Joshi
    译者:  java达人
    在过去的几个月中,我一直在ThoughtWorks上进行有关分布式系统的研讨会。举办研讨会时面临的主要挑战之一是如何将分布式系统的理论映射到诸如Kafka或Cassandra之类的开源代码库,同时保持研讨的通用性足以涵盖广泛的解决方案。模式的概念提供了一个不错的方式。
    模式结构本质上使我们能够专注于特定问题,从而很清楚地说明了为什么需要特定解决方案。然后,解决方案描述中给出一个代码结构,该结构足够具体以显示实际的解决方案,但又足够通用以涵盖各种变体。模式技术还允许我们将各种模式链接在一起以构建一个完整的系统。这为讨论分布式系统实现提供了很好的术语。
    接下来是在主流开源分布式系统中观察到的第一组模式。我希望这些模式集对所有开发人员都有用。
    分布式系统 - 实现的角度来看
    当今的企业体系结构充满了自然分布的平台和框架。如果我们看到今天在典型的企业体系结构中使用的框架和平台的示例列表,它将类似于以下内容:
    
    所有这些本质上都是“分布式的”。分布式系统意味着什么?有两个方面:
    ?它们在多台服务器上运行。集群中的服务器数量可以从三台到几千台不等。
       ?  它们管理数据。因此,这些天生就是“有状态”的系统。
    当多个服务器参与数据存储时,有几种途径可能会导致问题。上述所有系统都需要解决这些问题。这些系统的实现对这些问题有一些可复用的解决方案。理解这些解决方案的一般形式,有助于理解这些系统的广泛实现,并且在需要构建新系统时也可以作为很好的指导。下面进入模式章节。
    问题及其可复用的解决方案
    当数据存储在多个服务器上时,可能会出现几个问题。
    进程崩溃
    进程随时会由于硬件故障或软件故障崩溃。进程崩溃的方式有很多种。
    ?系统管理员可以将其下线进行日常维护。?由于磁盘已满并且该异常无法正确被处理,因此在执行某些文件IO时被杀死。?在云环境中,这可能会更加棘手,因为一些不相关的事件可能会使服务器宕机。
    最重要的是,如果进程负责存储数据,则必须对存储在服务器上的数据提供持久性保证。即使进程突然崩溃,它也应保留所有已通知用户已成功存储的数据。根据访问模式,不同的存储引擎具有不同的存储结构,从简单的哈希映射到复杂的图存储。由于将数据刷新到磁盘是最耗时的操作之一,因此无法将每次对存储的插入或更新都刷新到磁盘。因此,大多数数据库都具有内存存储结构,这些存储结构仅定期刷新到磁盘。如果进程突然崩溃,则可能会丢失所有数据。
    一种称为Write-Ahead Log的技术用于解决这种情况。服务器将每个状态更改作为命令存储在硬盘上的append-only文件中。append文件通常是非常快速的操作,因此可以在不影响性能的情况下进行。通过顺序附加单个日志的方式存储每一次更新。在服务器启动时,可以重放日志以再次建立内存状态。
    这提供了持久性保证。即使服务器突然崩溃,然后重新启动,数据也不会丢失。但是,在恢复服务器之前,客户端将无法获取或存储任何数据。因此,如果服务器发生故障,缺乏可用性。
    一种显而易见的解决方案是将数据存储在多个服务器上。因此,我们可以在多个服务器上复制预写日志。
    当涉及多个服务器时,还有更多的故障情况需要考虑。
    网络延迟
    在TCP / IP协议栈中,在跨网络传输消息时所引起的延迟没有上限。它可以根据网络上的负载而变化。例如,一条1 Gbps的网络连接可能会被触发的大数据作业吞没,从而填满网络缓冲区,并可能导致某些消息到达服务器的超长延迟。
    在典型的数据中心中,服务器一起放在机架中,并且有多个机架通过机架交换机连接。可能会有一个交换机树将数据中心的一部分连接到另一部分。在某些情况下,一组服务器可以相互通信,但与另一组服务器断开连接。这种情况称为网络分区。服务器通过网络进行通信的基本问题之一是何时知道特定服务器发生故障。
    这里有两个问题要解决。
    ?某台的服务器不能无限期地等待其他服务器是否崩溃。?不应有两组服务器,每组服务器都认为另一组服务器发生了故障,因此继续为不同组的客户端提供服务。这称为脑裂。
    为了解决第一个问题,每台服务器都会定期向其他服务器发送HeartBeat消息。如果心跳丢失,则将发送心跳的服务器视为已崩溃。心跳间隔足够小,以确保不需要花费很多时间来检测服务器故障。如我们将下面看到的,在最坏的情况下,服务器可能已启动并正在运行,但是考虑到服务器出现故障,集群作为一个整体可以继续运行。这样可以确保提供给客户端的服务不会中断。
    第二个问题是脑裂。脑裂,如果两组服务器独立接受更新请求,则不同的客户端可以获取和设置不同的数据,一旦脑裂得到解决,就不可能自动解决数据冲突。
    为了解决脑裂问题,我们必须确保彼此断开连接的两组服务器不能独立运展。为确保这一点,该服务器执行的每个动作只有获得大多数服务器的确认才被认为是成功的。如果服务器无法获得多数确认,则它们将无法提供所需的服务,并且某些客户端组可能无法接收该服务的响应,但是集群中的服务器将始终处于一致状态。占多数的服务器数量称为Quorum。如何确定Quorum?这是根据群集可以容忍的故障数决定的。因此,如果我们有五个节点的集群,则需要三个仲裁。通常,如果我们要容忍f个故障,则需要2f + 1的集群大小。
    Quorum确保我们有足够的数据副本以承受某些服务器故障。但是,仅向客户提供强大的一致性保证是不够的。假设客户端在quorum上开始了写操作,但是该写操作仅在一台服务器上成功。quorum的其他服务器仍是旧值。当客户端从quorum 取值时,如果具有最新值的服务器可用,则它可能会获得最新值。但是,当客户端开始读取值时,具有最新值的服务器不可用,它就会获取旧值。为了避免这种情况,需有设备跟踪quorum是否同意特定的操作,并且仅将值发送给保证在所有服务器上都可用的客户端。在这种情况下使用 Leader and Followers模式。其中一台服务器当选领导者,其他服务器充当追随者。领导者控制并协调对跟随者的复制。领导者现在需要确定哪些更改应该对客户可见。High-Water Mark用于跟踪已知已成功复制到追随者Quorum的预写日志中的条目。客户端可以看到所有High-Water之前的条目。领导者还将High-Water Mark传播给跟随者。因此,如果领导者失败并且其中一个跟随者成为新领导者,那么客户看到的内容就不会出现不一致之处。
    进程暂停
    但这还不是全部,即使有了Quorums和Leader and Followers,仍然需要解决一个棘手的问题。领导者进程暂停。进程暂停的原因有很多。具有较长垃圾收集暂停时间的领导者会与追随者者断开连接,并在恢复后继续向追随者发送消息。同时,由于追随者没有收到领导者的任何心跳,因此他们可能选择了新的领导者并接受了客户的更新。如果旧领导者的请求按原逻辑处理,它们可能会覆盖某些更新。因此,我们需要一种机制来检测过时领导者的请求。Generation Clock 用于标记和检测来自过期领导者的请求。Generation是单调增加的数字。
    不同步的时钟和事件顺序
    从较新的消息中检测较旧的领导者消息的问题是保持消息顺序的问题。我们似乎可以使用系统时间戳来排序一组消息,但事实上不能。我们不能使用系统时钟的主要原因是不能保证跨服务器的系统时钟是同步的。计算机中的一天中的时钟由石英晶体管理,并根据晶体的振荡来测量时间。
    这种机制易于出错,因为晶体可以更快或更慢地振荡,因此不同的服务器可能具有截然不同的时间。一组服务器上的时钟由称为NTP的服务进行同步。该服务会定期检查一组全局时间服务器,并相应地调整计算机时钟。
    因为这是通过网络上的通信发生的,并且网络延迟可能会如上一节中所述发生变化,所以时钟同步可能会由于网络问题而延迟。这可能会导致服务器时钟彼此偏移,并且在NTP同步发生后甚至会向后移。由于计算机时钟存在这些问题,因此通常不将一天中的时间用于排序事件。取而代之的是使用一种称为Lamport时间戳的简单技术。Generation Clock就是一个例子。
    这些问题可能会发生在最复杂的设置中。考虑Amazon、谷歌和Github的例子。
    一次Github宕机实质上导致了东海岸和西海岸数据中心之间的连接中断。这会导致数据无法跨数据中心复制,使两台mysql服务器的数据不一致。
    https://github.blog/2018-10-30-oct21-post-incident-analysis/
    一次AWS宕机是由人为错误造成的,其中自动化脚本错误地传递了一个参数,关闭了大量服务器。https://aws.amazon.com/message/41926/
    一次谷歌中断是由一些错误配置引起的,对网络容量造成了重大影响,从而导致网络拥塞和服务中断。
    https://status.cloud.google.com/incident/cloud-networking/19009
    汇总-分布式系统示例
    我们可以发现理解这些模式如何帮助我们从头开始建立一个完整的系统。我们将以共识实现为例。分布式共识是分布式系统实现的特例,它提供了最强的一致性保证。在流行的企业系统中常见的示例有Zookeeper,etcd和Consul。他们实现了zab和Raft等共识算法,以提供复制和强一致性。还有其他流行的算法可以实现共识,Paxos用于Google的Chubby锁服务,查stamp replication和virtual-synchrony。用非常简单的术语来说,“共识”是指一组服务器,它们在存储的数据,存储的顺序以及何时使该数据对客户端可见方面达成一致。
    实现共识的模式序列
    共识实现使用状态机复制来实现容错。在状态机复制中,存储服务(如键值存储)在所有服务器上复制,并且用户的输入在每个服务器上以相同顺序执行。实现此目的的关键技术是在所有服务器上复制预写日志以获得“ Replicated Wal”。
    我们可以将这些模式放在一起以实现Replicated Wal,如下所示。
    
    为了提供持久性保证,请使用Write-Ahead Log。使用Segmented Log将Write-Ahead Log分为多个段。这有助于Low-Water Mark 处理日志清理。通过在多个服务器上复制预写日志来提供容错能力。服务器之间的复制是通过使用“领导者”和“追随者”来管理的。Quorum法定数用于更新High-Water Mark,以确定客户端可以看到哪些值。通过使用Singular Update Queue,所有请求均按严格顺序处理。使用Single Socket Channel将领导者的请求发送给追随者时,顺序将得到维护。为了优化单个套接字通道上的吞吐量和延迟性,使用Request Pipeline。追随者通过从领导者处获得HeartBeat获知其可用性。如果领导者由于网络分区而暂时从集群断开连接,则可以使用Generation Clock进行检测。
    通过这种方式,理解问题及其一般形式的可复用解决方案,有助于理解完整系统的构建模块
    下一步
    分布式系统是一个广泛的话题。这里讨论的模式集只是一小部分,它涵盖了不同类别,以展示模式方法如何帮助理解和设计分布式系统。我将继续在这个集合中添加内容,任何分布式系统中都广泛地包含了以下问题类别。
    ?集群成员和故障检测?分区?复制和一致性?存储?处理