基于 Redis 的分布式锁到底安全吗?

【完整版】javascript

网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你能够拿关键词“Redis 分布式锁”随便到哪一个搜索引擎上去搜索一下就知道了。这些文章的思路大致相近,给出的实现算法也看似合乎逻辑,但当咱们着手去实现它们的时候,却发现若是你越是仔细推敲,疑虑也就愈来愈多。html

实际上,大概在一年之前,关于Redis分布式锁的安全性问题,在分布式系统专家Martin Kleppmann和Redis的做者antirez之间就发生过一场争论。因为对这个问题一直以来比较关注,因此我前些日子仔细阅读了与这场争论相关的资料。这场争论的大概过程是这样的:为了规范各家对基于Redis的分布式锁的实现,Redis的做者提出了一个更安全的实现,叫作Redlock。有一天,Martin Kleppmann写了一篇blog,分析了Redlock在安全性上存在的一些问题。而后Redis的做者当即写了一篇blog来反驳Martin的分析。但Martin表示仍然坚持原来的观点。随后,这个问题在Twitter和Hacker News上引起了激烈的讨论,不少分布式系统的专家都参与其中。java

对于那些对分布式系统感兴趣的人来讲,这个事件很是值得关注。无论你是刚接触分布式系统的新手,仍是有着多年分布式开发经验的老手,读完这些分析和评论以后,大概都会有所收获。要知道,亲手实现过Redis Cluster这样一个复杂系统的antirez,足以算得上分布式领域的一名专家了。但对于由分布式锁引起的一系列问题的分析中,不一样的专家却能得出迥异的结论,从中咱们能够窥见分布式系统相关的问题具备何等的复杂性。实际上,在分布式系统的设计中常常发生的事情是:许多想法初看起来毫无破绽,而一旦详加考量,却发现不是那么完美无缺。node

下面,咱们就从头到尾把这场争论过程当中各方的观点进行一下回顾和分析。在这个过程当中,咱们把影响分布式锁的安全性的那些技术细节展开进行讨论,这将是一件颇有意思的事情。这也是一个比较长的故事。固然,其中也免不了包含一些小“八卦”。web

Redlock算法

就像本文开头所讲的,借助Redis来实现一个分布式锁(Distributed Lock)的作法,已经有不少人尝试过。人们构建这样的分布式锁的目的,是为了对一些共享资源进行互斥访问。redis

可是,这些实现虽然思路大致相近,但实现细节上各不相同,它们能提供的安全性和可用性也不尽相同。因此,Redis的做者antirez给出了一个更好的实现,称为Redlock,算是Redis官方对于实现分布式锁的指导规范。Redlock的算法描述就放在Redis的官网上:算法

在Redlock以前,不少人对于分布式锁的实现都是基于单个Redis节点的。而Redlock是基于多个Redis节点(都是Master)的一种实现。为了能理解Redlock,咱们首先须要把简单的基于单Redis节点的算法描述清楚,由于它是Redlock的基础。数据库

基于单Redis节点的分布式锁

首先,Redis客户端为了获取锁,向Redis节点发送以下命令:apache

SET resource_name my_random_value NX PX 30000复制代码

上面的命令若是执行成功,则客户端成功获取到了锁,接下来就能够访问共享资源了;而若是上面的命令执行失败,则说明获取锁失败。c#

注意,在上面的SET命令中:

  • my_random_value是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在全部客户端的全部获取锁的请求中都是惟一的。
  • NX表示只有当resource_name对应的key值不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能得到锁,而其它客户端在锁被释放以前都没法得到锁。
  • PX 30000表示这个锁有一个30秒的自动过时时间。固然,这里30秒只是一个例子,客户端能够选择合适的过时时间。

最后,当客户端完成了对共享资源的操做以后,执行下面的Redis Lua脚原本释放锁

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end复制代码

这段Lua脚本在执行的时候要把前面的my_random_value做为ARGV[1]的值传进去,把resource_name做为KEYS[1]的值传进去。

至此,基于单Redis节点的分布式锁的算法就描述完了。这里面有好几个问题须要重点分析一下。

首先第一个问题,这个锁必需要设置一个过时时间。不然的话,当一个客户端获取锁成功以后,假如它崩溃了,或者因为发生了网络分割(network partition)致使它再也没法和Redis节点通讯了,那么它就会一直持有这个锁,而其它客户端永远没法得到锁了。antirez在后面的分析中也特别强调了这一点,并且把这个过时时间称为锁的有效时间(lock validity time)。得到锁的客户端必须在这个时间以内完成对共享资源的访问。

第二个问题,第一步获取锁的操做,网上很多文章把它实现成了两个Redis命令:

SETNX resource_name my_random_value
EXPIRE resource_name 30复制代码

虽然这两个命令和前面算法描述中的一个SET命令执行效果相同,但却不是原子的。若是客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE了,致使它一直持有这个锁。

第三个问题,也是antirez指出的,设置一个随机字符串my_random_value是颇有必要的,它保证了一个客户端释放的锁必须是本身持有的那个锁。假如获取锁时SET的不是一个随机字符串,而是一个固定值,那么可能会发生下面的执行序列:

  1. 客户端1获取锁成功。
  2. 客户端1在某个操做上阻塞了很长时间。
  3. 过时时间到了,锁自动释放了。
  4. 客户端2获取到了对应同一个资源的锁。
  5. 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。

以后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。

第四个问题,释放锁的操做必须使用Lua脚原本实现。释放锁其实包含三步操做:'GET'、判断和'DEL',用Lua脚原本实现能保证这三步的原子性。不然,若是把这三步操做放到客户端逻辑中去执行的话,就有可能发生与前面第三个问题相似的执行序列:

  1. 客户端1获取锁成功。
  2. 客户端1访问共享资源。
  3. 客户端1为了释放锁,先执行'GET'操做获取随机字符串的值。
  4. 客户端1判断随机字符串的值,与预期的值相等。
  5. 客户端1因为某个缘由阻塞住了很长时间。
  6. 过时时间到了,锁自动释放了。
  7. 客户端2获取到了对应同一个资源的锁。
  8. 客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。

实际上,在上述第三个问题和第四个问题的分析中,若是不是客户端阻塞住了,而是出现了大的网络延迟,也有可能致使相似的执行序列发生。

前面的四个问题,只要实现分布式锁的时候加以注意,就都可以被正确处理。但除此以外,antirez还指出了一个问题,是由failover引发的,倒是基于单Redis节点的分布式锁没法解决的。正是这个问题催生了Redlock的出现。

