Lease 机制

Lease 机制是最重要的分布式协议,广泛应用于各种实际的分布式系统中。

即使在某些系统中相似的设计不被称为 lease ,但我们可以分析发现其本质就是一种 lease 的实现。

本节从一个分布式cache 系统出发介绍最初的 lease 机制,接着加以引申,探讨 lease 机制的本质。

最后介绍了 lease 机制最重要的应用:判定节点状态。

基于 lease 的分布式 cache 系统

问题背景

在一个分布式系统中,有一个中心服务器节点,中心服务器存储、维护着一些数据,这些数据是系统的元数据。

系统中其他的节点通过访问中心服务器节点读取、修改其上的元数据。

由于系统中各种操作都依赖于元数据,如果每次读取元数据的操作都访问中心服务器节点,那么中心服务器节点的性能成为系统的瓶颈。

为此,设计一种元数据 cache ,在各个节点上cache 元数据信息,从而减少对中心 服务器节点的访问,提高性能。

另一方面,系统的正确运行严格依赖于元数据的正确,这就要求各个节点上 cache 的数据始终与中心服务器上的数据一致, cache中的数据不能是旧的脏数据。

最后,设计的 cache 系统要能最大可能的处理节点宕机、网络中断等异常,最大程度的提高系统的可用性。

基于 lease 设计 cache

中心服务器在向各节点发送数据时同时向节点颁发一个 lease 。

每个 lease 具有一个有效期,和信用卡上的有效期类似,lease 上的有效期通常是一个明确的时间点,例如 12:0 0:10 ,一旦真实时间超过这个时间点,则 lease 过期失效。

这样 lease 的有效期与节点收到 lease 的时间无关,节点可能收到 lease 时该 lease 就已经过期失效。

这里首先假设中心服务器与各节点的时钟是同步的,下节中讨论时钟不同步对 lease 的影响。

中心服务器发出的 lease 的含义为:在 lease 的有效期内,中心服务器保证不会修改对应数据的值。

因此,节点收到数据和 lease 后,将数据加入本地 Cache,一旦对应的 lease 超时,节点将对应的本地 cache数据删除。

中心服务器在修改数据时,首先阻塞所有新的读请求,并等待之前为该数据发出的所有lease 超时过期,然后修改数据的值。

ps: 这里通过等待,让 lease 超时,所以 lease 的超时时间不能太久。不然不可用的时间就会很长。

核心

上述 lease 机制可以容错的关键是:服务器一旦发出数据及 lease ,无论客户端是否收到,也无论后续客户端是否宕机,也无论后续网络是否正常服务器只要等待 lease 超时,就可以保证对应的客户端节点不会再继续 cache 数据,从而可以放心的修改数据而不会破坏 cache 的一致性。

性能和可用性问题

上述基础流程有一些性能和可用性上的问题,但可以很容易就优化改性。

优化点一:服务器在修改元数据时首先要阻塞所有新的读请求,造成没有读服务。

这是为了防止发出新的 lease 从而引起不断有新客户端节点持有 lease 并缓存着数据,形成“活锁” 。

优化的方法很简单,服务器在进入修改数据流程后,一旦收到读请求则只返回数据但不颁发 lease 。

从而造成在修改流程执行的过程中,客户端可以读到元数据,只是不能缓存元数据。

进一步的优化是,当进入修改流程,服务器颁发的lease 有效期限选择为已发出的 lease 的最大有效期限。

这样做,客户端可以继续在服务器进入修改流程后继续缓存元数据,但服务器的等待所有 lease 过期的时间也不会因为颁发新的 lease 而不断延长。

实际使用中,第一层优化就足够了,因为等待 lease 超时的时间会被“优化点二”中的优化方法大大减少。

优化点二:服务器在修改元数据时需要等待所有的 lease 过期超时,从而造成修改元数据的操作时延大大增大。

优化的方法是,在等待所有的 lease 过期的过程中,服务器主动通知各个持有lease 的节点放弃 lease 并清除 cache 中的数据,如果服务器收到客户端返回的确认放弃 lease 的消息,则服务器不需要在等待该 lease 超时。

该过程中,如果因为异常造成服务器通知失败或者客户端节点发送应答消息失败,服务器只需依照原本的流程等待 lease 超时即可,而不会影响协议的正确性。

cache 机制与多副本的差异

最后,我们分析一下cache 机制与多副本机制的区别。

Cache 机制与多副本机制的相似之处都是将一份数据保存在多个节点上。

但Cache 机制却要简单许多,对于 cache 的数据,可以随时删除丢弃,并命中 cache 的后果仅仅是需要访问数据源读取数据;

然而副本机制却不一样,副本是不能随意丢弃的,每失去一个副本,服务质量都在下降,一旦副本数下降到一定程度,则往往服务将不再可用。

