Quantcast
Channel: CodeSection,代码区,数据库(综合) - CodeSec
Viewing all articles
Browse latest Browse all 6262

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

$
0
0

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

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

还没有看过上半部分的同学,请先阅读:

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

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?",地址如下:

http://antirez.com/news/101

这是高手之间的过招。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步:

获取当前时间。

完成 获取锁 的整个过程(与N个Redis节点交互)。

再次获取当前时间。

把两个时间相减,计算 获取锁 的过程是否消耗了太长时间,导致锁已经过期了。如果没过期,

客户端持有锁去访问共享资源。

在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上展开了激烈的讨论。这些讨论所在地址如下:

针对Martin的blog的讨论: https://news.ycombinator.com/item?id=11059738

针对antirez的blog的讨论: https://news.ycombinator.com/item?id=11065933

在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,那么遵循下面的伪码所定义的步骤。

先设置X.currlock = token。

读出资源X(包括它的值和附带的X.currlock)。

按照"write-if-currlock == token"的逻辑,修改资源X的值。意思是说,如果对X进行修改的时候,X.currlock仍然和当初设置进去的token相等,那么才进行修改;如果这时X.currlock已经是其它值了,那么说明有另外一方也在试图进行修改操作,那么放弃当前的修改,从而避免冲突。

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

A: X.currlock = Token_ID_A

A: resource read

A:

Viewing all articles
Browse latest Browse all 6262

Trending Articles