这个问题是这样的。假如Redis节点宕机了,那么全部客户端就都没法得到锁了,服务变得不可用。为了提升可用性,咱们能够给这个Redis节点挂一个Slave,当Master节点不可用的时候,系统自动切到Slave上(failover)。但因为Redis的主从复制(replication)是异步的,这可能致使在failover过程当中丧失锁的安全性。考虑下面的执行序列:

  1. 客户端1从Master获取了锁。
  2. Master宕机了,存储锁的key尚未来得及同步到Slave上。
  3. Slave升级为Master。
  4. 客户端2重新的Master获取到了对应同一个资源的锁。

因而,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。针对这个问题,antirez设计了Redlock算法,咱们接下来会讨论。

其它疑问

前面这个算法中出现的锁的有效时间(lock validity time),设置成多少合适呢?若是设置过短的话,锁就有可能在客户端完成对于共享资源的访问以前过时,从而失去保护;若是设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会致使全部其它客户端都没法获取锁,从而长时间内没法正常工做。看来真是个两难的问题。

并且,在前面对于随机字符串my_random_value的分析中,antirez也在文章中认可的确应该考虑客户端长期阻塞致使锁过时的状况。若是真的发生了这种状况,那么共享资源是否是已经失去了保护呢?antirez从新设计的Redlock是否能解决这些问题呢?

分布式锁Redlock

因为前面介绍的基于单Redis节点的分布式锁在failover的时候会产生解决不了的安全性问题,所以antirez提出了新的分布式锁的算法Redlock,它基于N个彻底独立的Redis节点(一般状况下N能够设置成5)。

运行Redlock算法的客户端依次执行下面各个步骤,来完成获取锁的操做:

  1. 获取当前时间(毫秒数)。
  2. 按顺序依次向N个Redis节点执行获取锁的操做。这个获取操做跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过时时间(好比PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法可以继续运行,这个获取锁的操做还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败之后,应该当即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,好比该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的状况,但也应该包含其它的失败状况)。
  3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。若是客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,而且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;不然,认为最终获取锁失败。
  4. 若是最终获取锁成功了,那么这个锁的有效时间应该从新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
  5. 若是最终获取锁失败了(可能因为获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该当即向全部Redis节点发起释放锁的操做(即前面介绍的Redis Lua脚本)。

固然,上面描述的只是获取锁的过程,而释放锁的过程比较简单:客户端向全部Redis节点发起释放锁的操做,无论这些节点当时在获取锁的时候成功与否。

因为N个Redis节点中的大多数能正常工做就能保证Redlock正常工做,所以理论上它的可用性更高。咱们前面讨论的单Redis节点的分布式锁在failover的时候锁失效的问题,在Redlock中不存在了,但若是有节点发生崩溃重启,仍是会对锁的安全性有影响的。具体的影响程度跟Redis对数据的持久化程度有关。

假设一共有5个Redis节点:A, B, C, D, E。设想发生了以下的事件序列:

  1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
  2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
  3. 节点C重启后,客户端2锁住了C, D, E,获取锁成功。

这样,客户端1和客户端2同时得到了锁(针对同一资源)。

在默认状况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),所以最坏状况下可能丢失1秒的数据。为了尽量不丢数据,Redis容许设置成每次修改数据都进行fsync,但这会下降性能。固然,即便执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。因此,上面分析的因为节点重启引起的锁失效问题,老是有可能出现的。为了应对这一问题,antirez又提出了延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不当即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过时,它在重启后就不会对现有的锁形成影响。

关于Redlock还有一点细节值得拿出来分析一下:在最后释放锁的时候,antirez在算法描述中特别强调,客户端应该向全部Redis节点发起释放锁的操做。也就是说,即便当时向某个节点获取锁没有成功,在释放锁的时候也不该该漏掉这个节点。这是为何呢?设想这样一种状况,客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操做,可是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求因为超时而失败了,但在Redis这边看来,加锁已经成功了。所以,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点一样发起请求。实际上,这种状况在异步通讯模型中是有可能发生的:客户端向服务器通讯是正常的,但反方向倒是有问题的。

其它疑问

前面在讨论单Redis节点的分布式锁的时候,最后咱们提出了一个疑问,若是客户端长期阻塞致使锁过时,那么它接下来访问共享资源就不安全了(没有了锁的保护)。这个问题在Redlock中是否有所改善呢?显然,这样的问题在Redlock中是依然存在的。

另外,在算法第4步成功获取了锁以后,若是因为获取锁的过程消耗了较长时间,从新计算出来的剩余的锁有效时间很短了,那么咱们还来得及去完成共享资源访问吗?若是咱们认为过短,是否是应该当即进行锁的释放操做?那到底多短才算呢?又是一个选择难题。

Martin的分析

Martin Kleppmann在2016-02-08这一天发表了一篇blog,名字叫"How to do distributed locking
",地址以下:

Martin在这篇文章中谈及了分布式系统的不少基础性的问题(特别是分布式计算的异步模型),对分布式系统的从业者来讲很是值得一读。这篇文章大致能够分为两大部分:

  • 前半部分,与Redlock无关。Martin指出,即便咱们拥有一个完美实现的分布式锁(带自动过时功能),在没有共享资源参与进来提供某种fencing机制的前提下,咱们仍然不可能得到足够的安全性。
  • 后半部分,是对Redlock自己的批评。Martin指出,因为Redlock本质上是创建在一个同步模型之上,对系统的记时假设(timing assumption)有很强的要求,所以自己的安全性是不够的。

首先咱们讨论一下前半部分的关键点。Martin给出了下面这样一份时序图:

在上面的时序图中,假设锁服务自己是没有问题的,它老是能保证任一时刻最多只有一个客户端得到锁。上图中出现的lease这个词能够暂且认为就等同于一个带有自动过时功能的锁。客户端1在得到锁以后发生了很长时间的GC pause,在此期间,它得到的锁过时了,而客户端2得到了锁。当客户端1从GC pause中恢复过来的时候,它不知道本身持有的锁已通过期了,它依然向共享资源(上图中是一个存储服务)发起了写数据请求,而这时锁实际上被客户端2持有,所以两个客户端的写请求就有可能冲突(锁的互斥做用失效了)。

初看上去,有人可能会说,既然客户端1从GC pause中恢复过来之后不知道本身持有的锁已通过期了,那么它能够在访问共享资源以前先判断一下锁是否过时。但仔细想一想,这丝毫也没有帮助。由于GC pause可能发生在任意时刻,也许刚好在判断完以后。

