分布式架构之「 lease机制」

2,569 阅读15分钟

上一篇文章我们讲了副本协议,副本协议主要是数据读写控制、数据同步、对抗异常、primaty副本的确定与切换。常见的类型有中心化副本控制协议和去中心化副本控制协议。因为网络故障是无法避免的,怎么感知primary副本的状态是否正常。本篇文章讲通过「 lease机制」确定副本的状态。

lease的字面意思是租约,租契,指在一定时间内是按照协议来走,到期后如果不续约协议是作废的。先从一个分布式cache系统出发介绍最初的lease机制,探讨lease机制的本质。


基于lease的分布式cache

场景描述:在一个分布式系统中,有一个中心服务器节点,中心服务器存储、维护着一些数据,这些数据都是元数据。系统中其它的节点通过访问中心服务节点读取、修改其实上的元数据。由于系统中各种操作都依赖于元数据,如果每次读取元数据的操作都访问中心服务器节点,那么中心服务器节点的性能成为系统的瓶颈。为此,设计一种元数据cache,在各个节点上cache的数据始终与中心服务器上的数据一致,cache中的数据不能是旧的脏数据,最后,设计的cache系统要最大可能的处理节点宕机、网络中断等异常,最大程度的提高系统的可用性。

利用lease机制设计一套cache系统,基本原理如下。中心服务器在向各节点发送数据的同时向节点颁发一个lease(租约数据)。每个lease具有一个有效期,和信用卡是哪个的有效期类似,lease上的有效期通常是一个明确的时间点,比如12:00:10,一旦真实节点超过这个时间点,则lease过期失效。这样lease的有效期与节点收到的lease的时间无关,节点可能收到lease时该lease就已经过期失效。这里首先假设中心服务器与各个节点的时钟是同步的,后面讨论时钟不同对对lease的影响。中心服务器发出的lease的含义为:在lease的有效期内。中心服务器保证不会修改对应数据的值。因此,节点收到数据和lease后,将数据加入本地cache,一旦对应的lease超时,节点对应的本地cache数据删除。中心服务器在修改数据时,首先阻塞有所新的读请求,并等等之前为该数据发出的所有lease超时过期,然后修改数据的值。

具体的服务器与客户端节点一个基本流程如下:

基于lease的cache,客户端节点读取元数据,每一端都可能成功或失败。


1、判断元数据是否已经处于本地cache且lease处于有效期内

1.1 是:直接返回cache中的元数据

1.2 否: 向中心服务器节点请求读取元数据信息

1.2.1 服务器收到读取请求后,返回元数据以及一个对应的lease

1.2.2 客户端是否成功收到服务器返回的数据

1.2.2.1 失败或超时:退出流程,读取失败,可重试

1.2.2.2 成功:将元数据与该元数据的lease记录到内存中,返回元数据


基于lease的cache,客户端节点修改元数据流程

1.节点向服务器发起修改元数据请求
2.服务收到修改请求后,阻塞所有新的读数据请求,即接收读请求,但不返回数据。
3.服务器等待所有与该元数据相关的lease超时。
4.服务器修改元数据并向客户端节点返回修改成功。

上述机制可以保证各个节点是上的cache与中心服务器上的数据始终一致。这是因为中心服务器节点在发送数据的同时授予了节点对应的lease,在lease有效期内,服务器不会修改数据,从而客户端可以放心的在lease有效期内cache数据。上述lease机制可以容错的关键是:服务器一旦发出数据以及lease,无论是客户端是否收到,也无论是后续客户端是否宕机,也无论后续网络是否正常,服务器只要等待lease超时,就可以保证对应的客户端节点不会再继续cache数据,从而可以放心的修改数据而不会破坏cache的一致性。

