Skip to content

Latest commit

 

History

History
93 lines (50 loc) · 8.75 KB

8.2. 分布式系统的挑战(2).md

File metadata and controls

93 lines (50 loc) · 8.75 KB

知识,真相与谎言

真相由多数决定

假定在一个发生了非对称故障的网络环境中,即某节点能够收到发送给它的消息,但是该节点发出的所有消息要么被丢弃,要么被推迟发送。该节点即使本身运行良好,可以接收来自其他节点的请求,但其他节点却无法顺利收到响应。当消息超时之后,由于都收不到回复,其他节点就会一致声明上述节点发生失效。

接下来是一个情况稍好的场景,半断开的节点可能会注意到其发送的消息没有被其他节点所确认,因此意识到网络一定发生了某种故障。尽管如此,节点还是会被其他节点错误地宣告为失效,改变不了该节点最终的命运。

第三种情况,该节点上垃圾回收运行了很长时间,所有线程包括那些事务处理任务都被 GC 抢占并暂停了足足一分钟,在此期间,没有处理任何请求,也没有发送任何响应。那么其他节点只能苦苦等待,不停重试,最后无奈宣布该节点巳经失效。可节点最终还是完成了垃圾回收,原有的工作线程得以继续,好像什么都没有发生。

因此节点不能根据自己的信息来判断自身的状态。由于节点可能随时会失效,可能会暂停-假死,甚至最终无法恢复,因此,分布式系统不能完全依赖于单个节点。目前,许多分布式算法都依靠法定票数,即在节点之间进行投票(参阅第 5 章 “读写quorum”)。任何决策都需要来自多个节点的最小投票数,从而减少对特定节点的依赖。

这其中包括关于宣告节点失效的决定。如果有法定数量的节点声明另一个节点失效,即使该节点仍感觉活得很自在,那它也必须接受失效的裁定,所有个体节点必须遵循法定投票的决议然后离线。

最常见的法定票数是取系统节点半数以上(也有其他类型的法定人数)。如果某些节点发生故障,quorum 机制可以使系统继续工作。

主节点与锁⭐

有很多情况,我们需要在系统范围内只能有一个实例。例如:

  • 只允许一个节点作为数据库分区的主节点,以防止出现脑裂。
  • 只允许一个事务或客户端持有特定资源的锁,以防止同时写入从而导致数据破坏。
  • 只允许一个用户来使用特定的用户名,从而确保用户名可以唯一标识用户。

在分布式系统实现时需要额外注意:==即使某个节点自认为它是 “唯一的那个"(例如分区的主节点,锁的持有者,成功拿走用户名的请求),但不一定获得了系统法定票数的同意!==一个节点可能以前确实是主节点,但其他节点有可能在此期间已宣布其失效(例如,出现了网络中断或 GC 暂停),节点已被降级而系统选出了另一个主节点。

当多数节点声明节点已失效,而该节点还继续充当 “唯一的那个",如果系统设计不周就会导致负面后果。该节点会按照自认为正确的信息向其他节点发送消息,其他节点如果还选择相信它,那么系统就会出现错误的行为。

例如图 8-4 展示了由于不正确的加锁而导致数据破坏的例子。

image-20220124223125375

这个问题属于前面 “进程暂停” 中的一种情况:持有租约的客户端被暂停太久直到租约到期。然后另一个客户端已经获得了文件的锁租约,并开始写文件。接下来,当暂停的客户端重新回来时,它仍然(错误地)认为合法持有锁并尝试写文件。结果导致客户 2 的文件写入被破坏。

Fencing tokens⭐

当使用锁和租约机制来保护资源的并发访问时(见图 8-4),必须确保过期的 “唯一的那个” 节点不能影响其他正常部分。要实现这一目标,可以采用一种相当简单的技术 ==fencing==,如图 8-5 所示。

我们假设每次锁服务在授予锁或租约时,还会同时返回一个 fencing 令牌,该令牌(数字)每授授予一次就会递增(例如,由锁服务增加)。然后,要求客户端每次向存储系统发送写请求时,都必须包含所持有的 fencing 令牌。

