【2021-07-27更新】【梳理】简明操作系统原理 第十七章 分布式系统(docx)

配套教材:
Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Reiher
参考书目:
1、计算机操作系统(第4版) 汤小丹 梁红兵 哲凤屏 汤子瀛 编著 西安电子科技大学出版社

在线阅读:
http://pages.cs.wisc.edu/~remzi/OSTEP/
University of Wisconsin Madison 教授 Remzi Arpaci-Dusseau 认为课本应该是免费的
————————————————————————————————————————
这是专业必修课《操作系统原理》的复习指引。
需要掌握的概念在文档中以蓝色标识,并用可读性更好的字体显示 Linux 命令和代码。代码部分语法高亮。
文档下载地址:
链接:https://pan.baidu.com/s/1_kU2HExkc9wk8yjUMMb6UA
提取码:0000

十七 分布式系统

一个重要的思想:通信向来是不可靠的。包括但不限于:在计算机内部,缓冲区满、内存错误、磁盘错误;在外部,线缆故障、网络阻塞、电磁干扰、恶意攻击,都有一定概率使得数据出错或丢失。

分布式系统(distributed system)是运行在超过一台计算机上的操作系统。在分布式系统中,部分部件无法正常工作,一般不影响整个系统大致上正常运行。没有分布式系统,就没有今天的互联网、云计算、网络服务器集群和超级计算机。一个好的分布式操作系统,性能要达标、安全性要过关。在本章,我们新引入一个评估分布式系统的指标:通信(communication)。

分布式系统运行在多台计算机上,所以需要建立一个计算机网络进行互连。交换机(network switch)和路由器(router)是计算机网络的一部分,是使网络中的计算机得以通信的必须部件。在网络中,丢包(packet loss)是很常见的。比如说,当数据包来到路由器以后,暂存在路由器的内存中;如果内存已满,在处理完数据包腾出空间之前,新来的数据包只能被丢弃。必须正确处理丢包,才能确保通信的正确性。

一种消极的态度是:分布式系统不处理丢包。在已知应用程序可以处理丢包的条件下,允许这样做。UDP / IP网络栈(networking stack)就是一个典例。一个进程调用套接字(socket)API创建一个通信终端(communication endpoint);另一台机的进程发送UDP数据报(datagram)到原进程。虽然在发生丢包后,发送数据包的进程不会被通知,但是UDP也包含校验和,通过该校验和能够检测丢包。

校验和在网络通信中常用。接收者每收到一段信息就进行一次校验码计算,计算结果会与发送者给出的校验码比对。若两者一致,就可认为传送没有错误。选择校验函数,要综合性能与碰撞率两方面考虑。这两者一般是矛盾的:碰撞率低的校验函数,耗费的算力一般更大。

检测对方是否收到数据,有一个很直接的手段:确认(acknowledgement,ACK)。发送者发送一段信息,接收者收到后返回信息给发送者,告诉发送者已经收到信息。发送者设置一个超时(timeout)。超过该时限仍未接收到确认信息,就视为已经丢包。发送者可以在丢包后选择重试(retry)。如果可能选择重试,发送者要保留发送的信息,而不是一发送完就删除。
不过,万一确认信息本身丢失了,假设发送者在等待超时后重新发送,接收方就会收到同一段数据两次。有时候重复收到数据没什么影响,但一般情况下不行。还需要使用其它手段,确保同一信息只保存一份。接收者侦测到重复的信息时,不将其再次发送给需要该信息的应用程序。可以为每份信息标记一个唯一的ID,一旦收到ID相同的信息两次,第二次收到的内容不被传给相关应用。我们可以在第1条信息的开头标记其ID为1,该信息结束后标记ID为N,下一条信息开头的ID则为N+1,以此类推。这种顺序计数(sequence count)方法占用额外内存非常少。接收端会将信息的ID与自己的计数器比对。ID对上了,就认为收到了信息,同时自己的计数器自增1。如果确认接收的通知发送失败,发送端会超时,并将信息重发。这时候,由于ID与接收端的计数器的值不匹配,接收端判定该信息重复,虽然仍然会告知发送方已接收,但不再将信息发送给需要的应用程序。
TCP / IP(简称TCP)通信层采用了许多比上述手段复杂得多的手段来确保通信的可靠,包括解决网络拥堵的机制。关于TCP的知识,请认真学习专业必修课《计算机网络》。

