文章目录
本章对分布式系统可能出现的故障作出了一个全面,近乎悲观的总结。故障可能来自网络问题,以及时钟与时序问题等等,并讨论这些问题的可控程度。关键在于,分布式系统与在单节点上的软件有着非常显著的区别:很多在单节点上不可能出现的问题,却是困扰分布式系统的大问题!
1. 不确定性与部分失效
1.1 单节点的确定-正确性
在单节点上开发程序时,通常它会以一种确定-正确性的方式运行,不会出现模棱两可的现象:要么工作,要么出错,而不会介于两者之间。
这背后设计计算机设计的一个非常审慎的选择:如果发生了某种内部错误,我们宁愿使计算机全部崩溃,也不愿返回一个错误的结果,错误的结果往往更难处理。这种确定-正确性的设计思想可以一直追溯到第一台数字计算机。
1.2 分布式系统的不确定性
然而,对于分布式系统,理想化的确定-正确性模型不再适用。可能出现系统的一部分工作正常,但其他某些部分出现难以预测的故障,这被称为“部分失效”。
问题的难点在于这种部分失效是不确定的:因为涉及多个节点和网络,哪个节点何时出问题?是完全不可预料的。正是这种不确定性和部分失效极大地提高了分布式系统的复杂性。
要使分布式系统可靠工作,就必然面临部分失效,这就需要依靠软件系统来提供容错机制。换句话说,我们要在不可靠的组件上构建可靠的系统。
2. 不可靠的网络
2.1 可能存在的网络故障
分布式系统一般采用无共享架构的水平扩展,即通过网络连接多个节点,网络是跨节点通信的唯一途径。一个节点可以发送消息到另一个节点,但网络并不保证它是否能到达,什么时候到达。发送之后等待响应的过程中,有很多事情可能会出错,常见的出错有:
(a)请求丢失;(b)远程节点关闭;(c)响应丢失
这些问题在一个异步网络中无法明确区分,发送者拥有的唯一信息是:尚未收到响应。至于原因,无法判断。
2.2 解决方法:超时
处理网络故障问题通常采用超时机制:在等待一段时间后,如果仍然没有收到回复则选择放弃,并认为响应不会到达。
问题:超时设置多长?
两难:较长的时长意味着更长时间的等待,这会影响性能和效率;较短的时长可以帮助快速检测故障,但误判几率增大。
对于分布式系统,没有公式条例可以设置超时时长。
只能通过实验方式来一步步设置超时:先在多台机器上,多次测量网络往返时间,以确定延迟的大概范围;然后结合应用特点,在故障检测与过早超时风险之间选择一个合适的中间值。
更好的做法是:超时设置并不是一个不变的常量,而是持续测量响应时间及其变化,然后根据最新的响应时间分布来自动调整。
3. 不可靠的时钟
在分布式系统中,时间总是件棘手的问题。
一方面,延迟带来了不确定性。由于跨节点通信不可能即使完成,消息经由网络从一台机器到另一台机器总是需要花费时间。理论上,收到消息的时间应该晚于发送的时间,但是由于网络的不确定延迟,精确测量面临着很多挑战。这些情况使得多节点通信时间很难确定事情发生的先后顺序。
另一方面,每台机器都有自己的时钟硬件设备,导致各自的时间本来就可能不同。因此,我们需要同步机器之间的时钟,最常用的方法是网络时间协议(Network Time Protocol,NTP),它可以根据一组专门的时间服务器来调整本地时间,时间服务器则从精确更高的时间源获得高精度时间。
两个时钟
现代计算机内部至少有两种不同的时钟:墙上时钟,单调时钟。
墙上时钟
墙上时钟是根据某个日历(墙上时间)返回当前的日期与时间。比如,Linux的clock_gettime(CLOCK_REALTIME)和java中的System.currentTimeMillis(),取时都是自1970年1月1日(UTC)以来的秒数和毫秒数。
墙上时钟可以与NTP同步,并且可以回拨。如果本地时钟远快于NTP服务器,强行重置后会跳回到先前的某个时间点。
单调时钟
单调时钟用于测量持续时间段(时间间隔)。比如,Linux的clock_gettime(CLOCK_MONOTONIC)和java中的System.nanoTime()。单调时钟的名字来源于它们保证总是向前,而不会出现墙上时钟的回拨。单调时钟的绝对值没有任何意义,相对值才有意义。
单调时钟不需要同步,不可以回拨。
时钟的置信区间
或许墙上时钟会返回微秒甚至纳秒级别的信息,但是这种精度的测量值其实并不可行。如前所述,石英漂移问题可导致偏差高达几毫秒,即使每分钟都与本地网络的NTP服务器进行同步,也无法保证上述精度。
因此,我们不应该将时钟读数视为一个精确的时间点,而更应该视为带有置信区间的时间范围。
依赖同步的时钟
对于分布式系统,如果需要精确同步的时钟,最好仔细监控所有节点上的时钟偏差。如果某个节点的时钟漂移超出上限,应将其宣告为失效,并从集群中移除。这样的监控目的是确保在造成重大影响之前尽早发现并处理问题。
4. 进程暂停
对于分布式系统,假设数据库每个分区只有一个主节点,只有主节点才能接受写入。那么其他节点如何确定该主节点没有被宣告失效?
典型方法是:主节点从其他节点获得一个租约,类似一个=带有超时的锁。某一个时间只有一个节点可以拿到租约,某节点获得租约之后,在租期到期之前,它就是这段时间内的主节点。为了维持主节点的身份,节点必须在到期之前定期去更新租约。如果节点发生故障,则续约失败,这样另一个节点到期之后就可以接管。
分布式系统中的一个节点必须假定,执行过程中的任何时刻都可能被暂停相当长一段时间,包括运行在某个函数中间。暂停期间,整个集群的其它部分都在照常运行,甚至会一致将暂停的节点宣告为故障节点。最终,暂停的节点可能会回来继续运行,除非再次检查时钟,否则它对刚刚过去的暂停毫无意识。
5. 谎言与决定
幽幽子没有意识到,书中记载的逝去少女其实就是她自己,西行妖的开放将与自己的死联系在一起,西行妖满开的时刻永远不会到来!你永远都无法到达真实,这就是真实的世界。——《东方project》
拜占庭将军问题
如果节点存在“撒谎”的情况,那么分布式系统处理的难度就上了一个台阶。例如,节点明明没有收到某条信息,却对外声称收到了。这种行为被称为“拜占庭故障”。在这样不信任的环境中需要达成共识的问题也被称为拜占庭将军问题。
如果某个系统中即使发生部分节点故障,甚至不遵从协议,或者恶意攻击、干扰网络,但是仍可以继续正常运行,那么我们称之为拜占庭式容错系统。解决拜占庭容错的系统协议异常复杂,而容错的嵌入式系统还依赖于硬件层面的支持。因而在绝大多数服务器,部署拜占庭容错解决方案不太可行。