也有人会说,若是客户端使用没有GC的语言来实现,是否是就没有这个问题呢?Martin指出,系统环境太复杂,仍然有不少缘由致使进程的pause,好比虚存形成的缺页故障(page fault),再好比CPU资源的竞争。即便不考虑进程pause的状况,网络延迟也仍然会形成相似的结果。

总结起来就是说,即便锁服务自己是没有问题的,而仅仅是客户端有长时间的pause或网络延迟,仍然会形成两个客户端同时访问共享资源的冲突状况发生。而这种状况其实就是咱们在前面已经提出来的“客户端长期阻塞致使锁过时”的那个疑问。

那怎么解决这个问题呢?Martin给出了一种方法,称为fencing token。fencing token是一个单调递增的数字,当客户端成功获取锁的时候它随同锁一块儿返回给客户端。而客户端访问共享资源的时候带着这个fencing token,这样提供共享资源的服务就能根据它进行检查,拒绝掉延迟到来的访问请求(避免了冲突)。以下图:

在上图中,客户端1先获取到的锁,所以有一个较小的fencing token,等于33,而客户端2后获取到的锁,有一个较大的fencing token,等于34。客户端1从GC pause中恢复过来以后,依然是向存储服务发送访问请求,可是带了fencing token = 33。存储服务发现它以前已经处理过34的请求,因此会拒绝掉此次33的请求。这样就避免了冲突。

如今咱们再讨论一下Martin的文章的后半部分。

Martin在文中构造了一些事件序列,可以让Redlock失效(两个客户端同时持有锁)。为了说明Redlock对系统记时(timing)的过度依赖,他首先给出了下面的一个例子(仍是假设有5个Redis节点A, B, C, D, E):

  1. 客户端1从Redis节点A, B, C成功获取了锁(多数节点)。因为网络问题,与D和E通讯失败。
  2. 节点C上的时钟发生了向前跳跃,致使它上面维护的锁快速过时。
  3. 客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)。
  4. 客户端1和客户端2如今都认为本身持有了锁。

上面这种状况之因此有可能发生,本质上是由于Redlock的安全性(safety property)对系统的时钟有比较强的依赖,一旦系统的时钟变得不许确,算法的安全性也就保证不了了。Martin在这里实际上是要指出分布式算法研究中的一些基础性问题,或者说一些常识问题,即好的分布式算法应该基于异步模型(asynchronous model),算法的安全性不该该依赖于任何记时假设(timing assumption)。在异步模型中:进程可能pause任意长的时间,消息可能在网络中延迟任意长的时间,甚至丢失,系统时钟也可能以任意方式出错。一个好的分布式算法,这些因素不该该影响它的安全性(safety property),只可能影响到它的活性(liveness property),也就是说,即便在很是极端的状况下(好比系统时钟严重错误),算法顶可能是不能在有限的时间内给出结果而已,而不该该给出错误的结果。这样的算法在现实中是存在的,像比较著名的Paxos,或Raft。但显然按这个标准的话,Redlock的安全性级别是达不到的。

随后,Martin以为前面这个时钟跳跃的例子还不够,又给出了一个由客户端GC pause引起Redlock失效的例子。以下:

  1. 客户端1向Redis节点A, B, C, D, E发起锁请求。
  2. 各个Redis节点已经把请求结果返回给了客户端1,但客户端1在收到请求结果以前进入了长时间的GC pause。
  3. 在全部的Redis节点上,锁过时了。
  4. 客户端2在A, B, C, D, E上获取到了锁。
  5. 客户端1从GC pause从恢复,收到了前面第2步来自各个Redis节点的请求结果。客户端1认为本身成功获取到了锁。
  6. 客户端1和客户端2如今都认为本身持有了锁。

Martin给出的这个例子其实有点小问题。在Redlock算法中,客户端在完成向各个Redis节点的获取锁的请求以后,会计算这个过程消耗的时间,而后检查是否是超过了锁的有效时间(lock validity time)。也就是上面的例子中第5步,客户端1从GC pause中恢复过来之后,它会经过这个检查发现锁已通过期了,不会再认为本身成功获取到锁了。随后antirez在他的反驳文章中就指出来了这个问题,但Martin认为这个细节对Redlock总体的安全性没有本质的影响。

抛开这个细节,咱们能够分析一下Martin举这个例子的意图在哪。初看起来,这个例子跟文章前半部分分析通用的分布式锁时给出的GC pause的时序图是基本同样的,只不过那里的GC pause发生在客户端1得到了锁以后,而这里的GC pause发生在客户端1得到锁以前。但两个例子的侧重点不太同样。Martin构造这里的这个例子,是为了强调在一个分布式的异步环境下,长时间的GC pause或消息延迟(上面这个例子中,把GC pause换成Redis节点和客户端1之间的消息延迟,逻辑不变),会让客户端得到一个已通过期的锁。从客户端1的角度看,Redlock的安全性被打破了,由于客户端1收到锁的时候,这个锁已经失效了,而Redlock同时还把这个锁分配给了客户端2。换句话说,Redis服务器在把锁分发给客户端的途中,锁就过时了,但又没有有效的机制让客户端明确知道这个问题。而在以前的那个例子中,客户端1收到锁的时候锁仍是有效的,锁服务自己的安全性能够认为没有被打破,后面虽然也出了问题,但问题是出在客户端1和共享资源服务器之间的交互上。

在Martin的这篇文章中,还有一个颇有见地的观点,就是对锁的用途的区分。他把锁的用途分为两种:

  • 为了效率(efficiency),协调各个客户端避免作重复的工做。即便锁偶尔失效了,只是可能把某些操做多作一遍而已,不会产生其它的不良后果。好比重复发送了一封一样的email。
  • 为了正确性(correctness)。在任何状况下都不容许锁失效的状况发生,由于一旦发生,就可能意味着数据不一致(inconsistency),数据丢失,文件损坏,或者其它严重的问题。

最后,Martin得出了以下的结论:

  • 若是是为了效率(efficiency)而使用分布式锁,容许锁的偶尔失效,那么使用单Redis节点的锁方案就足够了,简单并且效率高。Redlock则是个太重的实现(heavyweight)。
  • 若是是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用Redlock。它不是创建在异步模型上的一个足够强的算法,它对于系统模型的假设中包含不少危险的成分(对于timing)。并且,它没有一个机制可以提供fencing token。那应该使用什么技术呢?Martin认为,应该考虑相似Zookeeper的方案,或者支持事务的数据库。

Martin对Redlock算法的形容是:

neither fish nor fowl (非驴非马)