上面的基础流程有一些性能和可用性上的问题,但可以很容易就优化改进。优化点一:服务器在修改元数据时首先要阻塞所有新的读请求,造成没有读服务。这种为了防止发出新lease从而引起不断有新客户端持有lease并缓存着旧的数据。优化方法很简单,服务器在进入修改数据流程后,一旦收到读请求则只返回数据但不颁发lease。客户端可以读到元数据,只是不能缓存元数据。优化点二:服务器修改元数据时需要等待所有的lease过期超时,从而造成修改元数据的操作时延大大增大。优化的方法是,在等待所有lease过期的过程中,服务器主动通知各个持有lease的节点放弃lease并清除cache中的数据,这个是中心节点去推的方式,也可以是客户端自己去watch拉的方式。如果服务器收到客户端返回的确认放弃lease的消息,则服务器不需要再等待该lease超时。该过程中,如果因为异常造成服务器通知失败或者客户端节点发送应答消息失败,服务器只需要依照原本的流程等待lease超时即可,不会影响协议的正确性。

最后,我们分析一下cache机制与多副本机制的区别。cache机制与多副本机制的相似之处都是将一份数据保存在多个节点上。但cache机制却要简单许多,对于cache的数据,可以随时删除丢弃,未命中cache的后果仅仅是需要访问数据源读取数据;然后副本机制却不一样,副本是不能随意丢弃的,每失去一个副本,服务质量都在下降,一旦副本数下降到一定程度,则往往服务将不再可用。


lease机制的分析

lease的定义:lease是由颁发者授予的在某一有效期内的承诺。颁发者一旦发出lease,则无论接受方是否收到,无论后续接收方处于何种状态,只要lease不过期,颁发者一定要严守承诺;另一方面,接收方在lease的有效期内可以使用颁发者的承诺,但一旦lease过期,接收方一定不能继续使用颁发者承诺。

由于lease是一种承诺,具有的承诺内容可以非常的宽泛,可以是上面提到的数据的正确性;也可以是某种权限,例如当需要做并发控制时,同一时刻只给某一个节点颁发lease,只有持有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-backup架构的系统中,有三个节点A、B、C互为副本,其中有一个节点为primary,且同一时刻只能有一个primary节点、。另有一个节点Q(哨兵)负责判断节点A、B、C的状态,一旦Q发现primary异常,节点Q将选择另一个节点作为primary、假设开始节点A为primary,B、C为backup。节点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,则认为该节点可正常可以提供服务。节点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都是基于这样的设计。 小结:引入第三方的/使用(自身的)中心节点,可以认为是哨兵。系统中的节点像中心节点发心跳,中心节点像系统中的节点发lease。primary节点不可用时,中心节点通知大家放弃lease并删除cache。大家把删除的结果回放中心节点,假如删除失败则等待lease超时的最大时间重新选择primary。把故障机器标记为不可用状态,待故障机器数据同步完全后重新加入,标记为可用。

lease的有效期时间选择

lease的有效期虽然是一个确定的时间点,当颁发者在发布lease时通常都是将当前时间加上一个固定的时长从而计算出lease的有效期。如何选择lease的时长在工程实践中是一个值得讨论的问题。如果lease的时长太短,例如1s,一旦出现网络抖动lease很容易丢失,从而造成节点失去lease,使得依赖lease的服务停止;如果lease的时长太大,例如一分钟,则一旦接收者异常,颁发者需要过长的时间收回lease承诺。过短会因为网络瞬断时节点收不到lease从而引起服务不稳定,lease过长会因为节点宕机异常需要等待很久。工程中,常选择lease时长是10秒级别,这是一个经过验证的经验值,实践中可以作为参考并综合选择合适的时长。

总结:lease作用的用来判断各个副本的状态。如果状态异常会被标记不可用,从副本集中剔除,待机器恢复后重新加入进行数据同步完全后。被标记为可用状态。一般就是心跳和lease结合使用,实际中可以是借助第三方(zookeeper)来实现lease的功能。也可以是副本集自身去实现该功能。


参考资料:《分布式系统原理介绍》作者:刘杰


-----------------------

公众号:井底倁蛙(ID:upgrade366)

这是一个坚韧的男人,你完全不知道生活对他做了,他依然期待每天清晨的阳光。


长按下图二维码关注,你将感受到一个执着的灵魂,且每篇文章都很走心。