图 8-5 中,客户端 1 获得锁租约的同时得到了令牌号 33,但随后陷入了一个长时间的暂停直到租约到期。这时客户端 2 已经获得了锁租约和令牌号 34,然后发送写请求(以及令牌号 34)到存储服务。接下来客户端 1 恢复过来,并以令牌号 33 来尝试写入,存储服务器由于记录了最近已经完成了更高令牌号(34),因此拒绝令牌号 33 的写请求。

image-20220124223409525

当使用 ZooKeeper 作为锁服务时,可以用事务标识 zxid 或节点版本 cversion 来充当 fending 令牌,这两个都可以满足单调递增的要求。

请注意,只靠客户端自己检查锁状态是不够的,这种机制要求资源本身必须主动检查所持令牌信息,如果发现已经处理过更高令牌的请求,要拒绝持有低令牌的所有写请求。

拜占庭故障

fencing 令牌可以检测并阻止那些无意的误操作(例如节点并没有发现其租约已经过期)。但是,如果节点故意试图破坏系统,在发送消息时可以简单地伪造令牌即可。

本书总是假设节点虽然不可靠但一定是诚实的:它们尽管运行很慢或者由于故障而无法响应,或者状态可能已经过期(例如由于 GC 暂停或网络延迟),但一旦做出了响应,则一定是完全基于其所知的全部信息和事先协议约定好的行为准则,响应代表了其所知的 “真相”。

如果节点存在 “撒谎” 的情况(即故意发送错误的或破坏性的响应),那么分布式系统处理的难度就上了一个台阶。例如,节点明明没有收到某条消息,但却对外声称收到了。这种行为称为==拜占庭故障==(Byzantine fault),在这样不信任的环境中需要达成共识的问题也被称为==拜占庭将军问题==(Byzantine Generals Problem)。

如果某个系统中即使发生部分节点故障,甚至不遵从协议,或者恶意攻击、干扰网络,但仍可继续正常运行,那么我们称之为==拜占庭式容错系统==(Byzantine fault-tolerant)。

然而,在本书所讨论的这些系统中,我们可以安全地假定没有拜占庭式的故障。

弱的谎言形式

理论系统模型与现实

目前分布式系统方面已有许多不错的具体算法,如第 9 章中要介绍的共识算法。这些算法需要容忍本章所讨论的各种故障。

算法的实现不能过分依赖特定的硬件和软件配置。这就要求我们需要对预期的系统错误进行形式化描述。我们通过定义一些系统模型来形式化描述算法的前提条件。

关于计时方面,有三种常见的系统模型:

  • 同步模型:同步模型假定有上界的网络延迟,有上界的进程暂停和有上界的时钟误差。
  • 部分同步模型:部分同步意味着系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟,进程暂停和时钟漂移的预期上界。这是一个比较现实的模型:大多数情况下,网络和进程比较稳定(否则几乎不可能提供持续的服务),但是我们必须考虑到任何关于时机的假设都有偶尔违背的情况,而一旦发生,网络延迟,暂停和时钟偏差可能会变得非常大。
  • 异步模型:在这个模型中,一个箕法不会对时机做任何的假设, 甚至里面根本没有时钟(也就没有超时机制)。某些算法可以支持纯异步模型,但并不常见。

除了时机之外,我们还需要考虑节点失效。有以下三种最常见的节点失效系统模型:

  • 崩溃中止模型:在崩溃-中止模型中,算法假设一个节点只能以一种方式发生故障,即遭遇系统崩溃。这意味着节点可能在任何时候突然停止响应,且该节点以后永远消失,无法恢复。
  • 崩溃恢复模型:节点可能会在任何时候发生崩溃,且可能会在一段(未知的)时间之后得到恢复并再次响应。在崩溃-恢复模型中,节点上持久性存储(即非易失性存储)的数据会在崩溃之后得以保存,而内存中状态可能会丢失。
  • 拜占庭(任意)失效模型:如上节所述,节点可能发生任何事情,包括试图作弊和欺骗其他节点。

算法的正确性

安全与活性

将系统模型映射到现实世界