联机数据持久-分布式系统
2023-1-16
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 
 
分布式系统改变了世界的面貌。当你的Web 浏览器连接到地球上其他地方的Web 服务器时,它就会参与似乎是简单形式的客户端/服务器(client/server)分布式系统。当你连上Google 和Facebook 等现代网络服务时,不只是与一台机器进行交互。在幕后,这些复杂的服务是利用大量机器(成千上万台)来提供的,每台机器相互合作,以提供站点的特定服务。
构建分布式系统时会出现许多新的挑战。我们关注的主要是故障(failure)。机器、磁盘、网络和软件都会不时故障,因为我们不知道(并且可能永远不知道)如何构建“完美”的组件和系统。但是,构建一个现代的Web 服务时,我们希望它对客户来说就像永远不会 失败一样。怎样才能完成这项任务?
其他重要问题也存在。系统性能(performance)通常很关键。对于将分布式系统连接在一起的网络,系统设计人员必须经常仔细考虑如何完成给定的任务,尝试减少发送的消息数量,并进一步使通信尽可能高效(低延迟、高带宽)。
最后,安全(security)也是必要的考虑因素。连接到远程站点时,确保远程方是他们声称的那些人,这成为一个核心问题。此外,确保第三方无法监听或改变双方之间正在进行的通信,也是一项挑战。
 

通信基础

现代网络的核心原则是,通信基本是不可靠的。无论是在广域Internet,还是Infiniband等局域高速网络中,数据包都会经常丢失、损坏,或无法到达目的地。
数据包丢失或损坏的原因很多。有时,在传输过程中,由于电气或其他类似问题,某些位会被翻转。有时,系统中的某个元素(例如网络链接或数据包路由器,甚至远程主机)会以某种方式损坏,或以其他方式无法正常工作。网络电缆确实会意外地被切断,至少有时候。
然而,更基本的是由于网络交换机、路由器或终端节点内缺少缓冲,而导致数据包丢失。具体来说,即使我们可以保证所有链路都能正常工作,并且系统中的所有组件(交换机、路由器、终端主机)都按预期启动并运行,仍然可能出现丢失,原因如下。想象一下数据包到达路由器。对于要处理的数据包,它必须放在路由器内某处的内存中。如果许多此类数据包同时到达,则路由器内的内存可能无法容纳所有数据包。此时路由器唯一的选择是丢弃(drop)一个或多个数据包。同样的行为也发生在终端主机上。当你向单台机器发送大量消息时,机器的资源很容易变得不堪重负,从而再次出现丢包现象。
因此,丢包是网络的基本现象。所以问题变成:应该如何处理丢包?
 

不可靠的通信层

一个简单的方法是:我们不处理它。由于某些应用程序知道如何处理数据包丢失,因此让它们用基本的不可靠消息传递层进行通信有时很有用,这是端到端的论点(end-to-end argument)的一个例子,人们经常听到(参见本章结尾处的补充)。这种不可靠层的一个很好的例子,就是几乎所有现代系统中都有的UDP/IP 网络栈。要使用UDP,进程使用套接字(socket)API 来创建通信端点(communication endpoint)。其他机器(或同一台机器上)的进程将UDP 数据报(datagram)发送到前面的进程(数据报是一个固定大小的消息,有最大大小)。
下面展示了一个基于UDP/IP 构建的简单客户端和服务器。客户端可以向服务器发送消息,然后服务器响应回复。
UDP 是不可靠通信层的一个很好的例子。如果你使用它,就会遇到数据包丢失(丢弃),从而无法到达目的地的情况。发送方永远不会被告知丢失。但是,这并不意味着UDP 根本不能防止任何故障。例如,UDP 包含校验和(checksum),以检测某些形式的数据包损坏。
但是,由于许多应用程序只是想将数据发送到目的地,而不想考虑丢包,所以需要在不可靠的网络之上进行可靠的通信。
 
 

可靠的通信层

为了构建可靠的通信层,需要一些新的机制和技术来处理数据包丢失。考虑一个简单的示例,其中客户端通过不可靠的连接向服务器发送消息。发送方如何知道接收方实际收到了消息?
使用的技术称为确认(acknowledgment),或简称为ack。这个想法很简单:发送方向接收方发送消息,接收方然后发回短消息确认收到。
notion image
当发送方收到该消息的确认时,它可以放心接收方确实收到了原始消息。但是,如果没有收到确认,发送方应该怎么办?
为了处理这种情况,需要一种额外的机制,称为超时(timeout)。当发送方发送消息时,发送方现在将计时器设置为在一段时间后关闭。如果在此时间内未收到确认,则发送方断定该消息已丢失。发送方然后就重试(retry)发送,再次发送相同的消息,希望这次 它能送达。要让这种方法起作用,发送方必须保留一份消息副本,以防它需要再次发送。超时和重试的组合导致一些人称这种方法为超时/重试(timeout/retry)。
notion image
 
