网络异常
网络异常是另一类常见的异常形式。节点间通过不可靠的网络进行通信。下面介绍几种常见的网络异常:1: 消息丢失
消息丢失是最常见的网络异常。对于常见的 IP 网络来说,网络层不保证数据报文(IP fragment)的可靠传递,在发生网络拥塞、路由变动、设备异常等情况时,都可能发生发送的数据丢失。由于网络数据丢失的异常存在,直接决定了分布式系统的协议必须能处理网络数据丢失的情况。依据网络质量的不同,网络消息丢失的概率也不同,甚至可能出现在一段时间内某些节点之间的网络消息完全丢失的情况。如果某些节点的直接的网络通信正常或丢包率在合理范围内,而某些节点之间始终无法正常通信,则称这种特殊的网络异常为“网络分化”(network partition)。网络分化是一类常见的网络异常,尤其当分布式系统部署在多个机房之间时。用虚线分割了两片节点,这两片节点之间彼此完全无法通信,即出现了“网络分化”。
例 :某分布式系统部署于两个机房,机房间使用内部独立光纤链路。由于机房间的光纤链路交割调整,两个机房间通信中断,期间,各机房内的节点相互通信正常。更为严重的是,所有的英特网用户都可以正常访问两个机房内对外服务节点。本文后续将讨论出现这种严重的网络分化时,对分布式系统的设计带来的巨大挑战。
.2 消息乱序
消息乱序是指节点发送的网络消息有一定的概率不是按照发送时的顺序依次到达目的节点。通常由于 IP 网络的存储转发机制、路由不确定性等问题,网络报文乱序也是一种常见的网络异常。这就要求设计分布式协议时,考虑使用序列号等机制处理网络消息的乱序问题,使得无效的、过期的网络消息不影响系统的正确性。
.3 数据错误
网络上传输的数据有可能发生比特错误,从而造成数据错误。通常使用一定的校验码机制可以较为简单的检查出网络数据的错误,从而丢弃错误的数据。
.4 不可靠的 TCP
TCP协议为应用层提供了可靠的、面向连接的传输服务。TCP协议是最优秀的传输层协议之一,其设计初衷就是在可靠的网络之上建立可靠的传输服务。TCP 协议通过为传输的每一个字节设置顺序递增的序列号,由接收方在收到数据后按序列号重组数据并发送确认信息,当发现数据包丢失时,TCP 协议重传丢失的数据包,从而 TCP 协议解决了网络数据包丢失的问题和数据包乱序问题。TCP 协议为每个 TCP 数据段(以太网上通常最大为 1460 字节)使用 32 位的校验和从而检查数据错误问题。TCP 协议通过设置接收和发送窗口的机制极大的提高了传输性能,解决了网络传输的时延与吞吐问题。TCP 协议最为复杂而巧妙的是其几十年来不断改进的拥塞控制算法,使得 TCP 可以动态感知底层链路的带宽加以合理使用并与其他 TCP 链接分享带宽(TCP friendly)。上述种种使得 TCP 协议成为一个在通常情况下非常可靠的协议,然而在分布式系统的协议设计中不能认为所有网络通信都基于 TCP 协议则通信就是可靠的。一方面,TCP 协议保证了 TCP 协议栈之间的可靠的传输,但无法保证两个上层应用之间的可靠通信。通常的,当某个应用层程序通过TCP 的系统调用发送一个网络消息时,即使 TCP 系统调用返回成功,也仅仅只能意味着该消息被本机的 TCP 协议栈接受,一般这个消息是被放入了 TCP 协议栈的缓冲区中。再退一步讲,即使目的机器的 TCP 协议栈后续也正常收到了该消息,并发送了确认数据包,也仅仅意味着消息达到了对方机器的协议栈,而不能认为消息被目标应用程序进程接收到并正确处理了。当发送过程中出现宕机等异常时,TCP 协议栈缓冲区中的消息有可能被丢失从而无法被目标节点正确处理。更有甚者,在网络中断前,某数据包已经被目标进程正确处理,之后网络立刻中断,由于接收方的 TCP 协议栈发送的确认数据包始终被丢失,发送方的 TCP 协议栈也有可能告知发送进程发送失败。另一方面,TCP协议只能保证同一个 TCP 链接内的网络消息不乱序,TCP 链接之间的网络消息顺序则无法保证。但
在分布式系统中,一个节点向另一个节点发送数据,有可能是先后使用多个 TCP 链接发送,也有可能是同时并发多个 TCP 链接发送,那么发送进程不能认为先调用 TCP 系统调用发送的消息就一定会先于后发送的消息到达对方节点并被处理。由上述分析,在设计分布系统的网络协议时即使使用 TCP 协议,也依旧要考虑网络异常,不能简单的认为使用 TCP 协议后通信就是可靠的。另一方面,如果完全放弃使用 TCP 协议。
分布式系统的三态
由于网络异常的存在,分布式系统中请求结果存在“三态”的概念。在单机系统中,我们调用一个函数实现一个功能,则这个函数要么成功、要么失败,只要不发生宕机其执行的结果是确定的。
然而在分布式系统中,如果某个节点向另一个节点发起 RPC(Remote procedure call)调用,即某个节点 A 向另一个节点 B 发送一个消息,节点 B 根据收到的消息内容完成某些操作,并将操作的结果通过另一个消息返回给节点 A,那么这个 RPC 执行的结果有三种状态:“成功”、“失败”、“超时(未知)”,称之为分布式系统的三态。如果请求 RPC 的节点 A 收到了执行 RPC 的节点 B 返回的消息,并且消息中说明执行成功,则该 RPC 的结果为“成功”。如果请求 RPC 的节点 A 收到了执行
RPC 的节点 B 返回的消息,并且消息中说明执行失败,则该 RPC 的结果为“失败”。但是,如果请求 RPC 的节点 A 在给定的时间内没有收到执行 RPC 的节点 B 返回的消息,则认为该操作“超时”。对于超时的请求,我们无法获知该请求是否被节点 B 成功执行了。这是因为,如果超时是由于节点 A 发向节点 B 的请求消息丢失造成的,则该操作肯定没有被节点 B 成功执行;但如果节点 A 成功的向节点 B 发送了请求消息,且节点 B 也成功的执行了该请求,但节点 B 发向节点 A 的结果消息被网络丢失了或者节点
B 在执行完该操作后立刻宕机没有能够发出结果消息,从而造成从节点A看来请求超时。所以一旦发生超时,请求方是无法获知 RPC 的执行结果的。图 1-2 给出了操作成功但超时的例子。
一个非常易于理解的例子是在网上银行进行转账操作,当系统超时,页面提示:“如果系统长时
间未返回,请检查账户余额以确认交易是否成功”。
分布式系统一般需要区别对待 RPC 的“成功”、“失败”、“超时”三种状态。当出现“超时”时可以通过发起读取数据的操作以验证 RPC 是否成功(例如银行系统的做法)。另一种简单的做法是,
设计分布式协议时将执行步骤设计为可重试的,即具有所谓的“幂等性”。例如覆盖写就是一种常见的幂等性操作,因为重复的覆盖写最终的结果都相等。如果使用可重试的设计,当出现“失败”和“超时”时,一律重试操作直到“成功”。这样,即使超时的操作实际上已经成功了,重试操作也不会对正确性造成影响,从而简化了设计。后续本文中,如果说明“不成功”即指“失败”或“超时”两种状态之一。如果说明“失败”、则表示收到了明确的“失败”消息。