lease机制的分析

上节中介绍了一个lease 的实例,本节进一步分析 lease 机制的本质。

定义

Lease 是由颁发者授予的在某一有效期内的承诺。

颁发者一旦发出 lease ,则无论接受方是否收到,也无论后续接收方处于何种状态,只要 lease 不过期,颁发者一定严守承诺;

另一方面,接收方在 lease 的有效期内可以使用颁发者的承诺,但一旦 lease 过期,接收方一定不能继续使用颁发者的承诺。

由于lease 是一种承诺,具体的承诺内容可以非常宽泛,可以是上节的例子中数据的正确性;也可以是某种权限,例如当需要做并发控制时,同一时刻只给某一个节点颁发 lease ,只有持有 lease的节点才可以修改数据;也可以是某种身份,例如在 primary secondary( 2.2.2 架构中,给节点颁发lease ,只有持有 lease 的节点才具有 primary 身份。

Lease 的承诺的内涵还可以非常宽泛,这里不再一一列举。

容错能力

Lease 机制具有很高的容错能力。

首先,通过引入有效期,Lease 机制能否非常好的容错网络异常。

Lease 颁发过程只依赖于网络可以单向通信,即使接收方无法向颁发者发送消息,也不影响 lease的颁发。

由于 lease 的有效期是一个确定的时间点, lease 的语义与发送 lease 的具体时间无关,所以同一个 lease 可以被颁发者不断重复向接受方发送。(幂等性)

即使颁发者偶尔发送 lease 失败,颁发者也可以简单的通过重发的办法解决。

一旦 lease 被接收方成功接受,后续 lease 机制不再依赖于网络通信,即使网络完全中断 lease 机制也不受影响。

再者, Lease 机制能较好的容错节点宕机。

如果颁发者宕机,则宕机的颁发者通常无法改变之前的承诺,不会影响 lease 的正确性。

在颁发者机恢复后,如果颁发者恢复出了之前的 lease 信息,颁发者可以继续遵守 lease 的承诺。

如果颁发者无法恢复 lease信息,则只需等待一个最大的 lease 超时时间就可以使得所有的 lease 都失效,从而不破坏 lease 机制。

最后, lease 机制不依赖于存储。

颁发者可以持久 化颁发过的 lease 信息,从而在宕机恢复后可以使得在有效期的 lease 继续有效。

但这对于 lease 机制只是一个优化,如之前的分析,即使颁发者没有持久化 lease 信息,也可以通过等待一个最大的 lease 时间的方式使得之前所有颁发的 lease 失效,从而保证机制继续有效。

时钟一致性问题

Lease 机制依赖于有效期,这就要求颁发者和接收者的时钟是同步的。

一方面,如果颁发者的时钟比接收者的时钟慢,则当接收者认为 lease 已经过期的时候,颁发者依旧认为 lease 有效。

接收者可以用在 lease 到期前申请新的 lease 的方式解决这个问题。

另一方面,如果颁发者的时钟比接收者的时钟快,则当颁发者认为 lease 已经过期的时候,接收者依旧认为 lease 有效,颁发者可能将 lease颁发给其他节点,造成承诺失效,影响系统的正确性。

对于这种时钟不同步,实践中的通常做法是将颁发者的有效期设置得比接收者的略大,只需大过时钟误差就可以避免对 lease 的有效性的影响。

基于lease机制确定节点状态

在分布式系统中确定一个节点是否处于正常工作状态是一个困难的问题。

由于可能存在网络分化,节点的状态是无法通过网络通信来确定的。

例子

下面举一个较为具体的例子来讨论这个问题 。

在一个 primary secondary 架构的系统中,有三个节点 A 、 B 、 C 互为副本,其中有一个节点为 primary ,且同一时刻只能有一个 primary 节点。

另有一个节点 Q 负责判断节点 A 、 B 、 C的状态,一旦 Q 发现 primary 异常,节点 Q 将选择另一个节点作为 primary 。

假设最开始时节点 A为 primary B 、 C 为 secondary 。节点 Q 需要判断节点 A 、 B 、 C 的状态是否正常。

心跳方案

首先需要说明的是基于“心跳”(heartbeat) 的方法无法很好的解决这个问题。

节点 A 、 B 、 C 可以周期性的向 Q 发 送心跳信息,如果节点 Q 超过一段时间收不到某个节点的心跳则认为这个节点异常。

这种方法的问题是假如节点 Q 收不到节点 A 的心跳,除了节点 A 本身的异常外,也有可能是因为节点 Q 与节点 A 之间的网络中断导致的。

在工程实践中,更大的可能性不是网络中断,而是节点 Q 与节点 A 之间的网络拥塞造成的所谓“瞬断”,“瞬断”往往很快可以恢复。

另一种原因甚至是节点 Q 的机器异常,以至于处理节点 A 的心跳被延迟了,以至于节点 Q 认为节点 A 没有发送心跳。

假设节点 A 本身工作正常,但 Q 与节点 A 之间的网络暂时中断,节点 A 与节点 B 、 C 之间的网络正常。

此时节点 Q 认为节点 A 异常,重新选择节点 B 作为新的 primary ,并通知节点 A 、 B 、 C 新的 primary 是节点 B 。

由于节点 Q 的通知消息到达节点 A 、 B 、 C 的顺序无法确定,假如先到达 B则在这一时刻,系统中同时存在两个工作中的 primary ,一个是 A 、另一个是 B 。

假如此时 A 、 B 都接收外部请求并与 C 同步数据,会产生严重的数据错误。

上述即所谓“双主”问题,虽然看似这种问题出现的概率非常低,但在工程实践中,笔者不止一次见到过这样的情况发生。

上述问题的出现的原因在于虽然节点 Q 认为节点 A 异常,但节点 A 自己不认为自己异常,依旧作为 primary 工作。

其问题的本质是由于网络分化造成的系统对于“节点状态”认知的不一致

解决思路

上面的例子中的分布式协议依赖于对节点状态认知的全局一致性,即一旦节点Q 认为某个节点A 异常,则节点 A 也必须认为自己异常,从而节点 A 停止作为 primary ,避免“双主”问题的出现。

解决这种问题有两种思路.

第一、设计的分布式协议可以容忍“双主”错误,即不依赖于对节点状态的全局一致性认识,或者全局一致性状态是全体协商后的结果;

第二、利用 lease 机制。

对于第一种思路即放弃使用中心化的设计,而改用去中心化设计,超过本节的讨论范畴。

下面着重讨论利用 lease 机制确定节点状态。

lease 解决方案

由中心节点向其他节点发送 lease ,若某个节点持有有效的 lease ,则认为该节点正常可以提供服务。

节点 A 、 B 、 C 依然周期性的发送 heart beat 报告自身状态,节点 Q 收到 heart beat 后发送一个 lease ,表示节点 Q 确认了节点 A 、 B 、 C 的状态,并允许节点在 lease 有效期内正常工作。

节点 Q 可以给 primary 节点一个特殊的 lease ,表示节点可以作为 primary 工作。

一旦节点 Q 希 望切换新的 primary ,则只需等前一个 primary 的 lease 过期,则就可以安全的颁发新的 lease 给新的 primary 节点,而不会出现“双主”问题。

在实际系统中,若用一个中心节点发送lease 也有很大的风险,一旦该中心节点宕机或网络异常,则所有的节点没有 lease ,从而造成系统高度不可用。

为此,实际系统总是使用多个中心节点互为副本,成为一个小的集群,该小集群具有高可用性,对外提供颁发 lease 的功能。

chubby 和 zookeeper 都是基于这样的设计。

ps: 这种方案的优势就是,当网络不同的时候,一定是以 Q 中心为准的,因为 lease 到期,A 如果通讯不可达,也会放弃作为 primary。

lease 的有效期时间选择

Lease 的有效期虽然是一个确定的时间点,当颁发者在发布 lease 时通常都是将当前时间加上一个固定的时长从而计算出 lease 的有效期。

如何选择 L ease 的时长在工程实践中是一个值得讨论的问题。

如果 lease 的时长太短,例如 1s ,一旦出现网络抖动 lease 很容易丢失,从而造成节点失去 lease使得依赖 lease 的服务停止;如果 lease 的时长太大,例如 1 分钟,则一旦接受者异常,颁发者需要过长的时间收回 lease 承诺。

例如,使用 lease 确定节点状态时,若 lease 时间过短,有可能造成网络瞬断时节点收不到 lease 从而引起服务不稳定,若 lease 时间过长,则一旦某节点宕机异常,需 要较大的时间等待 lease 过期才能发现节点异常。

工程中,常选择的 lease 时长是 10 秒级别,这是一个经过验证的经验值,实践中可以作为参考并综合选择合适的时长。

间接使用Lease

笔者很难想象,如何在工程上既不使用 Lease 而又实现一个一致性较高的系统。

直接实现 lease 机制的确会对增加系统设计的复杂度。

然而,由于有类似 Zo okeeper 这样的开源的高可用系统,在工程中完全可以 间接 使用 Lease 。

借助 zookeeper ,我们可以简单的实现高效的、无单点选主、状态监控、分布式锁、分布式消息队列等功能,而实际上,这些功能的实现都是依赖于背后 zookeeper 与 client 之间的 Lease 的。

ps: 所以还是应该学习背后的本质。实际工程中,使用成熟的中间件即可。

参考资料

《分布式系统原理》