其它疑问

  • Martin提出的fencing token的方案,须要对提供共享资源的服务进行修改,这在现实中可行吗?
  • 根据Martin的说法,看起来,若是资源服务器实现了fencing token,它在分布式锁失效的状况下也仍然能保持资源的互斥访问。这是否是意味着分布式锁根本没有存在的意义了?
  • 资源服务器须要检查fencing token的大小,若是提供资源访问的服务也是包含多个节点的(分布式的),那么这里怎么检查才能保证fencing token在多个节点上是递增的呢?
  • Martin对于fencing token的举例中,两个fencing token到达资源服务器的顺序颠倒了(小的fencing token后到了),这时资源服务器检查出了这一问题。若是客户端1和客户端2都发生了GC pause,两个fencing token都延迟了,它们几乎同时到达了资源服务器,但保持了顺序,那么资源服务器是否是就检查不出问题了?这时对于资源的访问是否是就发生冲突了?
  • 分布式锁+fencing的方案是绝对正确的吗?能证实吗?

(以上是上部)


自从我写完这个话题的上半部分以后,就感受头脑中出现了许多细小的声音,久久挥之不去。它们就像是在为了一些鸡毛蒜皮的小事而相互争吵个不停。的确,有关分布式的话题就是这样,琐碎异常,并且每一个人说的话听起来彷佛都有道理。

今天,咱们就继续探讨这个话题的后半部分。本文中,咱们将从antirez反驳Martin Kleppmann的观点开始讲起,而后会涉及到Hacker News上出现的一些讨论内容,接下来咱们还会讨论到基于Zookeeper和Chubby的分布式锁是怎样的,并和Redlock进行一些对比。最后,咱们会提到Martin对于这一事件的总结。

antirez的反驳

Martin在发表了那篇分析分布式锁的blog (How to do distributed locking)以后,该文章在Twitter和Hacker News上引起了普遍的讨论。但人们更想听到的是Redlock的做者antirez对此会发表什么样的见解。

Martin的那篇文章是在2016-02-08这一天发表的,但据Martin说,他在公开发表文章的一星期以前就把草稿发给了antirez进行review,并且他们之间经过email进行了讨论。不知道Martin有没有意料到,antirez对于此事的反应很快,就在Martin的文章发表出来的次日,antirez就在他的博客上贴出了他对于此事的反驳文章,名字叫"Is Redlock safe?",地址以下:

这是高手之间的过招。antirez这篇文章也条例很是清晰,而且中间涉及到大量的细节。antirez认为,Martin的文章对于Redlock的批评能够归纳为两个方面(与Martin文章的先后两部分对应):

  • 带有自动过时功能的分布式锁,必须提供某种fencing机制来保证对共享资源的真正的互斥保护。Redlock提供不了这样一种机制。
  • Redlock构建在一个不够安全的系统模型之上。它对于系统的记时假设(timing assumption)有比较强的要求,而这些要求在现实的系统中是没法保证的。

antirez对这两方面分别进行了反驳。

首先,关于fencing机制。antirez对于Martin的这种论证方式提出了质疑:既然在锁失效的状况下已经存在一种fencing机制能继续保持资源的互斥访问了,那为何还要使用一个分布式锁而且还要求它提供那么强的安全性保证呢?即便退一步讲,Redlock虽然提供不了Martin所讲的递增的fencing token,但利用Redlock产生的随机字符串(my_random_value)能够达到一样的效果。这个随机字符串虽然不是递增的,但倒是惟一的,能够称之为unique token。antirez举了个例子,好比,你能够用它来实现“Check and Set”操做,原话是:

When starting to work with a shared resource, we set its state to “<token>”, then we operate the read-modify-write only if the token is still the same when we write.
(译文:当开始和共享资源交互的时候,咱们将它的状态设置成“<token>”,而后仅在token没改变的状况下咱们才执行“读取-修改-写回”操做。)

第一遍看到这个描述的时候,我我的是感受没太看懂的。“Check and Set”应该就是咱们日常听到过的CAS操做了,但它如何在这个场景下工做,antirez并无展开说(在后面讲到Hacker News上的讨论的时候,咱们还会提到)。

而后,antirez的反驳就集中在第二个方面上:关于算法在记时(timing)方面的模型假设。在咱们前面分析Martin的文章时也提到过,Martin认为Redlock会失效的状况主要有三种:

  • 时钟发生跳跃。
  • 长时间的GC pause。
  • 长时间的网络延迟。

antirez确定意识到了这三种状况对Redlock最致命的实际上是第一点:时钟发生跳跃。这种状况一旦发生,Redlock是无法正常工做的。而对于后两种状况来讲,Redlock在当初设计的时候已经考虑到了,对它们引发的后果有必定的免疫力。因此,antirez接下来集中精力来讲明经过恰当的运维,彻底能够避免时钟发生大的跳动,而Redlock对于时钟的要求在现实系统中是彻底能够知足的。

Martin在提到时钟跳跃的时候,举了两个可能形成时钟跳跃的具体例子:

  • 系统管理员手动修改了时钟。
  • 从NTP服务收到了一个大的时钟更新事件。

antirez反驳说:

  • 手动修改时钟这种人为缘由,不要那么作就是了。不然的话,若是有人手动修改Raft协议的持久化日志,那么就算是Raft协议它也无法正常工做了。
  • 使用一个不会进行“跳跃”式调整系统时钟的ntpd程序(多是经过恰当的配置),对于时钟的修改经过屡次微小的调整来完成。

而Redlock对时钟的要求,并不须要彻底精确,它只须要时钟差很少精确就能够了。好比,要记时5秒,但可能实际记了4.5秒,而后又记了5.5秒,有必定的偏差。不过只要偏差不超过必定范围,这对Redlock不会产生影响。antirez认为呢,像这样对时钟精度并非很高的要求,在实际环境中是彻底合理的。

好了,到此为止,若是你相信antirez这里关于时钟的论断,那么接下来antirez的分析就基本上瓜熟蒂落了。

关于Martin提到的能使Redlock失效的后两种状况,Martin在分析的时候刚好犯了一个错误(在本文上半部分已经提到过)。在Martin给出的那个由客户端GC pause引起Redlock失效的例子中,这个GC pause引起的后果至关于在锁服务器和客户端之间发生了长时间的消息延迟。Redlock对于这个状况是能处理的。回想一下Redlock算法的具体过程,它使用起来的过程大致能够分红5步:

  1. 获取当前时间。
  2. 完成获取锁的整个过程(与N个Redis节点交互)。
  3. 再次获取当前时间。
  4. 把两个时间相减,计算获取锁的过程是否消耗了太长时间,致使锁已通过期了。若是没过时,
  5. 客户端持有锁去访问共享资源。