遗憾的是,这种形式的超时/重试还不够。
notion image
可能导致故障的数据包丢失。丢失的不是原始消息,而是确认消息。从发送方的角度来看,情况似乎是相同的:没有收到确认,因此超时和重试是合适的。但是从接收方的角度来看,完全不同:现在相同的消息收到了两次!虽然可能存在这种情况,但通常情况并非如此。设想下载文件时,在下载过程中重复多个数据包,会发生什么。因此,如果目标是可靠的消息层,我们通常还希望保证接收方每个消息只接收一次(exactly once)。
为了让接收方能够检测重复的消息传输,发送方必须以某种独特的方式标识每个消息,并且接收方需要某种方式来追踪它是否已经看过每个消息。当接收方看到重复传输时,它只是简单地响应消息,但(严格地说)不会将消息传递给接收数据的应用程序。因此,发 送方收到确认,但消息未被接收两次,保证了上面提到的一次性语义。
有许多方法可以检测重复的消息。例如,发送方可以为每条消息生成唯一的ID。接收方可以追踪它所见过的每个ID。这种方法可行,但它的成本非常高,需要无限的内存来跟踪所有ID。
一种更简单的方法,只需要很少的内存,解决了这个问题,该机制被称为顺序计数器(sequence counter)。利用顺序计数器,发送方和接收方就每一方将维护的计数器的起始值达成一致(例如1)。无论何时发送消息,计数器的当前值都与消息一起发送。此计数器值(N)作为消息的ID。发送消息后,发送方递增该值(到N + 1)。
接收方使用其计数器值,作为发送方传入消息的ID 的预期值。如果接收的消息(N)的ID 与接收方的计数器匹配(也是N),它将确认该消息,将其传递给上层的应用程序。在这种情况下,接收方断定这是第一次收到此消息。接收方然后递增其计数器(到N+1),并等待下一条消息。
如果确认丢失,则发送方将超时,并重新发送消息N。这次,接收器的计数器更高(N+1),因此接收器知道它已经接收到该消息。因此它会确认该消息,但不会将其传递给应用程序。以这种简单的方式,顺序计数器可以避免重复。
最常用的可靠通信层称为TCP/IP,或简称为TCP。TCP 比上面描述的要复杂得多,包括处理网络拥塞的机制,多个未完成的请求,以及数百个其他的小调整和优化。
 
 

通信抽象

构建分布式系统时,应该使用什么抽象通信? 多年来,系统社区开发了许多方法。其中一项工作涉及操作系统抽象,将其扩展到在分布式环境中运行。例如,分布式共享内存(Distributed Shared Memory,DSM)系统使不同机器上的进程能够共享一个大的虚拟地址空间[LH89]。这种抽象将分布式计算变成貌似多线程应用程序。唯一的区别是这些线程在不同的机器上运行,而不是在同一台机器上的不同处理器上。
大多数DSM 系统的工作方式是通过操作系统的虚拟内存系统。在一台计算机上访问页面时,可能会发生两种情况。在第一种(最佳)情况下,页面已经是机器上的本地页面,因此可以快速获取数据。在第二种情况下,页面目前在其他机器上。发生页面错误,页面错误处理程序将消息发送到其他计算机以获取页面,将其装入请求进程的页表中,然后继续执行。
由于许多原因,这种方法今天并未广泛使用。DSM 最大的问题是它如何处理故障。例如,想象一下,如果机器出现故障。那台机器上的页面会发生什么?如果分布式计算的数据结构分布在整个地址空间怎么办?在这种情况下,这些数据结构的一部分将突然变得不可用。如果部分地址空间丢失,处理故障会很难。想象一下链表,其中下一个指针指向已经消失的地址空间的一部分。
另一个问题是性能。人们通常认为,在编写代码时,访问内存的成本很低。在DSM 系统中,一些访问是便宜的,但是其他访问导致页面错误和远程机器的昂贵提取。因此,这种DSM 系统的程序员必须非常小心地组织计算,以便几乎不发生任何通信,从而打败了这种方法的主要出发点。虽然在这个领域进行了大量研究,但实际影响不大。没有人用DSM构建可靠的分布式系统。
 
 

远程过程调用(RPC)