有时候,丢包意味着服务器已经满载,不能及时处理发送的信息。这时候,发送端往往会增加等待时限。这种指数回退(exponential back-off)的策略被用于夏威夷大学(University of Hawai’i)在1971年开发的早期计算机网络ALOHAnet,并在早期的以太网(Ethernet)中采用,用于避免无意义的发送重试过多。

分布式共享内存(distributed shared memory,DSM),是由运行在单一计算机上的操作系统的虚拟内存机制扩展而来的,常用于分布式系统中。
使用DSM的分布式系统中,当要访问的页处于计算机内存中时,照常访问;当该页位于其它计算机时,产生缺页中断,缺页中断处理程序将消息发送给其它计算机。从其它计算机取得页后,将其信息写入页表,继续执行。
单纯这样并不能实现一个良好的分布式系统的内存子系统,因此该方法在今日使用得并不多。有许多问题没有考虑到,譬如:当一台计算机意外关机,这台机的内存中的页怎么办?那些分布在多台计算机上的数据结构又怎么处理?分布式系统中,当有计算机故障时,这些数据结构的一部分就无效了。一部分的地址空间突然丢掉,问题很大:随便想一想,数据结构里面的一个next指针指向的位置突然就怎么样都无法访问了。其严重程度显而易见。此外,访问其它计算机上的内存页的速率相比访问本机的内存页慢许多倍,应当尽量减少在其它计算机上取得页面的频率。虽然这方面的研究有很多,但是几乎没有实际的影响;如今,没有人会使用DSM来构建可靠的分布式系统。

由于操作系统层面的抽象对构建分布式系统不是好的选择,编程语言(PL)层面的抽象就显得更有道理。远程过程调用(remote procedure call,RPC)在现代编程语言中也称远程方法调用(remote method invocation,RMI)。RPC机制具有不可撼动的地位,其目标是:令调用在其它计算机上的过程,尽可能与调用本机的过程相同。RPC系统一般具有两部分:存根生成器(stub generator,有时也叫协议编译器,protocol compiler)、运行时库(run-time library)。

存根生成器的功能是:自动化打包(pack)函数参数和结果。这不但避免了手写相应代码带来的麻烦,而且存根生成器还可以优化这些代码。
协议编译器的输入仅仅是服务器希望暴露给客户端的调用。例如,输入可以像这样简单:

interface {
int func1(int arg1);
int func2(int arg2);
};
存根生成器根据这样的输入产生一组不同的代码片段。为客户端生成的客户端存根(client stub)包含了接口中的每个函数;希望使用该RPC服务的客户端程序将与该客户端存根链接,之后就可以调用它们。
客户端只需要将这些函数按照一般的函数进行调用。客户端存根的代码会完成如下的步骤:
·创建消息缓冲。消息缓存通常只是连续的字节数组。
·将需要的信息打包放入消息缓冲。这些信息包括将调用的函数的某些标识符,以及函数需要的参数。这个过程也叫做消息的封送(marshaling)或序列化(serialization)。
·将消息发送到目标RPC服务器。RPC运行时库负责这一步的细节,包括与RPC服务器通信等。
·等待回复。函数调用一般是同步的,调用将等待函数的完成。
·解包(unpack)返回代码和其它参数。如果只有一个返回值,则过程很直接;返回更复杂的结果(如:列表)时,可能需要更复杂的处理。这一步也称为解封送(unmarshaling)或去序列化(deserialization,反序列化)。
·返回到调用者。
对于服务器,也会生成相应的代码。服务器上进行的具体步骤主要有:
·解包消息。函数标识符和变量被提取出来。
·调用实际函数。这一步真正执行了用户需要调用的函数。参数会被RPC运行时传输给特定ID的函数。
·打包结果。返回的参数将会被整理并放入回复缓冲。
·发送回复。
存根生成器还需要考虑几个重要的问题。首先,怎样打包并发送一个复杂度数据结构?例如,write()系统调用具有三个参数:整型文件描述符、缓冲区指针、将要写入的字节数。如果RPC包以指针的形式传送,则需要提供解释这个指针的方法,并执行正确的动作。通常,通过熟知的类型(比如,RPC编译器能够识别的buffer_t类型),或用更多的信息来注释数据结构,使得编译器得知哪些字节需要序列化。
另一个重要的问题,就是服务器的并发。如果RPC总是阻塞,服务器的资源就会被浪费。因此,许多服务器都采用了并发机制。常见的方法就是线程池(thread pool):当服务器启动时,会创建线程的一个有限集合;当消息到达时,就会被分发给其中一个工作线程,工作线程会完成RPC,并发回结果;在此期间,主线程可以继续接收新到达的请求,并分发给新的工作线程。

运行时库处理RPC系统中的绝大多数负载,主要的性能与可靠性问题都在这里处理。
第一个需要克服的困难,是如何定位一项远程服务。这种命名的问题在分布式系统中很常见,有的已经超出本书的范围。最简单的方案建立在已有的命名系统上,比如说,当前的Internet协议提供的主机名(hostname)和端口号(port number)。在这类系统中,客户端必须知道运行了需要的RPC服务的计算机的主机名或IP地址,以及使用的端口号。配套的协议必须提供将数据包从系统中的任何机器路由到特定地址的机制。在《计算机网络》课程中,你将学习解决Internet中的命名问题的DNS(域名系统)机制。
客户端得知需要与哪台服务器进行通信以后,下一个问题就是:需要采用何种运输层(transport layer)协议。例如,RPC系统应当使用可靠的协议,譬如TCP / IP,还是不可靠的通信层协议,例如UDP / IP?
使用TCP固然简单,但是额外开销比较大。具体而言,无论是发送端发送消息给接收端,还是反过来,都需要发送确认信息给对方。TCP的拥塞控制(congestion control)等功能对性能的负面影响,在一些场景中可能很大。因此,许多RPC包都使用UDP等不可靠协议进行传送。由于传输层协议不可靠,RPC系统需要自行实现可靠传输。

 RPC运行时还需要考虑其它问题。举例:如果一次远程调用需要很长时间才能完成,应当怎么办?如果客户端等待太长时间,就可能将这次远程调用视为失败,于是进行重试。一种解决方案是:如果回复不能立即生成,则发回额外的确认信息,使客户端得知:服务器已经成功接收此请求。客户端周期性地检查服务器是否依然能够发回确认。若是,则客户端应当继续等待。
运行时还需要考虑传送大量参数的情况。这时候单个数据包可能放不下。一些低层的网络协议,例如IP,提供分片(fragmentation)机制,由中途的路由器或接收端重新组装分片(IPv6不允许在路由器上组装分片)。如果使用的网络协议不支持,则RPC也需要自行实现类似的机制。
不同的计算机可能接受不同的字节序(byte order)。在网络通信中,一般都使用大端(big endian)。传送到相应的计算机后,可能需要转换为小端(little endian)。字节序的转换,会对性能产生少量的影响。
有的时候,一些RPC可能是异步的。处理此类调用时,RPC系统需要立即返回,使客户端可以去做其它工作。客户端可能需要查看过程的执行进度,或者干脆暂停工作,等待RPC完成。RPC系统需要预留相应的接口。

猜你喜欢

转载自blog.csdn.net/COFACTOR/article/details/119156158