在Martin举的例子中,GC pause或网络延迟,实际发生在上述第1步和第3步之间。而无论在第1步和第3步之间因为什么缘由(进程停顿或网络延迟等)致使了大的延迟出现,在第4步都能被检查出来,不会让客户端拿到一个它认为有效而实际却已通过期的锁。固然,这个检查依赖系统时钟没有大的跳跃。这也就是为何antirez在前面要对时钟条件进行辩护的缘由。

有人会说,在第3步以后,仍然可能会发生延迟啊。没错,antirez认可这一点,他对此有一段颇有意思的论证,原话以下:

The delay can only happen after steps 3, resulting into the lock to be considered ok while actually expired, that is, we are back at the first problem Martin identified of distributed locks where the client fails to stop working to the shared resource before the lock validity expires. Let me tell again how this problem is common with all the distributed locks implementations, and how the token as a solution is both unrealistic and can be used with Redlock as well.
(译文:延迟只能发生在第3步以后,这致使锁被认为是有效的而实际上已通过期了,也就是说,咱们回到了Martin指出的第一个问题上,客户端没可以在锁的有效性过时以前完成与共享资源的交互。让我再次申明一下,这个问题对于全部的分布式锁的实现是广泛存在的,并且基于token的这种解决方案是不切实际的,但也能和Redlock一块儿用。)

这里antirez所说的“Martin指出的第一个问题”具体是什么呢?在本文上半部分咱们提到过,Martin的文章分为两大部分,其中前半部分与Redlock没有直接关系,而是指出了任何一种带自动过时功能的分布式锁在没有提供fencing机制的前提下都有可能失效。这里antirez所说的就是指的Martin的文章的前半部分。换句话说,对于大延迟给Redlock带来的影响,刚好与Martin在文章的前半部分针对全部的分布式锁所作的分析是一致的,而这种影响不仅仅针对Redlock。Redlock的实现已经保证了它是和其它任何分布式锁的安全性是同样的。固然,与其它“更完美”的分布式锁相比,Redlock彷佛提供不了Martin提出的那种递增的token,但antirez在前面已经分析过了,关于token的这种论证方式自己就是“不切实际”的,或者退一步讲,Redlock能提供的unique token也可以提供彻底同样的效果。

另外,关于大延迟对Redlock的影响,antirez和Martin在Twitter上有下面的对话:

antirez:
@martinkl so I wonder if after my reply, we can at least agree about unbound messages delay to don’t cause any harm.

Martin:
@antirez Agree about message delay between app and lock server. Delay between app and resource being accessed is still problematic.

(译文:
antirez问:我想知道,在我发文回复以后,咱们可否在一点上达成一致,就是大的消息延迟不会给Redlock的运行形成损害。
Martin答:对于客户端和锁服务器之间的消息延迟,我赞成你的观点。但客户端和被访问资源之间的延迟仍是有问题的。)

经过这段对话能够看出,对于Redlock在第4步所作的锁有效性的检查,Martin是予以确定的。但他认为客户端和资源服务器之间的延迟仍是会带来问题的。Martin在这里说的有点模糊。就像antirez前面分析的,客户端和资源服务器之间的延迟,对全部的分布式锁的实现都会带来影响,这不仅仅是Redlock的问题了。

以上就是antirez在blog中所说的主要内容。有一些点值得咱们注意一下:

  • antirez是赞成大的系统时钟跳跃会形成Redlock失效的。在这一点上,他与Martin的观点的不一样在于,他认为在实际系统中是能够避免大的时钟跳跃的。固然,这取决于基础设施和运维方式。
  • antirez在设计Redlock的时候,是充分考虑了网络延迟和程序停顿所带来的影响的。可是,对于客户端和资源服务器之间的延迟(即发生在算法第3步以后的延迟),antirez是认可全部的分布式锁的实现,包括Redlock,是没有什么好办法来应对的。

讨论进行到这,Martin和antirez之间谁对谁错其实并非那么重要了。只要咱们可以对Redlock(或者其它分布式锁)所能提供的安全性的程度有充分的了解,那么咱们就能作出本身的选择了。

Hacker News上的一些讨论

针对Martin和antirez的两篇blog,不少技术人员在Hacker News上展开了激烈的讨论。这些讨论所在地址以下:

在Hacker News上,antirez积极参与了讨论,而Martin则始终置身事外。

下面我把这些讨论中一些有意思的点拿出来与你们一块儿分享一下(集中在对于fencing token机制的讨论上)。

关于antirez提出的“Check and Set”操做,他在blog里并无详加说明。果真,在Hacker News上就有人出来问了。antirez给出的答复以下:

You want to modify locked resource X. You set X.currlock = token. Then you read, do whatever you want, and when you write, you "write-if-currlock == token". If another client did X.currlock = somethingelse, the transaction fails.

翻译一下能够这样理解:假设你要修改资源X,那么遵循下面的伪码所定义的步骤。

  1. 先设置X.currlock = token。
  2. 读出资源X(包括它的值和附带的X.currlock)。
  3. 按照"write-if-currlock == token"的逻辑,修改资源X的值。意思是说,若是对X进行修改的时候,X.currlock仍然和当初设置进去的token相等,那么才进行修改;若是这时X.currlock已是其它值了,那么说明有另一方也在试图进行修改操做,那么放弃当前的修改,从而避免冲突。

随后Hacker News上一位叫viraptor的用户提出了异议,它给出了这样一个执行序列:

  • A: X.currlock = Token_ID_A
  • A: resource read
  • A: is X.currlock still Token_ID_A? yes
  • B: X.currlock = Token_ID_B
  • B: resource read
  • B: is X.currlock still Token_ID_B? yes
  • B: resource write
  • A: resource write

到了最后两步,两个客户端A和B同时进行写操做,冲突了。不过,这位用户应该是理解错了antirez给出的修改过程了。按照antirez的意思,判断X.currlock是否修改过和对资源的写操做,应该是一个原子操做。只有这样理解才能合乎逻辑,不然的话,这个过程就有严重的破绽。这也是为何antirez以前会对fencing机制产生质疑:既然资源服务器自己都能提供互斥的原子操做了,为何还须要一个分布式锁呢?所以,antirez认为这种fencing机制是很累赘的,他之因此仍是提出了这种“Check and Set”操做,只是为了证实在提供fencing token这一点上,Redlock也能作到。可是,这里仍然有一些不明确的地方,若是将"write-if-currlock == token"看作是原子操做的话,这个逻辑势必要在资源服务器上执行,那么第二步为何还要“读出资源X”呢?除非这个“读出资源X”的操做也是在资源服务器上执行,它包含在“判断-写回”这个原子操做里面。而假如不这样理解的话,“读取-判断-写回”这三个操做都放在客户端执行,那么看不出它们如何才能实现原子性操做。在下面的讨论中,咱们暂时忽略“读出资源X”这一步。