虽然最终结果表明,操作系统抽象对于构建分布式系统来说是一个糟糕的选择,但编程语言(PL)抽象要有意义得多。最主要的抽象是基于远程过程调用(Remote Procedure Call),或简称RPC。
远程过程调用包都有一个简单的目标:使在远程机器上执行代码的过程像调用本地函数一样简单直接。因此,对于客户端来说,进行一个过程调用,并在一段时间后返回结果。服务器只是定义了一些它希望导出的例程。其余的由RPC 系统处理,RPC 系统通常有两部分:存根生成器(stub generator,有时称为协议编译器,protocol compiler)和运行时库(run-time library)。
 
存根生成器
存根生成器的工作很简单:通过自动化,消除将函数参数和结果打包成消息的一些痛苦。这有许多好处:通过设计避免了手工编写此类代码时出现的简单错误。此外,存根生成器也许可以优化此类代码,从而提高性能。
这种编译器的输入就是服务器希望导出到客户端的一组调用。从概念上讲,它可能就像这样简单:
存根生成器类似于写好接口文档,自动生成一个头文件,可以被其他函数调用。
在内部,客户端存根中的每个函数都执行远程过程调用所需的所有工作。对于客户端,代码只是作为函数调用出现(例如,客户端调用func1(x))。
在内部,func1()的客户端存根中的代码执行此操作:
  • 创建消息缓冲区。消息缓冲区通常只是某种大小的连续字节数组。
  • 将所需信息打包到消息缓冲区中。该信息包括要调用的函数的某种标识符,以及函数所需的所有参数(例如,在上面的示例中,func1 需要一个整数)。将所有这些信息放入单个连续缓冲区的过程,有时被称为参数的封送处理(marshaling)或消息的序列化(serialization)。
  • 将消息发送到目标 RPC 服务器。与 RPC 服务器的通信,以及使其正常运行所需的所有细节,都由 RPC 运行时库处理,如下所述。
  • 等待回复。由于函数调用通常是同步的(synchronous),因此调用将等待其完成。
  • 解包返回代码和其他参数。如果函数只返回一个返回码,那么这个过程很简单。但是,较复杂的函数可能会返回更复杂的结果(例如,列表),因此存根可能也需要对它们解包。此步骤也称为解封送处理(unmarshaling)或反序列化(deserialization)。
  • 返回调用者。最后,只需从客户端存根返回到客户端代码。
 
对于服务器,也会生成代码。在服务器上执行的步骤如下:
  • 解包消息。此步骤称为解封送处理(unmarshaling)或反序列化(deserialization),将信息从传入消息中取出。提取函数标识符和参数。
  • 调用实际函数。终于,我们到了实际执行远程函数的地方。RPC 运行时调用 ID 指定的函数,并传入所需的参数。
  • 打包结果。返回参数被封送处理,放入一个回复缓冲区。
  • 发送回复。回复最终被发送给调用者。
问题1:一个包如何发送复杂的数据结构?需要合理序列化
问题2:并发性的服务器组织方式?常见的组织方式是线程池(thread pool)。在这种组织方式中,服务器启动时会创建一组有限的线程。消息到达时,它被分派给这些工作线程之一,然后执行 RPC 调用的工作,最终回复。在此期间,主线程不断接收其他请求,并可能将其发送给其他工作线程。
 
运行时库
如何找到远程服务?需要命名解析,如DNS。
TCP作为可靠传输协议有性能损失。
许多 RPC 软件包都建立在不可靠的通信层之上,例如 UDP。这样做可以实现更高效的 RPC 层,但确实增加了为 RPC 系统提供可靠性的责任。
通过使用某种形式的序列编号,通信层可以保证每个 RPC 只发生一次(在没有故障的情况下),或者最多只发生一次(在发生故障的情况下)。
 
其他问题
当远程调用需要很长时间才能完成时,一种解决方案是在没有立即生成回复时使用显式确认(从接收方到发送方)。这让客户端知道服务器收到了请求
运行时还必须处理具有大参数的过程调用,发送方分组(fragmentation,较大的包分成一组较小的包)和接收方重组(reassembly,较小的部分组成一个较大的逻辑整体)。
许多系统要处理的一个问题是字节序(byte ordering)。有些机器存储值时采用所谓的大端序(big endian),而其他机器采用小端序(little endian)。
之前提到服务端可以异步处理请求,客户端也要能异步调用接口,客户端在某些时候会希望看到异步 RPC 的结果。因此它再次调用 RPC 层,告诉它等待未完成的 RPC 完成,此时可以访问返回的结果。
 
  • 计算机基础
  • 操作系统
  • 单机数据持久-数据完整性和保护联机数据持久-AFS 与NFS
    目录