[翻译][关于分布式架构和系统设计]分布式系统的模式-综述
(最近把这篇文章重新整理了下,欢迎阅读最新版 https://lichuanyang.top/posts/45718/)
本文翻译自https://martinfowler.com/articles/patterns-of-distributed-systems/ ,原作者对目前各类企业级架构中使用的多种分布式系统进行了总结,从中提取出了一些通用的“模式”(pattern)。本文作为系列文章的第一篇,介绍了分布式系统的特点和一些常见问题。 建议好好阅读一下本文以及英文原文,对于分布式系统设计和分布式架构理念,会有非常大的帮助。
What this is about
分布式系统给程序设计带来了特殊的挑战。 它们通常要求我们拥有多个数据副本,这些副本需要保持同步。 但是我们无法期望各节点能永远可靠地工作,同时,网络延迟也很容易导致不一致的现象出现。 尽管如此,许多组织仍依赖一系列核心的分布式系统来处理数据存储,消息传递,系统管理和计算等。 这些系统面临很多共同的问题,这些问题也可以通过类似的方案来解决。 本文将这些解决方案定义为模式,通过这些模式,我们对于如何更好地理解,交流和教授分布式系统,将建立起更好的认知。
在过去的几个月中,我一直在ThoughtWorks进行有关分布式系统的研讨会。研讨会中面临的主要挑战之一是如何将分布式系统的理论映射到诸如Kafka、Cassandra之类的开源项目中,同时还要保持讨论足够通用,能涵盖更广泛的解决方案。模式的概念提供了一个不错的出路。
“模式”这个结构,本质上使我们能够让我们专注于特定问题,从而很清楚地说明为什么需要特定的解决方案。然后,对解决方案的描述允许我们给出一个代码结构,这个结构足以具体到能够表示出实际解决方案的程度,但同时又足够通用以涵盖各种变化。模式还允许我们将各种模式链接在一起以构建一个完整的系统。这样,我们就有足够的词汇表,来讨论分布式系统的实现。
接下来是在主流开源分布式系统中观察到的第一组模式。我希望这些模式集对所有开发人员都有用。
分布式系统-实现角度
现在的企业架构中有很多天然分布式的平台和框架。如果我们在当前典型的架构抽样查看平台、框架,会看到下面这么一些东西。
Type of platform/framework | Example |
---|---|
Databases | Cassandra, HBase, Riak |
Message Brokers | Kafka, Pulsar |
Infrastructure | Kubernetes, Mesos, Zookeeper, etcd, Consul |
In Memory Data/Compute Grids | Hazlecast, Pivotal Gemfire |
Stateful Microservices | Akka Actors, Axon |
File Systems | HDFS, Ceph |
这些系统都天然是分布式的。关于什么样的系统叫“分布式”系统,包括两个方面:
- 运行在多台服务器上。集群内服务器的数量差别可以非常大,从三台到几千台都可能。
- 要处理数据,所以它们是内在“有状态”的系统。
当多个服务器参与存储数据时,有几种出错的可能。 上述所有系统都需要解决这些问题。 对这些问题,这些系统的实现方式中有一些重复的解决方案。 以更通用的形式理解这些解决方案,有助于理解这些系统的广泛实现方式,并且在需要构建新系统时也可以作为很好的指导。
模式,是Christopher Alexander提出的,在软件社区中被广泛接受的概念,用于在软件系统设计时记录设计架构。 模式为解决问题提供了一种结构化的方式,可以通过多次查看和验证来解决问题。 使用模式的一种有趣方式是能够以模式序列或模式语言的形式将多个模式链接在一起,这为实现“整个”或完整的系统提供了一些指导。 将分布式系统视为一系列模式,是深入了解其实现方式的有效方法。
问题及其反复出现的解决方案
当用多台服务器存储数据时,会有几种出问题的方式。
进程崩溃
由于硬件或软件因素,进程在任何时刻都可能崩溃。有若干种进程崩溃的方式。
- 系统管理员进行常规的系统维护,进程会被关闭
- 进行IO操作,但磁盘空间不足,且异常没有被正确处理时,会导致进程被杀掉
- 在云环境下,可能会更复杂,一些无关的原因也可能导致系统宕机。
最重要的是,如果进程负责存储数据,则必须将它们设计为能够对存储在服务器上的数据提供持久性保证。 即使进程突然崩溃,它也应保留所有成功给过用户ack确认消息的数据。 根据访问模式,不同的存储引擎具有不同的存储结构,范围从简单的hash表到复杂的图结构都有。 将数据刷新到磁盘是最耗时的操作之一,因此无法将每次插入或更新都立即刷新到磁盘。 因此,大多数数据库具有内存存储结构,内存中的数据定期刷新到磁盘。 如果进程突然崩溃,可能会丢失所有这些数据。
一种叫做Write-Ahead Log的技术就是用来解决这个问题的。服务器将每个状态更改作为命令存储在硬盘上的仅附加(append-only)文件中。 append操作通常是非常快的,因此可以在不影响性能的情况下完成。 顺序写单个日志文件,以存储每个更新。 在服务器启动时,可以重播日志以再次建立内存状态。
这提供了持久性的保证。 即使服务器突然崩溃,然后重新启动,数据也不会丢失。 但是,在服务器恢复之前,客户端将无法获取或存储任何数据。 因此,如果服务器发生故障,我们会缺乏可用性。
一种显而易见的解决方案是将数据存储在多个服务器上。 因此,我们可以在多个服务器上复制预写日志。
当涉及多个服务器时,还有更多的故障情况需要考虑。
网络延迟
在TCP / IP协议栈中,通过网络传输消息时所引起的延迟没有上限。 它会根据网络上的负载而变化。 例如,一个1 Gbps的网络连接可能会被定时触发的大数据作业淹没,从而填满网络缓冲区,并且可能导致某些消息在不确定的延迟后才到达服务器。
在典型的数据中心中,服务器打包在机架中,并且通过机架式交换机连接多个机架。 可能会有一棵交换机树将数据中心的一部分连接到另一部分。 在某些情况下,一组服务器可以相互通信,但与另一组服务器的连接是断开的。 这种情况称为网络分区。 服务器通过网络进行通信的基本问题之一是,何时能知道特定服务器发生了故障。
这里有两个问题要解决。
- 一台特定的服务器不能通过无限期的等待,去知道另一台服务器已经宕机。
- 不应有两套服务器,每套服务器都认为另一套服务器发生了故障,因此继续为不同的客户端提供服务。 这称为脑裂(split brain)。
为了解决第一个问题,每台服务器都会定期向其他服务器发送心跳消息。如果没有收到心跳,则将对应的服务器视为已崩溃。心跳间隔足够短,能够确保不需要花费很长时间就能检测服务器故障。在最坏的情况下,服务器可能在正常运行,但是被集群整体认为已经宕机,并继续运行。心跳这种方式可以确保提供给客户端的服务不会中断。
第二个问题是脑裂。在脑裂的情况下,如果两组服务器独立接受更新,则不同的客户端可以获取和设置不同的数据,当脑裂的问题被解决后,是不可能自动解决冲突的。
为了解决脑裂的问题,我们必须确保彼此断开连接的两组服务器不能独立地正常运行下去。为确保这一点,服务器执行动作时,只有大多数服务器可以确认这个动作,该才将动作视为成功。如果服务器无法选出来一个”大多数“,则它们将无法提供服务,并且某些客户端组可能无法接收该服务。但是集群中的服务器将始终处于一致状态。占多数的服务器数量称为Quorum。如何确定Quorum?这是根据集群可以容忍的故障数决定的。比如,我们有五个节点的群集,则需要三个quorum。通常,如果我们要容忍f个故障,则需要2f + 1的集群大小。
Quorum确保我们有足够的数据副本以承受某些服务器故障。但是,仅向客户提供强大的一致性保证是不够的。假设客户端在quorum上启动了写操作,但是该写操作仅在一台服务器上成功。Quorum中的其他服务器仍具有旧值。当客户端从Quorum读取值时,如果具有最新值的服务器可用,则它可能会获得最新值。但是,如果仅当客户端开始读取值时,具有最新值的服务器不可用,它就可以获得旧值。为了避免这种情况,需要有人跟踪判断quorum是否同意特定的操作,并且仅将值发送给保证在所有服务器上都可用的客户端。在这种情况下使用主从模式(master&slave)。其中一台服务器当选为master,其他服务器充当slave。Master控制并协调对slave的复制。Master需要确定哪些更改应对客户可见。高水位标记(High-water mark)用于跟踪预写日志中已成功复制到足够的slave中的条目。客户端可以看到所有高水位之前的条目。Master还将高水位标记发送给slave。因此,如果Master失败并且slave之一成为新master,那么客户看到的内容就不会出现不一致之处。
进程暂停
但这还不是全部,即使有了Quorums和Master and Slave,仍然需要解决一个棘手的问题。 Master进程随时可能暂停。 进程暂停的原因很多。 对于支持垃圾回收的语言来说,可能会有很长的垃圾回收暂停。 具有较长垃圾收集暂停时间的Master可能会与slave断开连接,并在暂停结束后继续向slave发送消息。 同时,如果slave没有收到master的任何心跳信号,他们可能选择了新的master并接受了客户端的更新。 如果旧master的请求按原样处理,则它们可能会覆盖某些更新。 因此,我们需要一种机制来检测过时master的请求。 Generation Clock用于标记和检测来自老master的请求。 世代(generation)是单调增加的数字。
时钟不同步及顺序消息
从较新的消息中检测较旧的master消息的问题是如何保证消息顺序。看起来我们可以使用系统时间来给一组消息排序,但实际上,是不行的。主要原因是我们无法保证跨服务器的系统时钟是同步的。计算机中的一天中的时钟由石英晶体管理,并根据晶体的振荡来测量时间。
这种机制容易出错,因为晶体可以更快或更慢地振荡,因此不同的服务器可能具有截然不同的时间。一组服务器上的时钟通过称为NTP的服务进行同步。该服务会定期检查一组全局时间服务器,并相应地调整计算机时钟。
由于这是通过网络上的通信发生的,并且网络延迟可能会如以上各节中所述发生变化,所以由于网络问题,时钟同步可能会延迟。这可能导致服务器时钟彼此之间漂移,并且在NTP同步发生后甚至会倒退。由于计算机时钟存在这些问题,因此通常不将一天中的时间用于顺序事件。取而代之的是使用一种称为Lamport’s timestamp的简单技术。Generation Clock就是一个例子。
整合-一个分布式系统的例子
我们可以看到,从头开始理解这些模式是如何帮助我们建立一个完整系统的。 我们将以一致性为例。 分布式一致性是分布式系统实现的一种特例,它提供了最强的一致性保证。 比如流行的企业架构中常见的Zookeeper,etcd和Consul。 他们实现了zab和Raft等一致性算法,以提供复制和强大的一致性。 还有其他流行的算法可以实现一致性,Google的Chubby中用于锁定服务的Paxos,考虑stamp replication and virtual-synchrony。 用非常简单的术语来说,一致性是指在一组服务器中,在存储的数据,存储的顺序以及何时使该数据对客户端可见,这些方面达成一致。
实现一致性的模式列表
一致性的实现中,使用 state machine replication来实现容错。在state machine replication中,存储服务,比如键值存储,在所有服务器上复制,而且用户的输入也会在每个服务器上以相同顺序执行。用于实现此目的的关键实现技术是在所有服务器上复制Write-Ahead log以具有“ Replicated Wal”。
我们可以将这些模式放在一起以实现Replicated Wal,如下所示。
为了提供持久性保证,使用Write-Ahead log。使用Segmented Log将预写日志分为多个段。这有助于Low-Water Mark进行日志清理。通过在多个服务器上复制预写日志来提供容错能力。服务器之间的复制通过使用主从模式进行管理。更新High-Water Mark时,使用Quorum,以确定哪些值对客户可见。通过使用Single Update Quest,使所有请求均按严格顺序处理。使用Single Socket Channel, 将master的请求发送给slave时,事件将得到维护。为了优化Single Socket Channel上的吞吐量和延迟,使用了Request Pipeline。Slavet通过HeartBeat确定Master的可用性。如果Master由于网络分区而暂时从集群断开连接,则可以使用“Generation Clock”来检测它。
通过这种方式,理解这些问题,和它们的通用解决方式,帮助我们理解构建复杂系统的方式。
下一步
分布式系统是一个巨大的话题。 这里涵盖的模式集只是一小部分,涵盖了不同的类别,以展示模式方法如何帮助理解和设计分布式系统。 我将继续向这个合集添加下边这些问题。
- Group Membership and Failure Detection
- Partitioning
- Replication and Consistency
- Storage
- Processing