使用分布式系统与在单机系统中处理问题有很大的区别,分布式系统带来了更大的处理能力和存储容量之后,也带来了很多新的"烦恼"。在这一篇之中,我们将看看分布式系统带给我们新的挑战。
1.故障
当我们在使用单机系统时,它通常以一种相当可预测的方式工作:要么它正常工作,要么不工作。
而当我们在使用分布式系统时,情况就不同了。在分布式系统中,系统的某些部分可能以某种不可预知的方式被破坏,即使系统的其他部分工作正常。这种故障通常是不确定的:如果你想做涉及多个节点和网络的东西,可能甚至不知道某个消息是否成功,因为消息穿越网络所需的时间也是不确定的。
这种故障的不确定性,使得分布式系统的变得复杂而脆弱。一个系统越大,它的组件就越有可能出现故障。在一个有成千上万个节点的系统中,某些东西总是会出现故障。而错误处理策略仅仅是简单的放弃的话,一个大系统可能会花费大量时间从故障中恢复,而不是做有用的工作。所以我们需要分布式系统能够容忍失败的节点,并且仍然保持整体工作,将容错机制建立到软件中。换句话说,分布式系统需要从不可靠的组件中建立一个可靠的系统。
2.不可靠的网络
分布式系统是一组由网络连接的机器组成的。网络是这些机器通信的唯一方式,每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘。在网络中,一个节点可以向另一个节点发送消息,但是网络不能保证它何时到达或是否到达,所以网络是不可靠的。
如上图所示,如果发送的请求并没有得到响应,则无法区分
(a)请求丢失
(b)远程节点失效
(c)响应丢失。
处理这个问题的通常方法是超时:一段时间后,发送方放弃等待,并假定响应不会到达。 但是,当超时发生时,远程节点可能已经得到请求并进行了处理。
故障检测
由于网络的不确定性使得很难判断一个节点是否工作。分布式系统当中常用的便是超时检测的机制。如果超时检测是检测故障的方法,那么超时应该是多长时间呢?不幸的是,没有简单的答案。
长的超时时间意味着需要等待一个节点被宣告死亡。短的超时时间会更快地检测到故障,但是事实上节点并没有停止工作(例如由于节点或网络过载)时,会错误地检测一个节点失效。如果节点实际上是活着的,在执行某些操作的时,工作另一个节点接管,则该操作可能最终执行两次。而且当一个节点失效时,它的责任需要转移到其他节点,这将额外的负载放到其他节点和网络上。如果系统已经处于高负载之下,过早检测节点失效会使问题变得更糟。特别是,它可能发生的是节点实际上没有时效,但由于过载而响应缓慢,将其负载转移到其他节点会导致级联故障。
目前学界和业界的趋势是:不使用常数配置的超时,而是系统可以连续测量的响应时间和响应时间的抖动,并自动调整超时时间根据所观察到的响应时间动态分布。如Akka的超时器,Cassandra的动态检测,TCP的超时重传。
3.不可靠的时间
在分布式系统中,时间是一件棘手的事情,因为通信不是瞬时的:消息穿越网络从一台机器转到另一台机器需要时间。消息接收的时间总是比发送的时间晚,但由于网络中的可变延迟,我们不知道以后会有多少延迟。很难确定多台机器处理的逻辑与顺序。
每台机器都有自己的时钟,通常是一个石英晶体振荡器。这些设备并不完全准确,所以每台机器都有自己的时间,它可能比其他机器稍快或慢一些。存在同步时钟的网络协议:最常用的机制是网络时间协议(NTP),它允许计算机时钟根据一组服务器报告的时间进行调整。服务器可以从更精确的时间源获取时间。
时钟:
UTC时间以1970年1月1日为开始,根据公历,忽略闰秒,来计算当前时间。计算机时钟通常与NTP同步,这意味着一台机器的时间戳(理想情况下)意味着与另一台机器上的时间戳相同。
单调的时间:
您可以在一个时间点检查时钟的值,然后再一次检查时钟。两个值之间的差异告诉你这两个检查之间要花多少时间。在分布式系统中,通过一个单调的时钟测量时间(如超时)通常是好的,因为它不承担不同的节点的时钟之间的同步的细微误差。
事件的时间戳排序
跨多个节点的事件排序是一个令人头疼的问题。例如,如果两个客户机向分布式数据库写入,谁首先到达?哪个是最近写的? 如下图所示:
写x = 1的时间戳是42.004秒,但写x = 2的时间戳42.003秒。当Node 2接收到这两个事件时,它会错误地得出结论:x = 1是最新的值,忽略x=2的写入。Client B的增量操作将会丢失。这种冲突解决策略被称为最后写者胜(LWW),会导致一个具有滞后时钟的节点无法覆盖以前用一个快速时钟写入的节点的值,直到节点之间的时钟偏差消失。
所以对于有严格时序要求的系统,需要使用逻辑时钟(比如:Lamport Clock,Lanport老爷子真的是分布式领域的上古神牛啊~~~),这是基于递增计数器是一个来判断事件的更迭顺序。逻辑时钟不测量每天的时间或经过的秒数,只有事件的相对顺序,也就是判断一个事件是否发生在另一个事件之前或之后。
4.不可靠的租约
在分布式系统之中,有时需要确保在存储服务文件只能同时被一个客户端访问,因为如果多个客户端试图写它,文件会被损坏。您需要通过在访问文件之前从锁服务获得租约来实现分布式锁。但是有时这个锁并非有我们想象的可靠,如下图所示:
如果持有租约的客户端 1 因为GC等原因暂停太久,而它的租约到期了。另一个客户端 2 可以获取租约,并开始向文件写入数据。当暂停的客户端1返回时,它仍然认为自己拥有一个有效的租约,并且继续写入数据。于是造成了写入冲突。
栅栏令牌
我们可以使用栅栏令牌的方式,让不可靠的租约变的更加可靠,如下图所示:
锁服务器可以在每次授予租约时,返回一个令牌,它是一个在每次授予锁时增加的数字ID。每次客户端发出一个写请求时,必须包含当前的租约令牌。而存储服务会记录写入的租约令牌,成为一个栅栏,旧的令牌写入将被存储服务拒绝。
小结:
分布式系统最大的挑战是我们需要在不可靠的组件与复杂的多节点交互之中建立起一个可靠的系统,所以也需要我们付出更多的努力。我这里略过了拜占庭问题的讲解,通常我们开发的数据系统认为是拜占庭安全的,节点是可以信任的。