这个基于random token的“Check and Set”操做,若是与Martin提出的递增的fencing token对比一下的话,至少有两点不一样:

  • “Check and Set”对于写操做要分红两步来完成(设置token、判断-写回),而递增的fencing token机制只须要一步(带着token向资源服务器发起写请求)。
  • 递增的fencing token机制能保证最终操做共享资源的顺序,那些延迟时间太长的操做就没法操做共享资源了。可是基于random token的“Check and Set”操做不会保证这个顺序,那些延迟时间太长的操做若是后到达了,它仍然有可能操做共享资源(固然是以互斥的方式)。

对于前一点不一样,咱们在后面的分析中会看到,若是资源服务器也是分布式的,那么使用递增的fencing token也要变成两步。

而对于后一点操做顺序上的不一样,antirez认为这个顺序没有意义,关键是能互斥访问就好了。他写下了下面的话:

So the goal is, when race conditions happen, to avoid them in some way.
......
Note also that when it happens that, because of delays, the clients are accessing concurrently, the lock ID has little to do with the order in which the operations were indented to happen.
(译文: 咱们的目标是,当竞争条件出现的时候,可以以某种方式避免。
......
还须要注意的是,当那种竞争条件出现的时候,好比因为延迟,客户端是同时来访问的,锁的ID的大小顺序跟那些操做真正想执行的顺序,是没有什么关系的。)

这里的lock ID,跟Martin说的递增的token是一回事。

随后,antirez举了一个“将名字加入列表”的操做的例子:

  • T0: Client A receives new name to add from web.
  • T0: Client B is idle
  • T1: Client A is experiencing pauses.
  • T1: Client B receives new name to add from web.
  • T2: Client A is experiencing pauses.
  • T2: Client B receives a lock with ID 1
  • T3: Client A receives a lock with ID 2

你看,两个客户端(实际上是Web服务器)执行“添加名字”的操做,A原本是排在B前面的,但得到锁的顺序倒是B排在A前面。所以,antirez说,锁的ID的大小顺序跟那些操做真正想执行的顺序,是没有什么关系的。关键是能排出一个顺序来,能互斥访问就好了。那么,至于锁的ID是递增的,仍是一个random token,天然就不那么重要了。

Martin提出的fencing token机制,给人留下了无尽的疑惑。这主要是由于他对于这一机制的描述缺乏太多的技术细节。从上面的讨论能够看出,antirez对于这一机制的见解是,它跟一个random token没有什么区别,并且,它须要资源服务器自己提供某种互斥机制,这几乎让分布式锁自己的存在失去了意义。围绕fencing token的问题,还有两点是比较引人注目的,Hacker News上也有人提出了相关的疑问:

  • (1)关于资源服务器自己的架构细节。
  • (2)资源服务器对于fencing token进行检查的实现细节,好比是否须要提供一种原子操做。

关于上述问题(1),Hacker News上有一位叫dwenzek的用户发表了下面的评论:

...... the issue around the usage of fencing tokens to reject any late usage of a lock is unclear just because the protected resource and its access are themselves unspecified. Is the resource distributed or not? If distributed, does the resource has a mean to ensure that tokens are increasing over all the nodes? Does the resource have a mean to rollback any effects done by a client which session is interrupted by a timeout?

(译文:...... 关于使用fencing token拒绝掉延迟请求的相关议题,是不够清晰的,由于受保护的资源以及对它的访问方式自己是没有被明肯定义过的。资源服务是否是分布式的呢?若是是,资源服务有没有一种方式能确保token在全部节点上递增呢?对于客户端的Session因为过时而被中断的状况,资源服务有办法将它的影响回滚吗?)

这些疑问在Hacker News上并无人给出解答。而关于分布式的资源服务器架构如何处理fencing token,另一名分布式系统的专家Flavio Junqueira在他的一篇blog中有所说起(咱们后面会再提到)。

关于上述问题(2),Hacker News上有一位叫reza_n的用户发表了下面的疑问:

I understand how a fencing token can prevent out of order writes when 2 clients get the same lock. But what happens when those writes happen to arrive in order and you are doing a value modification? Don't you still need to rely on some kind of value versioning or optimistic locking? Wouldn't this make the use of a distributed lock unnecessary?

(译文: 我理解当两个客户端同时得到锁的时候fencing token是如何防止乱序的。可是若是两个写操做刚好按序到达了,并且它们在对同一个值进行修改,那会发生什么呢?难道不会仍然是依赖某种数据版本号或者乐观锁的机制?这不会让分布式锁变得没有必要了吗?)

一位叫Terr_的Hacker News用户答:

I believe the "first" write fails, because the token being passed in is no longer "the lastest", which indicates their lock was already released or expired.

(译文: 我认为“第一个”写请求会失败,由于它传入的token再也不是“最新的”了,这意味着锁已经释放或者过时了。)

Terr_的回答到底对不对呢?这很差说,取决于资源服务器对于fencing token进行检查的实现细节。让咱们来简单分析一下。

为了简单起见,咱们假设有一台(先不考虑分布式的状况)经过RPC进行远程访问文件服务器,它没法提供对于文件的互斥访问(不然咱们就不须要分布式锁了)。如今咱们按照Martin给出的说法,加入fencing token的检查逻辑。因为Martin没有描述具体细节,咱们猜想至少有两种可能。

第一种可能,咱们修改了文件服务器的代码,让它能多接受一个fencing token的参数,并在进行全部处理以前加入了一个简单的判断逻辑,保证只有当前接收到的fencing token大于以前的值才容许进行后边的访问。而一旦经过了这个判断,后面的处理不变。

如今想象reza_n描述的场景,客户端1和客户端2都发生了GC pause,两个fencing token都延迟了,它们几乎同时到达了文件服务器,并且保持了顺序。那么,咱们新加入的判断逻辑,应该对两个请求都会放过,而放过以后它们几乎同时在操做文件,仍是冲突了。既然Martin宣称fencing token能保证分布式锁的正确性,那么上面这种可能的猜想也许是咱们理解错了。

固然,还有第二种可能,就是咱们对文件服务器确实作了比较大的改动,让这里判断token的逻辑和随后对文件的处理放在一个原子操做里了。这可能更接近antirez的理解。这样的话,前面reza_n描述的场景中,两个写操做都应该成功。

