总结自:A Thorough Introduction to Distributed Systems
随着世界每一次技术发展,分布式系统作为计算机科学中一个巨大且复杂的领域变得越来越普遍。
本文将介绍分布式系统基础,概述各种不同类型分布式系统,且不会深入细节。
分布式系统最简单的定义:作为单个完整服务展现在终端用户面前的一组协同工作服务器。组中的服务器共享状态、同步操作,并且任意某个服务器独立故障时都不会影响整个系统的正常运行。
我们通过逐步完善的分布式系统来更好的理解分布式系统:
让我们从数据库开始!传统的数据库将我们插入、查询的信息存储在单个服务器的文件系统上,我们可以直接访问服务器。
而我们要分布数据库系统时,我们需要数据库同时运行在多台服务器上。用户必须可以与选择的任一服务器交互,且分布数据库系统向用户屏蔽用户只是与单一服务器交互的事实–也就是说,如果用户向某个插入了一条记录,那么必须能够从其它查询到此记录。
请谨记,只有必要的时候,再将系统分布化。这是由于管理分布式系统是一个充满陷阱的复杂话题。部署、维护、调试分布式系统是一件令人头痛的事。
分布式系统允许我们做水平扩展。回到我们之前单数据库服务器的例子,在单服务器场景下为了满足更多的访问量,只能升级数据库服务器的硬件。这种方式称为垂直扩展。
垂直扩展在可行的情况下也是较好的方式,但是当超过某个阈值时你会发现,最好的硬件也无法满足更多的访问量。
水平扩展简单来说就是通过添加更多的服务器代替升级单个服务器的硬件。水平扩展在超过某个阈值后明显比垂直扩展要便宜的多,但这并不是使用水平扩展的首要原因。
垂直扩展只能将性能提高到最新硬件能够提供的极限,但是这些能力对中、大型企业来说是远远不够的。
水平扩展最大的优势是当遇到性能瓶颈时,可以简单的通过添加服务器达到几乎没有上限的系统吞吐量。
简单扩展并不是从分布式系统获得的唯一优势,容错性和低延迟同样重要。
容错性–一个拥有十台服务器、横跨两个数据中心的集群比单机有更好的容错性。当一个数据中心挂掉,应用仍然可以正常工作。
低延迟–网络数据包的传递,物理上受到光速的限制。例如,纽约和悉尼之间最短来往距离会耗时160ms。分布式系统允许我们在各个城市部署节点,并访问最近节点(CDN),进而降低访问延迟。
但是,为使分布式系统运行,我们需要为多台服务器同时运行进行特殊的设计,以及处理随之而来的问题。这并不是一件简单的事。
假设我们的web应用非常受欢迎,我们的数据库开始接收其每秒处理查询数两倍以上的访问。此时系统用户会立刻感受到我们系统性能的下降。
让我们一起来实现使得我们的数据库可以面对更高访问量的需求。
通常,一个典型的web应用中读数据大于新增数据或修改旧数据。主从分离策略是一种提高性能的方法,这种策略需要创建多个与主数据库同步的从数据库(这里假设两个从库)。向主库中写入,同时只能从从库中读取。
当插入、修改数据时,我们需要与master交互。Master会异步的通知Slaver数据变化,Slave与Master同步数据。
现在,我们可以实现3倍的数据吞吐量了。但是!这个有一个陷阱!我们直接失去了ACID原则中的C,既一致性。
我们可以观察到,当我们新插入一条记录到Master后立刻查询新插入数据就有可能什么也查询不到!
新数据从Master传播到Slaver并不是立刻发生的。实际上,存在一个能够获取到旧数据的时间窗。我们也可以等待Master和Slaver同步数据后再查询,但是,这会影响写入性能。
分布式系统带来了一部分副作用。上文描述的例子只是我们进行水平扩展时必然会遇到的问题之一。
使用从数据库,我们可以水平扩展读取的流量。但是我们的写仍然集中于一台服务器。
此时,我们没有太多的选择,我们可以将单个服务器无法处理的写入流量分流到多个写入服务器上。
一种常见的方式是多主复制策略。代替之前多个只能读取的从机,我们会有多个支持读写的主节点。不幸的是,这么做会由于冲突的存在使应用急速复杂化(例如,同一ID插入两条记录)。
另一种技术称为分片(sharding),也叫分区(partitioning)。使用分片技术,我们将服务分隔为多个称为碎片的更小服务器。通过创建分片规则,各个不同的碎片中保存不同的记录。所以,创建规则使数据合理的划分是在碎片中是分片技术中极其重要的。
一般的方法是根据记录中的某些列来划分碎片(例如,使用姓名首字母A-D来划分姓名)。但是,由于访问并不总是基于划分关键字的范围,所以分片关键字的选择必须十分小心(例如更多人的人名以C开头而不是Z)。当某个碎片接收到的请求大于其他碎片,这个碎片称为热点,我们必须要避免产生热点。一旦分片完成,重新分片将是非常昂贵的操作,并且可能造成十分严重的问题—停止服务。
为了简单,假设我们的客户端知道每条记录使用那个数据库。还需要注意的是,分片是有很多策略的,我们的这里例子只是为了简单的介绍分片这个概念。
我们现在已经做了很多—将写流量扩展了N倍,N为碎片的数量。这实际上给我们带来了无限制的想象力,我们可以划分更加精细的粒度来提高写流向。
软件工程中的任何技术或多或少都是一种折中,分片也不例外。分片并不是银弹,只有需要时再使用:分片会造成使用非分片关键字的查询变得非常困难(需要遍历不同步碎片,类似于索引)。SQL的JOIN操作性能会非常差,并且复杂的查询是不可实现的。
在进一步讨论前,我们需要区分这两个术语(去中心化、分布式)的不同。
尽管两者听起来类似,并且在逻辑上类似。但他们的不同之处会造成重大的技术、管理差异。
在技术层面上,去中心化是分布式的子集。但是,去中心化系统不会属于一个拥有者。没有任何一个公司有完全去中心化的系统,因为去中心化系统不可能属于一个公司。这意味着,我们现在使用的大多数系统都应该称之为分布式中心化系统。
我们可以想象,创建一个去中心化系统是非常困难的,因为我们需要处理一些参与者是恶意的情况。常见的分布式系统则不需要考虑这种情况,因为我们了解系统内所有节点。
注意:这个定义已经有很多争论。在早期的文献中,它的定义也不尽相同。但是,这里的定义是现在区块链和加密货币推广这个术语的最广泛使用。
现在,我们来了解几个分布式系统类型并列出它们最广为人知的使用场景。
分布式数据存储是使用最广的分布式系统,一般表现为分布式数据库。大多数的分布式数据库都是NoSQL的非关系型数据库,使用key-value语义。分布式数据库在一致性、可用性的基础上提供优良的性能和扩展性。
讨论分布式存储之前 ,我们先要引入CAP定理。
2002年就已经证明,一个分布式存储系统不可能同时具备一致性、可用性、分区容错性。简单的定义:
实际上,分区容错是分布式数据存储必须提供的功能。一致性和可用性是基于分区容错性的。想想一下,如果有两个节点接收数据,并且他们之间的链接中断。他们怎么提供可用性和一致性?他们无法知道其他节点在做什么,因此他们可能变为离线和使用旧数据。
最终,我们需要根据业务场景选择在分区容错的基础上提供健壮的一致性还是高可用性。
实践证明,大多数系统都选择了高可用性–我们不一定需要强一致性。这种权衡并不是我们需要100%的可用性保证,而是由于网络延迟的存在我们必须保证服务器同步(分区容错性)才能获得更强的一致性(CAP三者无法兼顾)。这,以及其他因素使得应用通常采用提供更高可用性的解决方案。
大多数分布式数据库采用最弱的一致性模型:最终一致性。最终一致性模型保证在数据没有更新的情况下,所有的访问最终可以返回最近一次的更新数据。
这些数据库系统提供BASE特性(与传统数据库的ACID特性相反):
以上这种可用性的分布式数据库:Cassandra、Riak、Voldemort
当前也有其他分布式数据存储系统提供了更强壮的一致性:HBase、Couchbase、Redis、Zookeeper。
Cassandra是前文提到的一种分布式NoSQL数据库,其提供了CAP定理中的AP属性,既最终一致性和分区容错性。这里有一点片面,由于Cassandra具有高可配置性,它可以通过配置牺牲可用性来提供强一致性,但这并不是它的常见用例。
Cansandra使用一致性Hash来决定当有节点脱离集群时此节点管理的数据需要传输到其他哪些节点。我们需要通过设置复制因子来指出数据的复制份数。
Cassandra通过一致性Hash来读、写数据,写入的节点数与复制因子相同,读取时只能从写入节点读取:
Cassandra具有强大的扩展性,并提供极高的写入吞吐量:
尽管以上对比图可能是带有偏见的:用Cassandra与提供强一致性的数据库进行对比(否则无法解释MongoDB从4节点扩展到8节点后性能反而下降了),但是其仍然展示了Cassandra集群的能力。
无论如何,Cassandra作为横向扩展和极高吞吐量的分布式数据库代表,其不提供ACID数据库的基本特性–既,不支持事务。
数据库事务是分布式数据库极其难以实现的功能,这需要分布式数据库中的每个节点都执行正确的动作(终止或提交)。这个是分布式系统最基本的共识性问题。
如果分布式系统中各个参与节点和网络都是可靠的,那么达成“事务提交”问题所需的协议是简单的。但是,实际系统中有大量可能的问题,例如进程奔溃、网络分区、丢包、失真或重复消息等。
这里有一个已经证明的经典问题:不可能在有限时间内保证在不可靠的网络上达成正确的共识。
实际上,有些算法能够在不可靠网络上很快(并不是有限时间)达成共识。Cassandra提供了使用Paxos算法的轻量级事务。此外,还有Etcd使用的Raft算法,以及Zookeeper也使用了Paxos算法。
分布式计算是近几年大数据处理引入的关键技术。分布式计算是一门将庞大任务(单台计算机无法单独执行的任务)分解为能够在单个商用计算机上执行的技术。我们将巨大的任务分解为多个小任务,这些小任务可以并行的在多台服务器上运行,最终合理的汇总数据来解决一开始的问题。这种方法可以横向扩展—当任务变得更大时,通过简单的添加计算节点来解决。
分布式计算的早期革新者是Google,他们面对自身巨量数据时,不得不发明一个新的方式来解决分布式计算问题,这就是MapReduce。Google在2004年发布了论文,后来开源社区根据论文开发了Apache Hadoop。
MapReduce可以简单的定义为两个阶段:数据映射阶段(Mapping)以及合并数据为有意义数据阶段(Reducing)。
让我们再次通过一个例子来了解它:
假设我们是中型企业,我们将数据存储在一个两层的分布式数据库中,并将其作为数据仓库。我们希望获取2017年4月(一年前)每天发布的拍手次数数据。
这个例子尽量保持简短、清晰以及简单。假设我们在处理大量的数据(例如10亿数量级的数据)。显然,我们不可能将数据都存储在一个服务器上,同时我们也不可能在一台服务器上分析所有数据。我们不会查询产品数据库,因为这个数据库是一个数据仓库,它主要用于低优先级的离线任务。
每个映射(Map)任务都是一个处理其最大数据量的单独节点。每个任务遍历所有存储节点的数据,并映射为日期或时间的简单元组。然后,完成三个中间步骤–整理、排序以及分片。这三步用于进一步整理数据,并适当的删除部分数据以进行reduce任务。当我们处理巨量数据时,我们将Reduce任务分隔开来,每个Redue任务处理单个日期的数据。
以上是一个较好的示例,通过链式的组织多个MapReduce任务,可以完成不可思议的大量工作。
MapReduce有一些历史遗留问题,并带来了一些困扰:由于MapReduce本身是一个批处理技术,所以当任务失败时就会出现批处理固有的问题:需要从头开始整个工作。一个需要2小时任务的失败会拖延整个数据处理流程,并且我们不希望发生,尤其是运行高峰时段。
另一个问题是我们需要大量的时间等待结果。在分析系统中(由于包含大量数据所以分布式计算),重要的是让数据保持尽可能的实时,不能是几个小时之前的数据。
为了解决以上问题,其他架构被提出。例如:Lambda架构(批处理和流处理结合)以及Kappa架构(仅流处理)。架构上的进步带来了新的工具–Kafaka Streams、Apache Spark、Apache Storm、Apache Samza等。
分布式文件系统可以被看做分布式数据存储。它们在概念上一致—对外提供单个访问接口的大数据存储、访问集群。通常与分布式计算一起使用。
维基百科上定义二者的区别为,分布式文件系统提供与本地文件系统语义一样的访问接口,而不像分布式存储提供自定义的API。
Hadoop Distributed File System(HDFS)作为一个分布式文件系统应用于Hadoop分布式计算框架中。它被广泛用于跨服务器存储、备份大文件(GB或TB数量级)的场景。
HDFS架构由NameNode和DataNode构成。NameNode用于存储、维护集群元数据,例如哪个节点保存哪个文件块。它们(NameNode)扮演网络协调者的角色,用于计算存储、备份的最佳位置,跟踪系统健康度。DataNode用于存储文件并执行命令,例如复制文件、写新文件以及其他数据操作。
毫无疑问,HDFS的最佳实践是与Hadoop一起使用。HDFS为计算任务提供了数据意识–也就是说计算任务在数据存储节点运行,通过利用局部数据(DataNode内数据)来优化计算并减少网络传输量。
Interplanetary File System(IPFS)是一个使用点对点(P2P)协议/网络的新型分布式文件系统。通过使用区块链技术,它实现了完全去中心化,不会有单独拥有者以及单点故障。
IFPS提供一个名为IPNS的域名系统(类似于DNS),使得用户可以轻松访问信息。IPFS存储文件的历史版本,类似于Git的工作原理,运行访问文件的所有历史状态。
IPFS仍然在开发过程中,但是目前已经有基于其构建的项目了:FileCoin。
Alluxio是一个开源的基于内存的分布式存储系统,现在成为开源社区中成长最快的大数据开源项目之一。可以同时管理多个底层文件系统,将不同的文件系统统一在同一个命名空间下,让上层客户端可以自由访问同一命名空间内的不同路径–不同文件存储系统的数据。
在数据分析场景中,可以与Spark较好集成。这篇文章对Alluxio进行了概述。
消息中间件一般分为P2P模式和订阅/发布模式,但是在分布式场景下一般使用订阅/发布模式。消息系统提供了一个中心节点,用于存储、分发整个系统内的消息/事件。它允许我们将业务逻辑与消息处理解耦。
简单来说,消息中间件(Subscribe/Publish模式)工作流程如下:
一个消息被可能产生消息的应用(一般称为Producer)广播,然后进入消息平台,被多个可能对其感兴趣(一般通过Topic表示兴趣)的应用(一般称为Consumer)读取。
如果我们需要将某个事件保存到多个地方(例如,创建用户消息/时间保存到数据库、数据仓库、电子邮件发送服务等其他可能的位置),则消息中间件是最简单的实现方式。
消费者可以直接从代理(broker)拉取数据(pull模式),代理(broker)可以将数据直接推送到消费者(push模式)。
大多数分布式消息系统都推荐使用pull模式,而有些消息中间件的push模式的底层实现为pull。
以下是几个流行的顶级消息中间件:
如果我们在单个LB后有5台应用服务器,并且5台应用服务器都访问一个数据库,这种应用架构是否可以称为分布式应用?让我们回顾下之前的定义:
作为单个完整服务展现在终端用户面前的一组协同工作服务器。组中的服务器共享状态、同步操作,并且任意某个服务器独立故障时都不会影响整个系统的正常运行。
如果把数据库视为共享状态,那么可以将此系统归类为分布式系统—但是,这是错误的:遗漏了定义中协同工作部分。
只有应用内服务节点彼此通信以协调节点行为的系统才是分布式的。
Erlang是一种函数式语言,对并发、分布式以及容错性有较好的语义支持。Erlang虚拟机用于处理Erlang语言的分布式语义。
Erlang虚拟机的工作模型基于大量独立的轻量级进程,并且通过虚拟机内建的消息通信机制进行轻量级线程间通信。这被称为Actor模型,Erlang OTP库可以看做是一个分布式Actor框架(类似于JVM上的Akka架构)。
Actor模型可以帮助Erlang虚拟机达到较好的并发性—进程分布在运行他们的系统的可用内核中。与网络配置极其相似(除了丢弃消息的能力外):Erlang虚拟机可以连接运行于同一数据中心甚至另一大陆的Erlang虚拟机。这些虚拟机运行同一应用,并使用Takeover的方式来处理错误(异常节点周期性启动)。
实际上,语言的分布式层是为提供分区容错性(P)增加的。软件运行在单个虚拟机可能有单机故障致使应用下线的风险,软件运行在多个节点则更容易应对硬件故障,只需要应用以分布式层构建。
BitTorrent是网络上使用最广泛的通过流来传输大文件的协议之一。它最主要的思想是通过两点直接传输文件而不必通过一个主服务来传输文件,这样加快了文件传输速度。
使用BitTorrent客户端,我们可以与世界上多个计算机相连来下载文件。当我们打开一个.torrent文件,先与一个扮演协调者角色、名为tracker的服务器连接。它用来协助发现节点,向我们展示网络上包含我们需要文件的节点。
BitTorrent的用户分为两种:一种是leecher,另一种是seeder。Leecher是下载文件的,而seeder是上传文件的。
P2P网络中令人感兴趣的是:作为一个普通用户,我们有能力加入并向网络贡献。BitTorrent以及其先驱,允许我们自愿托管文件,并向需要文件的用户上传。BitTorrent如此流行的一个原因是它是第一个激励网络贡献的协议。BitTorrent之前文件共享协议的问题是用户只能下载文件。
BitTorrent这种通过seeder更多上传来提供更好下载速度的方式,一定程度上解决了用户只能下载的问题。它激励你在下载文件的同时上传文件。不幸的是,当我们下载完成后,我们无法在网络上保持活动状态。这就造成网络上很少存在持有完整文件的seeder而协议又严重依赖这些用户。解决方案是实现私有的Tracker,私有Tracker需要我们成为社区成员才能参与私有分布式网络。
随着这个领域的发展,发明了无Tracker协议。这是对BitTorrent协议的升级,它不需要中心化的tracker来收集元数据和发现节点,使用新算法来代替。例如Kademlia,一个允许节点来发现节点的分布式HashTable。实际上,每个节点都执行tracker的职责。
分布式账簿可以认为是一个不可篡改的、只能添加数据的数据库,这个数据库在分布式网络的节点中复制、同步、共享数据。
使用事件源的方式,允许重建账簿任意时刻的历史状态。
区块链是当前分布式账簿的基础技术,是分布式账簿技术的起点。这一分布式领域最新、最伟大的创新第一个真正的创建了一个分布式交易协议—比特币。
区块链是一个记录了其网络上发生的所有交易的有序列表。交易信息以块的方式组织、存储。整个区块链本质上是一个块的链表(所以叫区块链)。这里所说的块在创建时需要昂贵的计算并且通过加密技术紧密的相互链接。
简单的说,每个块包含代表当前块内容(默克尔树的形式)加上前一块的散列值再加上一个随机数来生成的特殊散列(以X个0开始)。这个散列需要大量的CPU能力来生成,因为只有通过暴力破解(不断重试)才能够计算出来。
我们常说的矿工就是通过暴力破解的方式尝试计算出这个hash。所有的矿工相互竞争来生成随机数,这个随机数与前一个节点的hash值、本节点默克尔树内容一起散列后的散列值必须满足X个0前缀的约束。当某个矿工找到这个正确的随机数,它将会向整个网络广播。每个节点修改内容,并接收这个结果到本地链中。
将上述思想转化为一个更改区块链(历史)成本极其昂贵、验证是否被篡改极其简单的系统。更改一个块的内容是极其昂贵的,因为会生成一个不同的散列值。记住,每个后续块的散列值都是依赖之前散列值的。如果修改了上图中第一个块的交易信息—既修改了默克尔树,这将会改变该块的散列值(很可能使其无法满足N个前导0的要求),进而改变第二个块的散列值,以至于后续所有块的散列值。这意味着,你需要暴力破解(重试、计算)修改块之后所有的块的随机数。
网络总是信任、复制最长的合法链。为了欺骗系统并最终生成一个更长的链,我们需要至少使用所有节点CPU的50%的能力。
区块链可以被认为是一种突发共识的分布式机制。共识没有被明确实现–没有共识发生时的选举和固定时刻。取而代之的是,共识是遵循协议规则的上千个独立节点异步交互的突发产物。
这项前所未有的创新最近成为科技领域的热潮,人们预测它将标志着Web 3.0的诞生。这绝对是目前软件工程领域最激动人心的方向,充满了正在等待解决、极具挑战性和有趣的问题。
之前的分布式交易系统缺乏一种分布式的实时、实用的阻止重复消费问题的方法。研究已经产生了有趣的命题,但是比特币是第一个具有巨大优势的、实现了实际解决问题的方案。
重复消费问题,既一个资源不能在多个地方消费。如果一个人有1元钱,那么他不能既给A又给B,资源不能被重复。在分布式系统中实时实现这种保证是十分困难的。在区块链技术提出之前,有许多有趣的方式来缓解这个问题,但是这些方式都无法真正、完全的解决这个问题。
比特币可以很容易的解决重复消费问题,因为在同一时间只能向链中添加一个块。单个块不可能有双重消费,即使两个块同时生成–最终只有一个会添加到链上。
比特币依赖于积累CPU计算能力的困难性。在一个投票系统中,攻击者只需要向网络添加节点(由于网路设计为无限制访问,这么做很简单),但是面对基于CPU计算能力的网络,攻击者会受到物理限制:需要获得越来越强力的硬件支持。
这也是恶意团队需要控制网络中50%以上计算节点才能够实施成功攻击的原因。少于这一点,网络中的其他部分将会比攻击计算更快的生成更长的区块链。
以太坊可以认为是一个基于可编程区块链的软件平台。它有自己的加密货币(Ether),来加速其在区块链上部署智能合约。
智能合约是以太坊区块链上一段存储单个交易信息的代码。要运行代码,所要做的就是以智能合约作为目标进行交易。这反过来又使矿工节点执行代码以及它发生的任何变化。代码在以太坊虚拟机中执行。
Solidity,以太坊的本地编程语言,用于编写智能合约。它是一种图灵完备的变成语言,直接与以太坊区块链交互,允许查询余额、其他智能合约结果等状态。为了避免无限循环,执行代码需要一定数量的Ether。
由于区块链可以被解释为一系列的状态变化,许多分布式应用都是构建在以太坊以及类似的平台之上。
存在证明–匿名并且安全存储某个时间点某个数据文件存在的证明。用于确保文档完整性、所有权和时间戳。
分布式自治组织—使用区块链作为自身改进达成一致性的方式的组织。例如:Dash‘s治理系统、SmarCash项目。
去中心化认证—使用区块链存储身份信息来,使得在任何地方使用单点登录、Sovrin、Civic。
还有很多、很多。分布式账簿技术打开了无限可能。可能在我们谈论时被发明出新的使用场景。
在这篇简短的文章中,我们定义了什么是分布式系统、为什么使用它以及对每个分类进行了概览。我们需要记住以下重要的事:
坦率的说,我们并没有触及分布式系统的表象。我们并没有尝试解决和解释核心问题:共识机制、复制策略、事件排序和时间、容错、网络广播等。
应该尽你所能的远离分布式系统。如果您可以通过不同方式解决问题或其他开箱即用的解决方案来避免此问题,那么就应该使用不同方式或开箱即用的方案来避免自己承担不必要的开销。