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

 2017-02-24


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

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

尚未看过上半部分的同窗,请先阅读:web

antirez的反驳

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

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

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

  • 带有自动过时功能的分布式锁,必须提供某种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上的讨论的时候,咱们还会提到)。session

而后,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的做者付磊,快手的李伟博,阿里的李波。固然,文中若是还有错漏,由我本人负责^-^。

其它精选文章:

相关文章
相关标签/搜索