基于ZooKeeper的分布式锁更安全吗?

不少人(也包括Martin在内)都认为,若是你想构建一个更安全的分布式锁,那么应该使用ZooKeeper,而不是Redis。那么,为了对比的目的,让咱们先暂时脱离开本文的题目,讨论一下基于ZooKeeper的分布式锁能提供绝对的安全吗?它须要fencing token机制的保护吗?

咱们不得不提一下分布式专家Flavio Junqueira所写的一篇blog,题目叫“Note on fencing and distributed locks”,地址以下:

Flavio Junqueira是ZooKeeper的做者之一,他的这篇blog就写在Martin和antirez发生争论的那几天。他在文中给出了一个基于ZooKeeper构建分布式锁的描述(固然这不是惟一的方式):

  • 客户端尝试建立一个znode节点,好比/lock。那么第一个客户端就建立成功了,至关于拿到了锁;而其它的客户端会建立失败(znode已存在),获取锁失败。
  • 持有锁的客户端访问共享资源完成后,将znode删掉,这样其它客户端接下来就能来获取锁了。
  • znode应该被建立成ephemeral的。这是znode的一个特性,它保证若是建立znode的那个客户端崩溃了,那么相应的znode会被自动删除。这保证了锁必定会被释放。

看起来这个锁至关完美,没有Redlock过时时间的问题,并且能在须要的时候让锁自动释放。但仔细考察的话,并不尽然。

ZooKeeper是怎么检测出某个客户端已经崩溃了呢?实际上,每一个客户端都与ZooKeeper的某台服务器维护着一个Session,这个Session依赖按期的心跳(heartbeat)来维持。若是ZooKeeper长时间收不到客户端的心跳(这个时间称为Sesion的过时时间),那么它就认为Session过时了,经过这个Session所建立的全部的ephemeral类型的znode节点都会被自动删除。

设想以下的执行序列:

  1. 客户端1建立了znode节点/lock,得到了锁。
  2. 客户端1进入了长时间的GC pause。
  3. 客户端1链接到ZooKeeper的Session过时了。znode节点/lock被自动删除。
  4. 客户端2建立了znode节点/lock,从而得到了锁。
  5. 客户端1从GC pause中恢复过来,它仍然认为本身持有锁。

最后,客户端1和客户端2都认为本身持有了锁,冲突了。这与以前Martin在文章中描述的因为GC pause致使的分布式锁失效的状况相似。

看起来,用ZooKeeper实现的分布式锁也不必定就是安全的。该有的问题它仍是有。可是,ZooKeeper做为一个专门为分布式应用提供方案的框架,它提供了一些很是好的特性,是Redis之类的方案所没有的。像前面提到的ephemeral类型的znode自动删除的功能就是一个例子。

还有一个颇有用的特性是ZooKeeper的watch机制。这个机制能够这样来使用,好比当客户端试图建立/lock的时候,发现它已经存在了,这时候建立失败,但客户端不必定就此对外宣告获取锁失败。客户端能够进入一种等待状态,等待当/lock节点被删除的时候,ZooKeeper经过watch机制通知它,这样它就能够继续完成建立操做(获取锁)。这可让分布式锁在客户端用起来就像一个本地的锁同样:加锁失败就阻塞住,直到获取到锁为止。这样的特性Redlock就没法实现。

小结一下,基于ZooKeeper的锁和基于Redis的锁相比在实现特性上有两个不一样:

  • 在正常状况下,客户端能够持有锁任意长的时间,这能够确保它作完全部须要的资源访问操做以后再释放锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。实际上,基于ZooKeeper的锁是依靠Session(心跳)来维持锁的持有状态的,而Redis不支持Sesion。
  • 基于ZooKeeper的锁支持在获取锁失败以后等待锁从新释放的事件。这让客户端对锁的使用更加灵活。

顺便提一下,如上所述的基于ZooKeeper的分布式锁的实现,并非最优的。它会引起“herd effect”(羊群效应),下降获取锁的性能。一个更好的实现参见下面连接:

咱们从新回到Flavio Junqueira对于fencing token的分析。Flavio Junqueira指出,fencing token机制本质上是要求客户端在每次访问一个共享资源的时候,在执行任何操做以前,先对资源进行某种形式的“标记”(mark)操做,这个“标记”能保证持有旧的锁的客户端请求(若是延迟到达了)没法操做资源。这种标记操做能够是不少形式,fencing token是其中比较典型的一个。

随后Flavio Junqueira提到用递增的epoch number(至关于Martin的fencing token)来保护共享资源。而对于分布式的资源,为了方便讨论,假设分布式资源是一个小型的多备份的数据存储(a small replicated data store),执行写操做的时候须要向全部节点上写数据。最简单的作标记的方式,就是在对资源进行任何操做以前,先把epoch number标记到各个资源节点上去。这样,各个节点就保证了旧的(也就是小的)epoch number没法操做数据。

固然,这里再展开讨论下去可能就涉及到了这个数据存储服务的实现细节了。好比在实际系统中,可能为了容错,只要上面讲的标记和写入操做在多数节点上完成就算成功完成了(Flavio Junqueira并无展开去讲)。在这里咱们能看到的,最重要的,是这种标记操做如何起做用的方式。这有点相似于Paxos协议(Paxos协议要求每一个proposal对应一个递增的数字,执行accept请求以前先执行prepare请求)。antirez提出的random token的方式显然不符合Flavio Junqueira对于“标记”操做的定义,由于它没法区分新的token和旧的token。只有递增的数字才能确保最终收敛到最新的操做结果上。

在这个分布式数据存储服务(共享资源)的例子中,客户端在标记完成以后执行写入操做的时候,存储服务的节点须要判断epoch number是否是最新,而后肯定能不能执行写入操做。若是按照上一节咱们的分析思路,这里的epoch判断和接下来的写入操做,是否是在一个原子操做里呢?根据Flavio Junqueira的相关描述,咱们相信,应该是原子的。那么既然资源自己能够提供原子互斥操做了,那么分布式锁还有存在的意义吗?应该说有。客户端能够利用分布式锁有效地避免冲突,等待写入机会,这对于包含多个节点的分布式资源尤为有用(固然,是出于效率的缘由)。

Chubby的分布式锁是怎样作fencing的?

提到分布式锁,就不能不提Google的Chubby。

Chubby是Google内部使用的分布式锁服务,有点相似于ZooKeeper,但也存在不少差别。Chubby对外公开的资料,主要是一篇论文,叫作“The Chubby lock service for loosely-coupled distributed systems”,下载地址以下:

