本文是martinfowler.com上关于分布式系统模式的文章。原文地址为:Patterns of Distributed Systems。如有侵权请联系我。
分布式系统给软件带来了很多挑战。分布式系统通常要求我们有多个数据副本,这些副本需要保持同步。然而,我们不能依赖处理节点可以稳定的工作,也不能依赖网络可靠不会因为延迟导致不一致。尽管如此,还是有很多组织仍然依赖一些列的分布式软件来管理数据存储、消息传递、系统管理和计算能力。这些系统面临的共同问题,他们需要使用类似的解决办法。本文将这些解决方案视为模式并加以开发,通过这些模式,我们可以了解如何更好地理解、交流和教授分布式系统设计。
Unmesh Joshi 是ThoughtWorks的首席顾问。他是一个软件体系结构爱好者,他认为理解分布式系统的原理与过去十年中理解web体系结构或面向对象编程一样重要。
目录
- 这是怎么回事
- 分布式系统——一种实现视角
- 模式
- 分布式系统——一种实现视角
- 问题及其反复出现的解决办法。
- 进程崩溃
- 网络延迟
- 进程暂停
- 非同步时钟和顺序事件
- 把它们放在一起-一个分布式系统的例子
- 实现共识的模式序列
- 下一步行动
模式
- 一致核心
- 生成时钟
- 心跳
- 高水位线
- 领导者和追随者
- 低水位线
- 法定人数
- 请求管道
- 分段测井
- 单插座通道
- 单一更新队列
- 国家观察
- 有时限租约
- 提前写入日志
这是怎么回事
在过去的几个月里,我一直在ThoughtWorks举办关于分布式系统的研讨会。在举办研讨会时所面临的主要挑战之一是如何将分布式系统的理论映射到Kafka或Cassandra等开源代码库,同时保持讨论的通用性,以涵盖广泛的解决方案。提供了一个不错的模式概念。
模式就其本质而言,允许我们专注于一个特定的问题,使我们非常清楚为什么需要一个特定的解决方案。然后,解决方案描述允许我们给出一个代码结构,它足够具体,可以显示实际的解决方案,但通用性足以涵盖广泛的变化。模式技术还允许我们将各种模式连接在一起,以构建一个完整的系统。这为讨论分布式系统实现提供了一个很好的词汇表。
下面是主流开源分布式系统中观察到的第一组模式。我希望这些模式对所有开发人员都有用。
分布式系统——一种实现视角
今天的企业架构充满了自然分布的平台和框架。我们可以看到目前典型企业架构中使用的框架和平台的示例列表,它将如下所示:
平台或框架 | 例子 |
---|---|
数据库 | Cassandra, HBase, Riak |
消息中间件 | Kafka, Pulsar |
基础设施 | Kubernetes, Mesos, Zookeeper, etcd, Consul |
内存数据库/计算网格 | Hazelcast, Pivotal Gemfire |
有状态微服务 | Akka Actors, Axon |
文件系统 | HDFS, Ceph |
模式
克里斯托弗·亚历山大(Christopher Alexander)提出的模式(Patterns)概念。在软件社区中被广泛接受,模式是记录用于构建软件系统的通用设计。模式提供了一种结构化的方式来查看问题空间,而解决方案可以多次看到并经过验证。使用模式的一种有趣方式是能够以模式序列或模式语言的形式将多个模式链接在一起,这为实现“整个”或完整的系统提供了一些指导。将分布式系统视为一系列模式是一种获得深入了解其实现的有用方法。
问题及其反复出现的解决办法。
当数据存储在多个服务器上时,可能会出错。
在最复杂的设置中可能会发生此类问题。 考虑一下Amazon,Google和Github的这些示例。
Github的中断实质上导致了其东海岸数据中心和西海岸数据中心之间的连接丢失。 这造成了一个很小的时间窗口,在该窗口中无法跨数据中心复制数据,从而导致两个mysql服务器的数据不一致。
这种AWS中断是由人为错误引起的,其中人为错误地向自动化脚本传递了一个参数,以使大量服务器停机。
由于某些配置错误造成的Google停运对网络容量造成了重大影响,从而导致网络拥塞和服务中断。
- 进程崩溃
进程可以随时崩溃。 由于硬件故障或软件故障。 进程崩溃的方式有很多种。
- 系统管理员可以将其取下来进行日常维护。
- 由于磁盘已满并且无法正确处理异常,因此可以在执行某些文件IO时将其杀死。
- 在云环境中,这可能会更加棘手,因为一些不相关的事件可能会使服务器宕机。
底线是,如果进程负责存储数据,那么它们的设计必须为存储在服务器上的数据提供持久性保证。即使一个进程突然崩溃,它也应该保留它已经通知用户成功存储的所有数据。根据访问模式的不同,不同的存储引擎有不同的存储结构,从简单的哈希映射到复杂的图形存储。因为将数据刷新到磁盘是最耗时的操作之一,所以对存储的每次插入或更新都不能刷新到磁盘。所以大多数数据库都有内存存储结构,这些结构只是定期刷新到磁盘上。如果进程突然崩溃,则有可能丢失所有数据。
一种称为预写日志的技术用于解决这种情况。 服务器将每个状态更改作为命令存储在硬盘上的仅追加文件中。 追加文件通常是非常快速的操作,因此可以完成文件而不会影响性能。 顺序追加的单个日志用于存储每个更新。 在服务器启动时,可以重播日志以再次建立内存状态。
这提供了耐久性保证。即使服务器突然崩溃,然后重新启动,数据也不会丢失。但在服务器恢复之前,客户端将无法获取或存储任何数据。因此,在服务器出现故障时,我们缺乏可用性。
一个显而易见的解决方案是将数据存储在多个服务器上。所以我们可以在多个服务器上复制预写日志。
当涉及多个服务器时,需要考虑更多的故障场景。
- 网络延迟
在TCP / IP协议栈中,在跨网络传输消息时所引起的延迟没有上限。 它可以根据网络上的负载而变化。 例如,一条1 Gbps的网络链接可能会被触发的大数据作业淹没,从而填满网络缓冲区,并可能导致某些消息到达服务器的任意延迟。
在典型的数据中心中,服务器打包在一起放在机架中,并且有多个机架通过机架交换机的顶部连接。 可能会有一棵交换机树将数据中心的一部分连接到另一部分。 在某些情况下,一组服务器可以相互通信,但与另一组服务器断开连接。 这种情况称为网络分区。 然后,服务器通过网络进行通信的基本问题之一是何时知道特定服务器发生故障。
这里有两个问题要解决。
- 特定的服务器不能无限期地等待其他服务器是否崩溃。
- 不应有两套服务器,每套服务器都认为另一套服务器发生了故障,因此继续为不同的客户端提供服务。 这称为裂脑。
为了解决第一个问题,每台服务器都会定期向其他服务器发送HeartBeat消息。 如果错过了心跳,则将发送心跳的服务器视为已崩溃。 心跳间隔足够小,以确保不需要花费很多时间来检测服务器故障。 如下所示,在最坏的情况下,服务器可能已启动并正在运行,但是考虑到服务器出现故障,群集作为一个整体可以继续前进。 这样可以确保提供给客户端的服务不会中断。
第二个问题是脑裂。 使用裂脑,如果两组服务器独立接受更新,则不同的客户端可以获取和设置不同的数据,一旦裂脑问题出现,就不可能自动解决冲突。
要解决分裂大脑的问题,我们必须确保这两套服务器彼此断开连接,不能独立取得进展。为了确保这一点,只有当大多数服务器都能确认该操作时,服务器执行的每个操作才被认为是成功的。如果服务器不能获得多数,它们将无法提供所需的服务,并且某些客户端组可能无法接收服务,但群集中的服务器将始终处于一致状态。占多数的服务器数量称为仲裁。如何决定法定仲裁人数?这是根据集群可以容忍的失败数量来决定的。所以如果我们有一个由五个节点组成的集群,我们需要三个节点组成的仲裁。一般来说,如果我们想容忍f失败,我们需要一个2f+1的集群大小。
仲裁确保我们有足够的数据副本以承受某些服务器故障。但是,仅向客户提供强大的一致性保证是不够的。假设客户端在仲裁上启动了写操作,但是该写操作仅在一台服务器上成功。仲裁中的其他服务器仍具有旧值。当客户端从仲裁读取值时,如果具有最新值的服务器可用,则它可能会获得最新值。但是,如果仅当客户端开始读取值时,具有最新值的服务器不可用,它就可以很好地获取旧值。为了避免这种情况,某人需要跟踪仲裁是否同意特定的操作,并且仅将值发送给保证在所有服务器上都可用的客户端。在这种情况下使用领导者和关注者。其中一台服务器当选为领导者,其他服务器充当跟随者。领导者控制并协调对跟随者的复制。领导者现在需要确定哪些更改应对客户可见。高水位标记用于跟踪已知已成功复制到关注者法定人数的预写日志中的条目。客户可以看到所有高水位之前的条目。领导者还将高水位标记传播给跟随者。因此,如果领导者失败并且跟随者之一成为新领导者,那么客户看到的内容就不会出现不一致之处。
- 进程暂停
但这还不是全部,即使有了仲裁、领导和跟随,仍然需要解决一个棘手的问题。领导者进程可以任意暂停,进程暂停的原因有很多。对于支持垃圾回收的语言,可能会有很长的垃圾回收暂停。具有较长垃圾收集暂停时间的领导者可以与关注者断开连接,并在暂停结束后继续向关注者发送消息。同时,由于关注者没有收到领导者的任何心跳信号,因此他们可能选择了新的领导者并接受了客户的更新。如果旧领导者的请求按原样处理,它们可能会覆盖某些更新。因此,我们需要一种机制来检测过时领导者的请求。生成时钟用于标记和检测较早的领导者的请求。世代是单调增加的数字。
- 异步时钟和顺序事件
从较新的消息中检测较旧的领导者消息的问题是保持消息顺序的问题。看来我们可以使用系统时间戳来订购一组消息,但是我们不能。我们不能使用系统时钟的主要原因是不能保证跨服务器的系统时钟是同步的。计算机中的一天中的时钟由石英晶体管理,并根据晶体的振荡来测量时间。
这种机制容易出错,因为晶体可以更快或更慢地振荡,因此不同的服务器可能具有截然不同的时间。一组服务器上的时钟由称为NTP的服务同步。此服务会定期检查一组全局时间服务器,并相应地调整计算机时钟。
因为这是通过网络上的通信发生的,并且网络延迟可能会如以上各节中所述发生变化,所以由于网络问题,时钟同步可能会延迟。这可能会导致服务器时钟彼此偏移,并且在NTP同步发生后甚至会倒退。由于计算机时钟存在这些问题,因此通常不将一天中的时间用于订购事件。取而代之的是使用一种称为Lamport时间戳的简单技术。世代时钟就是一个例子。
把它们放在一起-一个分布式系统的例子
我们可以看到从头开始理解这些模式如何帮助我们建立一个完整的系统。我们将以共识实施为例。分布式共识是分布式系统实现的一种特例,它提供了最强的一致性保证。在流行的企业系统中常见的示例有Zookeeper,etcd和Consul。他们实现了zab和Raft等共识算法,以提供复制和强大的一致性。还有其他流行的算法可以实现共识,Paxos用于Google的Chubby锁定服务,查看时间戳复制和虚拟同步。用非常简单的术语来说,“共识”是指一组服务器,它们在存储的数据,存储的顺序以及何时使该数据对客户端可见方面达成一致。
- 实现共识的模式序列
共识实现使用状态机复制来实现容错。在状态机复制中,存储服务(如键值存储)在所有服务器上复制,并且用户输入在每个服务器上以相同顺序执行。实现此目的的关键实现技术是在所有服务器上复制预写日志以具有“ Replicated Wal”。
我们可以将这些模式组合起来实现“ Replicated Wal”,如下所示。
要提供耐久性保证,请使用预写日志(Write-Ahead Log)。使用分段日志(Segmented Log
)将预写日志分成多个段。这有助于清除低水位(Low-Water Mark)处理的日志。容错是通过在多个服务器上复制预写日志来提供的。服务器之间的复制是通过使用Leader和Followers(Leader and Followers)来管理的。仲裁(Quorum)用于更新高水位线(High-Water Mark),以决定哪些值对客户端可见。所有的请求都是按照严格的顺序处理的,使用的是单一的更新队列(Singular Update Queue)。在使用单套接字通道(Single Socket Channel)将来自领导者的请求发送给追随者时,将维持顺序。为了优化单个套接字通道上的吞吐量和延迟,使用了请求管道(Request Pipeline)。追随者通过从领导者那里收到的心跳(HeartBeat)来了解领导者的可用性。如果leader由于网络分区而暂时与集群断开连接,则使用生成时钟(Generation Clock)进行检测。
这样,以一般形式理解问题及其反复出现的解决方案,有助于理解完整系统的构建块。
下一步行动
分布式系统是一个巨大的话题。这里涵盖的模式集只是一小部分,涵盖了不同类别,以展示模式方法如何帮助理解和设计分布式系统。我将继续添加到该集合中,以广泛地包括在任何分布式系统中解决的以下类别的问题
- 组成员和故障检测
- 分区
- 复制和一致性
- 存储
- 处理
模式:
核心一致性
生成时钟(Generation Clock)
心跳(HeartBeat)
高水位线(High-Water Mark)
幂等接收机
领导者和追随者(Leader and Followers)
低水位线(Low-Water Mark)
仲裁(Quorum)
请求管道(Request Pipeline)
分段日志(Segmented Log)
单socket通道(Single Socket Channel)
单一更新队列(Singular Update Queue)
状态观察
有时限租约
预写日志(Write-Ahead Log)
致谢
非常感谢Martin Fowler在整个过程中帮助我并引导我从模式的角度思考问题。
穆斯塔克·阿赫马德在整个过程中给了我很好的反馈和很多讨论
Rebecca Parsons、Dave Elliman、Samir Seth、Prasanna Pendse、Santosh Mahale、Sarthak Makhija、James Lewis、Kumar Sankara Iyer、Evan Bottcher、Jojo Swords、Gareth Morgan提供了早期草案的反馈
重大修订
2020年8月4日:首次发布,包含“生成时钟”和“心跳”模式