本文内容
1.面向的读者和预备知识
2.基本概念
3.实现方式
4.远程过程调用
5.分布式设计原则
6.练习
7.参考资料
-----------------------------------------------------------------------------------------------------------------
一、面向的读者和预备知识
本教程覆盖了分布式系统设计的基本概念。预备的知识包括一定的编程经验(C++,JAVA,etc)、网络知识的基本了解,以及数据结构和算法。
二、基本概念
什么是一个分布式系统?在其他相关定义未明朗之前这个概念很难定义。这里,给出一个“渐进”式的定义。
1.程序:你所写的代码
2.进程:运行中的程序
3.消息:进程间通信的媒介
4.包:消息在网络介质上传输的片段
5.协议:关于消息格式的正式化描述以及两个进程交换消息时需要遵循的规则
6.网络:连接计算机、工作站、终端、服务器等设备的基础设施。它包括由通信线路连接的路由器。
7.组件:可以是一个进程,也可以是用以提供进程运行、支持进程通信、存储数据的硬件设备
8.分布式系统
一个应用,该应用运行一组协议以协调网络中的多个进程,从而使所有组件共同完成一个或多个相关的任务。
那为什么我们需要建立一个分布式系统呢?这里有很多的好处,其中包括通过一种开放和可扩展的方式,连接远程用户以获得远程资源。当我们提到“开放”的时候,我们是指组件之间可以不断开放地交互。至于“可扩展”,我们是指系统可以容易地根据用户、资源和计算实体的数目变化而进行相应的改变。因此,我们可以看出,相对于把单机系统组合起来的方式,通过分布式组件的连接,分布式系统能获得更大的规模和更强的能力。要使分布式系统有实际用处,它必须是可靠的,然而这个目标并不容易实现,因为要同时运行的组件之间进行交互具有一定的复杂性。要使分布式系统真正地可靠,它必须拥有以下特性:
1.容错:系统能在组件出错时恢复并一直执行正确的指令。
2.高可用性:系统能对操作进行恢复,从而使得它在遇到组件出错的情况,也可以重新开始正在提供的服务。
3.自我恢复:错误修复以后,组件能自我重启并重新加入系统。
4.一致性保持:系统能协调多个组件的运作,而这些组件是并发运行的且随时会出现故障。这些问题都会使分布式系统出现“非分布式”的情况(译者注:即不同的组件不能保持一致性,各个组件好像单机的情况,维护不同的数据,即“非分布式”)
5.可扩展性:即使是系统的规模增大,它也能正确地运行。例如,我们可能想增大一个正在运行的系统的规模。对于一个不可扩展的系统,通常这些都会增加系统的损耗或使系统退化。类似地,对于一个可扩展的系统,我们想增加用户或服务器的数目,或系统的负载,这些变更不会因此明显的改变。
6.可预计的性能:系统能提供预期的响应时间。
7.安全:系统对数据和服务的访问提供认证。
以上是一些比较高的标准,实现具有一定的挑战性。其中最困难的可能是分布式系统必须在一些组件出现故障的情况下继续正常运行。在下面的一段对话摘录了Ken Arnold对这个问题的看法。他是Sun公司的研究者,同时参与建立了Jini的构建,另外,他也是CORBA架构团队的成员。
-----------------------------------------------------------------------------------------------------------------
“在本地编程和分布式编程里,‘故障’的含义有所不同,在设计分布式系统的时候,必须考虑到故障的情形。设想一下,问一个人,如果某事件发生的可能性是10的13次方分之一,那它究竟多久发生一次呢?一般的回答是,从不发生。在人类的眼光看来,这是一个无限大的数。但如果你问一个物理学家,他可能会说,一直都在发生,在一立方尺的空气里,这种事情一直都在发生。所以,当你设计一个分布式系统的时候,你必须认为,‘故障’是一直发生的。所以‘故障’问题是你首要考虑的问题。然后,究竟故障问题的该怎么看待呢?一个典型的描述是“部分故障”。例如我发一个消息给你,然后网络出错,接着又两种可能的结果。一是你收到消息后网络出错了,我得不到响应。二是你并没有收到消息因为在这之前网络已经出错,这种情况下,我也得不到响应。
所以假如我没有收到一个响应,我怎么知道是哪一种结果呢?只有找到你我才能知道。这样需要修复网络或者你要出现,因为这也可能不是网络的问题而是因为你不在网络中。这些情况使得我们在设计的时候有什么不同呢?首先,它使得设计的复杂性增大了。我和你的交互越多,我需要对恢复交互的方法做更多的考虑。”
-----------------------------------------------------------------------------------------------------------------
在分布式系统的设计中,故障(failure)处理是一个重要的主题。明显地,故障可以分为两种类型,软件故障和硬件故障。在80年代后期之前,硬件故障是一个主要考虑的因素,但从那以后,硬件的可靠性已经显著地提高。发热的减少、更微型的电路、更少的芯片外连接以及更高质量的生产技术都提高了硬件的可靠性。现在,问题主要出现在网络连接线路以及机械设备上,例如网络连接故障和驱动的故障。
而软件故障则是一个显著的问题。即使在严格的测试后,软件的bug(错误、漏洞)仍然带来一部分非预期宕机时间(约占总宕机时间25%-35%)。在一些成熟的系统里面,软件的bug可分为两种类型。
Heisenbug:这种bug在被察觉或研究的时候会消失或改变特性。一个典型的例子是,bug出现在程序的发行版编译模式(release mode),但在调试模式(debug mode)下并不出现。"Heisenbug”是根据"Heisenberg 不确定理论"命名的。该理论属于量子物理学的范畴,常被用来描述观察者的观察方式对于测量结果的影响。
Bohrbug:根据Bohr 原子模型命名。与Heisenbug相反,这种bug在观察研究的时候并不消失或改变特性。Bohrbug在一些定义好的条件下表现稳定。
相对于本地系统,Heisenbug更多地出现在分布式系统。其中一个原因是,程序员很难对并行进程的交互过程有全面和一致的认识。
下面,我们更具体地看一下,在分布式式系统里有那些类型的故障情况。
停机故障:即一个组件突然停止运行。除了设定超时,没有其他方式能检测这种故障。超时一般是指组件不能定期发送心跳消息或不能响应请求。例如,你的计算机死机或失去响应就是一种停机故障。
停止运行故障:与停机故障不同的是,这种故障发生的时候会通知其他组件。例如,一个网络文件服务器通知其客户端它将要停机,这就是一种停止运行故障。
丢失故障:发送或接收消息时,因为缓存不足导致消息被丢弃,而且发送方和接收方都不知情的故障。例如,路由器过载就会出现这种故障。
网络连接故障:网络连接线路中断。
网络分割故障:网络分割成两个或多个互不连接的子网络,子网络内部能进行消息通信,子网络之间则不行。当网络连接线路中断的时候会发生这种故障。
时序故障:系统的时序特性并扰乱。例如,不同计算机上的时钟不同步或消息延时超过时限。
Byzantine故障:恶意程序或网络错误等引起的数据污染、丢失或其他不合规范的行为,从而导致的错误。(译者注:这里取名于Byzantine Generals' Problem,拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军。问题是这些将军在地理上是分隔开来的,并且将军中存在叛徒。因此如何达到一致性是一个严峻的问题。)
我们的目标是设计一个分布式系统,具有之前提到的特性(容错、高可用性、可恢复性等),那样就意味着我们必须针对故障的情况进行设计。过程中,我们必须非常谨慎,不能对系统组件的可靠性抱有假设。然而几乎每一个人,在第一次建立分布式系统的时候都会有以下假设。因为这些假设已经被广泛的认识到,所以一般称之为“8大谬论(8 fallacies)。”
8大谬论如下:
1.延时为0.
2.无限带宽
3.网络是安全的。
4.网络拓扑不变。
5.传输消耗为0.
6.网络是同构的。
下面是以上一些名词的简单解释。
延时:初始化一个对数据的请求到数据真正开始传输的时间。
带宽:描述信道通信能力的参数。带宽越大,信道能承载更多的信息。
网络拓扑:网络建立的形式,例如环状、总线型、星型或网状。
同构网络:网络中运行单一的协议。
三、实现方式
在不可靠的通信网络之上建立一个可靠的系统似乎是不可能的。我们必须处理这些不确定性。一个进程知道自己的状态,也知道其他进程之前最近的状态。但进程间无法互相知道对方现时的状态。他们缺乏类似于共享内存这样的媒介。他们也缺乏一个准确的方法去检测错误,或辨别出一个故障是本地故障还是通信故障。
明显地,分布式系统设计是一项挑战性的工作。我们怎么能在不允许任何假设且有这么多复杂性的情况下完成这项工作呢?我们以一些限定规模的情况作为开始。我们接下来看看一个具体的分布式系统设计,它使用客户端-服务器(C/S)模型和标准的协议。这些标准的协议在低网络可靠性的情况为系统的运行提供了很大的帮助,这样会使我们的工作更简单。接下来我们看看C/S技术和这些协议。
在C/S架构的应用中,服务器提供诸如数据查询、发送现时股价等服务。客户端使用服务器提供的服务,为用户显示数据查询结果或提供股票购入建议。这两者通信的过程必须是可靠的。那意味着数据不能丢失而且数据必须按照服务器发送的顺序到达客户端。
在分布式系统里,有多种类型的服务器。例如文件服务器管理文件系统所在的磁盘存储单元。数据库服务器管理数据库以及使它们能被客户端访问。网络名字服务器维护一个进程,该进程实现符号名称或服务描述与IP地址及端口号的对应。
在分布式系统里,可能会有多个同一类型的服务器,例如多个文件服务器或网络名字服务器。这里使用“服务”一次表示同一类型的一组服务器。同时绑定也是个特别的概念。当一个进程需要访问一个服务时,这个进程就和提供该服务的服务器发生了绑定。有很多绑定的策略来定义怎样选择某一台特定的服务器进行绑定。例如,策略可以基于邻近性(一个UNIX NIS客户端会从本机器上开始寻找服务器);或者策略也可以基于负载平衡(一个CICS客户端的绑定则采用随机的方式)。
分布式系统可能会引入数据冗余存储,在这种情况下,服务必须维护多份数据的副本使得在数据在多个地方可以被访问,或者从而增加数据的可用性,即使某一服务器的进程崩溃了数据也能被访问。“缓存”则是一个相关概念,而且它广泛地被运用在分布式系统。如果一个进程维护一个数据的副本,使得它再次被使用时能被迅速地获取,那么,我们就说一个进程有缓存数据。同时,如果请求能从缓存中获取数据而不是原始的服务数据,我们就说缓存被命中了。例如,浏览器使用文档缓存来加速常用文档的访问。
缓存跟冗余存储比较相似。但缓存数据的实时性没那么好。所以,在使用一个缓存之前,我们可能需要一个策略来进行验证。如果一个缓存经常被原始的服务更新,那缓存就和冗余的副本一致。
正如刚才所提到的,服务器和客户端之间的通信需要可靠性。你很可能听说过TCP/IP。网际协议(IP)是一套运行于互联网和很多商业网络的通信协议。而传输控制协议(TCP)则是这其中一个核心协议。使用TCP协议,客户端和服务器能够互相创建连接,在这些连接的基础上能通过包交换数据。这个协议保证了通信的可靠性和双方之间数据传输的正确顺序。而IP协议体系则可以被看作一个分层的体系,每一个有各自的特点,而且它只使用下层的服务,只向上层提供服务。一个系统分层地实现了协议的行为,我们可以称之为协议栈。协议栈可以在软件或硬件实现,或者两者的混合。一般来说,较低的层在硬件上实现,较高的层在软件上实现。
--------------------------------------------------------------------------------------------------------------
相关材料:TCP/IP的历史反映了互联网的发展。这里 是对TCP/IP历史的简要概括。
---------------------------------------------------------------------------------------------------------------
在网际协议的体系里,分为四层,分别是应用层、传输层、网络层、链路层。
应用层:这里主要是需要进行网络通信的应用程序。数据在程序里按照特定的格式发送到下一层,然后被封装到传输层。例如HTTP,FTP,Telnet就处于应用层。
传输层:传输层负责端到端的消息传输,传输过程独立于下层网络。同时,传输层也负责错误控制、数据分块和流控制。在传输层里,端到端的传输可分为两种类型:面向连接的传输(TCP)和无连接的传输(UDP)。TCP连接比UDP连接更为丰富,可以提供可靠的传输。首先,TCP确保接受方处于准备接收的状态。它使用三次握手机制保证通信双方都准备好进行通信。第二,TCP确保数据到达了发送的目标。如果接受方没有返回一个确认包,TCP就会自动地重新传输数据包(一般情况下会进行3次重传)。如果需要的话,TCP也可以把一个大的数据包分成小的包来使数据能在通信双方之间稳定地传输。此外,TCP协议可以自动地丢弃重复的包,以及在接收方按照正确的顺序对包进行重新排列。
UDP和TCP很相似,因为它也是负责数据包在网络里进行传输的协议。但这两者有很大的差别。首先,UDP是无连接的。这意味着一个程序可以向网络中的另一方发送数据包,但这两者并不建立连接关系。后者可能会向前者返回一些消息,而前者也可能继续发送数据包,但这个过程中没有一个固定的连接。第二,UDP并不保证接收方按照正确的顺序接受数据包。保证的只是数据包的内容。因为没有额外的包检错开销,所以UDP比TCP效率更高。因此,在线游戏往往使用这个协议。在游戏里,如果一个用于更新画面的包丢失了,玩家只会有一下子的不愉快。另一个包将很快地对画面进行更新,虽然这会使画面出现抖动,但影响不大。
虽然TCP比UDP更可靠,但在很多时候仍然面临着故障的问题。TCP使用确认和重传即时来检测和修复错误。但如果传输损耗很大,出现连接长时间断开的时候,会导致重传机制的失效。一般地,最大的连接失效时间规定在30-90秒之间。超出个时间,TCP会发出一个故障信号,然后放弃重传。虽然TCP已经提供了一些策略减轻这个问题,但这只是TCP故障中的其中一个例子。
网络层:在定义之初,网络层就负责实现数据包在单一网络中的传输。随着网络互连的需要,网络层承载了更多的功能,即负责使数据从一个网络送达另一个网络。这主要包括把一个数据包在互联网中路由。IP协议负责数据包传出的基本任务。
链路层:链路层处理数据在物理介质上的传输。通常也负责在数据包上加上帧头和帧尾使得数据能在物理网络和组件间进行传输。
-----------------------------------------------------------------------------------------------------------------
相关材料:更多关于IP体系的内容,访问这里 。
-----------------------------------------------------------------------------------------------------------------
四、远程过程调用(Remote Procedure Calls ,RPC)
以前,很多分布式系统的组件通信是直接在TCP/IP体系的基础上建立起来的。逐渐地,一种使得客户端和服务器有效地进行交互的方式被提出来了,称之为RPC,即远程过程调用。它扩展了原来的本地过程调用,使得被调用的过程不仅仅处于调用者的地址空间里。这两个进程可以处于同一个系统,或网络中的不同系统。
RPC类似于函数调用。跟函数调用相似的是,当进行PRC的时候,调用者需要把参数传递到远程过程,然后等待响应。在下面的例子里,客户端向服务器发出一个RPC请求,然后客户端等待直到消息返回或超时。而当请求到达服务器以后,服务器调用一个分配的程序执行请求的服务然后把返回消息发送到客户端。当RPC完成以后,客户端继续往下执行。
在基于RPC的分布式系统里,线程的使用非常普遍。在服务器端,对于每个到达的请求将产生一个新的线程去执行。而在客户端,当线程发出一个RPC后则进入阻塞状态。当接收到返回后,客户端的线程继续执行。
程序员编写基于RPC的代码时,应该完成三项工作。
1.规定客户端和服务器之间的通信协议。
2.开发客户端程序。
3.开发服务器程序。
通信协议是通过多个stub规定和建立的。stub是通过协议编译器(译者注:例如rpcgen 就是一个协议编译器)产生的程序,它不具体完成工作,而是做了一个函数名称以及参数的声明。它仅包含使编译和链接能够进行的代码。
客户端和服务器的程序必须通过协议规定的过程和数据类型进行通信。服务端注册的过程可以被客户调用,然后返回必要的数据。客户端发送需要的参数,调用远程的过程,然后接受返回的数据。
所以,一个RPC程序使用stub产生器产生的类来执行RPC。而程序员需要提供服务器端的类,它包含了PRC的处理逻辑。
RPC也引入了一些在本地过程编程里面不会出现的问题。例如,当服务器不在运行的时候会出现绑定错误;客户端和服务器程序版本不同的时候会出现版本不匹配;当服务器崩溃,出现网络问题或客户端计算机出现问题的时候会产生超时。
一些PRC程序把某些错误看做是不能恢复的。然而,一个容错的系统,在主服务器和备份服务器上,为了应对重要的服务和故障,应该有多个可切换的服务源。
其中一项挑战性的错误处理工作发生在以下的情况。当服务器发生故障以后,客户端需要了解请求的结果以进行继续的工作。而在这种情况下,不正确的交互时有发生。例如,客户端向服务器发送一个Carnegie Hall管弦乐区域的订票请求。如果这座位是空的话,服务器记录这个请求和订票记录。可是请求却超时了。那那到底这个座位是空的且订票被记录下来了吗?即时有一个备份服务器使得请求可以重来,但这个客户会不会被记录为订了两张票呢?在Carnegie Hall里,这可是价格不菲的。
在下面的一些条件之下会发生错误,我们需要对这些情况进行处理:
1.网络数据丢失需要重传:一般地,系统最多只尝试重传一次。在最坏的情况下,如果进行了多次重传,我们需要尽力减少数据多次被接受造成的不良影响。
2.在执行PRC的时候,服务器崩溃了:如果服务器再它完成任务之前崩溃,系统一般能正确地恢复,因为客户端会重新初始化一个请求;如果服务器在完成任务以后发送RPC返回消息之前崩溃,客户端的重试会导致多个请求的处理。
3.客户端在接收响应之前崩溃了:客户端重启,且服务端丢弃响应数据。
五、分布式设计原则
根据本文上述提到的内容,我们可以定义分布式系统设计的一些基本原则。可能其中有些比较简单和明显,但刚开始做这方面的设计,列出来总是有好处的。
1.正如Ken Arnold所说,“在设计分布式系统的时候你必须考虑故障的情况。”避免对系统里任何部件的状态做假设。一个典型的场景是一个进程向另外一台机器上的进程发送数据。第一台机器的进程获得返回数据后进行处理,然后在不知道第二台机器是否就绪的情况下,把结果发给第二台机器。其实在这个过程中,很多情况可能发生,而发送的进程必须对可能的故障进行预测。
2.明确地定义故障的场景和它们发生的可能性。确保你的代码基本考虑到了一些较有可能发生的情况。
3.客户端和服务器都能够对无响应的发送方/接收方进行处理。
4.仔细考虑你需要在网络中传送多少数据。尽可能地减少网络传输。
5.延时是指初始化一个数据请求道数据真正开始传输之间的时间。最小化延时有时候会归根到一个问题:你是需要使用很多个小数据量的请求/响应还是一个大数据量的请求/响应。实验可以帮助你做出决定,做一些小的测试可以寻找出最好的折中方案。
6.不要假设数据经过传输后和他发送时保持一致(即使是机架上硬盘之间的传输)。校验和或有效性检验可以帮助检测数据是否有更改。
7.缓存和冗余存储是处理组件间状态问题的方法。我们尝试使组件拥有的状态最少,但这非常困难。状态是被存储于一个地方的数据,它代表另外一个地方的某个进程,它不能由其他部件重建。如果它能够重建的话,那它就是一个缓存。缓存有助于减轻维护部件间状态的风险。(译者注:这段想不明白状态究竟代表什么?有待高手解决。原文如下:Caches and replication strategies are methods for dealing with state across components. We try to minimize stateful components in distributed systems, but it's challenging. State is something held in one place on behalf of a process that is in another place, something that cannot be reconstructed by any other component. If it can be reconstructed it's a cache. Caches can be helpful in mitigating the risks of maintaining state across components. )但缓存不能保持更新,所以在使用之间需要一个策略去验证。
如果一个进程存储了一些不可重建的信息,问题就出现了。例如,这里会产生单点失效问题吗?要处理这个问题,可以使用冗余存储。冗余存储在减轻这类问题时非常有效。但仍然存在一些挑战,例如一致性问题。在怎样维护状态以及何时使用缓存和冗余的问题中,有很多需要权衡。但在这些场景中进行实验测试非常困难,因为每种机制的开销又有所不同。
8.注意速度和性能。认真考虑一下你的系统中那些部分对性能影响较大,哪里是性能瓶颈,为什么是性能瓶颈。采用一些测试来评价其他可能的方案,对这些方案进行测试和归纳。和你的同事讨论这些方案和结果,得出一个最佳的选择。
9.应答是相对耗费资源的,在分布式系统应尽量避免。
10.重传也是耗费资源的。你需要通过一些实验调节延时的范围来对重传的机制进行优化。
六、练习(略)
七、参考资料
[1] Birman, Kenneth. Reliable Distributed Systems: Technologies, Web Services and Applications. New York: Springer-Verlag, 2005.
[4] Wikipedia article on IP Suite
[5] Gray, J. and Reuter, A. Transaction Processing: Concepts and Techniques. San Mateo, CA: Morgan Kaufmann, 1993.