另外,YouTube上有一个的讲Chubby的talk,也很不错,播放地址:

Chubby天然也考虑到了延迟形成的锁失效的问题。论文里有一段描述以下:

a process holding a lock L may issue a request R, but then fail. Another process may ac- quire L and perform some action before R arrives at its destination. If R later arrives, it may be acted on without the protection of L, and potentially on inconsistent data.

(译文: 一个进程持有锁L,发起了请求R,可是请求失败了。另外一个进程得到了锁L并在请求R到达目的方以前执行了一些动做。若是后来请求R到达了,它就有可能在没有锁L保护的状况下进行操做,带来数据不一致的潜在风险。)

这跟Martin的分析大同小异。

Chubby给出的用于解决(缓解)这一问题的机制称为sequencer,相似于fencing token机制。锁的持有者能够随时请求一个sequencer,这是一个字节串,它由三部分组成:

  • 锁的名字。
  • 锁的获取模式(排他锁仍是共享锁)。
  • lock generation number(一个64bit的单调递增数字)。做用至关于fencing token或epoch number。

客户端拿到sequencer以后,在操做资源的时候把它传给资源服务器。而后,资源服务器负责对sequencer的有效性进行检查。检查能够有两种方式:

  • 调用Chubby提供的API,CheckSequencer(),将整个sequencer传进去进行检查。这个检查是为了保证客户端持有的锁在进行资源访问的时候仍然有效。
  • 将客户端传来的sequencer与资源服务器当前观察到的最新的sequencer进行对比检查。能够理解为与Martin描述的对于fencing token的检查相似。

固然,若是因为兼容的缘由,资源服务自己不容易修改,那么Chubby还提供了一种机制:

  • lock-delay。Chubby容许客户端为持有的锁指定一个lock-delay的时间值(默认是1分钟)。当Chubby发现客户端被动失去联系的时候,并不会当即释放锁,而是会在lock-delay指定的时间内阻止其它客户端得到这个锁。这是为了在把锁分配给新的客户端以前,让以前持有锁的客户端有充分的时间把请求队列排空(draining the queue),尽可能防止出现延迟到达的未处理请求。

可见,为了应对锁失效问题,Chubby提供的三种处理方式:CheckSequencer()检查、与上次最新的sequencer对比、lock-delay,它们对于安全性的保证是从强到弱的。并且,这些处理方式自己都没有保证提供绝对的正确性(correctness)。可是,Chubby确实提供了单调递增的lock generation number,这就容许资源服务器在须要的时候,利用它提供更强的安全性保障。

关于时钟

在Martin与antirez的这场争论中,冲突最为严重的就是对于系统时钟的假设是否是合理的问题。Martin认为系统时钟不免会发生跳跃(这与分布式算法的异步模型相符),而antirez认为在实际中系统时钟能够保证不发生大的跳跃。

Martin对于这一分歧发表了以下见解(原话):

So, fundamentally, this discussion boils down to whether it is reasonable to make timing assumptions for ensuring safety properties. I say no, Salvatore says yes — but that's ok. Engineering discussions rarely have one right answer.

(译文:
从根本上来讲,这场讨论最后归结到了一个问题上:为了确保安全性而作出的记时假设究竟是否合理。我认为不合理,而antirez认为合理 —— 可是这也不要紧。工程问题的讨论不多只有一个正确答案。)

那么,在实际系统中,时钟究竟是否可信呢?对此,Julia Evans专门写了一篇文章,“TIL: clock skew exists”,总结了不少跟时钟偏移有关的实际资料,并进行了分析。这篇文章地址:

Julia Evans在文章最后得出的结论是:

clock skew is real
(时钟偏移在现实中是存在的)

Martin的过后总结

咱们前面提到过,当各方的争论在激烈进行的时候,Martin几乎始终置身事外。可是Martin在这件事过去以后,把这个事件的先后通过总结成了一个很长的故事线。若是你想最全面地了解这个事件发生的先后通过,那么建议去读读Martin的这个总结:

在这个故事总结的最后,Martin写下了不少感性的评论:

For me, this is the most important point: I don't care who is right or wrong in this debate — I care about learning from others' work, so that we can avoid repeating old mistakes, and make things better in future. So much great work has already been done for us: by standing on the shoulders of giants, we can build better software.
......
By all means, test ideas by arguing them and checking whether they stand up to scrutiny by others. That's part of the learning process. But the goal should be to learn, not to convince others that you are right. Sometimes that just means to stop and think for a while.

(译文:
对我来讲最重要的一点在于:我并不在意在这场辩论中谁对谁错 —— 我只关心从其余人的工做中学到的东西,以便咱们可以避免重蹈覆辙,并让将来更加美好。前人已经为咱们创造出了许多伟大的成果:站在巨人的肩膀上,咱们得以构建更棒的软件。
......
对于任何想法,务必要详加检验,经过论证以及检查它们是否经得住别人的详细审查。那是学习过程的一部分。但目标应该是为了得到知识,而不该该是为了说服别人相信你本身是对的。有时候,那只不过意味着停下来,好好地想想。)


关于分布式锁的这场争论,咱们已经完整地作了回顾和分析。

按照锁的两种用途,若是仅是为了效率(efficiency),那么你能够本身选择你喜欢的一种分布式锁的实现。固然,你须要清楚地知道它在安全性上有哪些不足,以及它会带来什么后果。而若是你是为了正确性(correctness),那么请慎之又慎。在本文的讨论中,咱们在分布式锁的正确性上走得最远的地方,要数对于ZooKeeper分布式锁、单调递增的epoch number以及对分布式资源进行标记的分析了。请仔细审查相关的论证。

Martin为咱们留下了很多疑问,尤为是他提出的fencing token机制。他在blog中提到,会在他的新书《Designing Data-Intensive Applications》的第8章和第9章再详加论述。目前,这本书尚在预售当中。我感受,这会是一本值得一读的书,它不一样于为了出名或赚钱而出版的那种短平快的书籍。能够看出做者在这本书上投入了巨大的精力。

最后,我相信,这个讨论还远没有结束。分布式锁(Distributed Locks)和相应的fencing方案,能够做为一个长期的课题,随着咱们对分布式系统的认识逐渐增长,能够再来慢慢地思考它。思考它更深层的本质,以及它在理论上的证实。

(完)

感谢

由衷地感谢几位朋友花了宝贵的时间对本文草稿所作的review:CacheCloud的做者付磊,快手的李伟博,阿里的李波。固然,文中若是还有错漏,由我本人负责^-^。

其它精选文章

相关文章
相关标签/搜索