Skip to content

Latest commit

 

History

History
167 lines (97 loc) · 21 KB

distribute-lock-is-redlock-safe.md

File metadata and controls

167 lines (97 loc) · 21 KB

Redlock能保证锁的正确性吗?

       本文是Salvatore Sanfilippo反驳Martin Kleppmann关于论证Redlock难以确保分布式锁正确性的文章,文章原文在这里

       Martin Kleppmann关于论证Redlock难以确保分布式锁正确性的文章可以看我翻译的文档

       作者的整篇回复有些乱,没有或者回避了Martin观点的核心,或者说Martin也没有直白的说出来。如果读了Martin的文章,实际上可以看出,任意对时间的假设,都会导致锁超时(释放)的一刻,两个客户端同时有进行操作的可能,这点在理论模型上是能够说的通的,毕竟释放锁的不是锁的持有者,而是锁自己。作者说到所有的分布式锁都有这个问题,我会在之后说明,这个之后通篇没有看到。

       如果是基于Zookeeper的分布式锁,可以通过心跳的方式,让出问题的客户端由于心跳失败而导致锁过期,这种释放锁的判定方式会显得更加的自然,虽然复杂度有了提升,但是这和由超时直接释放锁有很大不同。个人认为,作者的回复是苍白无力的,Martin也没有继续纠缠,总之,正确与否交给用户自己决定吧。

       以下是笔者对这篇文章的(摘录)翻译和理解。如果是笔者的理解将会以self-think来开头,以引言的形式来展示,比如:

self-think 这个是作者的理解。。。

       描述内容示例。。。


       Martin Kleppmann,一个分布式系统研究者,昨天发布了一片关于Redlock的分析文章,你可以在这里找到它。

       Redlock是我设计基于Redis的一种客户端分布式锁算法,它使用一组具备了特定能力的数据存储(也就是Redis),用于构建一个具备多主容错,正确性有保障和自动释放(也就是高可靠)的分布式锁服务。你也可以通过MySQL来实现Redlock算法。

       该算法的目标是为了给使用单节点Redis或者主从复制Redis实现分布式锁的人以更稳定可靠和确保正确性的方案,当然它会带来一些少许的复杂性,但是性能是很好的。

       自从我发布了Redlock算法,许多人将其实现,这里不乏多种语言,当然它也被用在很多地方。

       MartinRedlock的分析表明,这个算法难以确保正确性。非常感谢Martin发表了它的文章,以及整个分析过程,Redlock原始的说明在这里:http://redis.io/topics/distlock。但是我对Martin分析的结果并不认同,好消息是分布式系统不向其他的编程领域,它可以通过数学的方式加以表达和论证,因此一组算法特性能够在假设成立的场景下确保其可靠性。在这篇文章中,我将会分析Martin的文章,最终我们能够明白,Redlock算法究竟是否能够确保正确性。

Martin认为Redlock无法确保正确性的原因

       在Martin的分析文档中,主要有两个观点:

  1. 具有自动释放功能的分布式锁(相互排斥的锁属性仅在获取锁后的固定时间内有效)需要一种方式(或策略)来避免客户端在过期后使用锁时出现问题,这违反了访问共享资源时的互斥性,也就是锁的正确性无法保障。Martin认为Redlock没有这样的机制。
  2. Martin认为不管问题1是否存在,只要它(redlock)在可能产生分区以及不可靠的分布式环境中运行就无法确保其正确性。

self-think 其实只有一个观点,就是Redlock基于时间的假设是很容易在分布式环境中被颠覆的,而一旦假设被推翻,则正确性也就不复存在了。作者的这两个观点,第一个避重就轻,只是说出了问题如何能够让他work round,第二个有些模棱两可。

       我将分别对这两个观点进行陈述,从第一个开始。

分布式锁,自动释放以及令牌

       一个分布式锁如果没有自动过期机制,那么锁的持有者就有几率永久的持有锁,这回导致它是无用的。如果一个持有锁的客户端挂掉,并在一段时间内没有恢复,那么死锁就会产生,而由分布式锁保护的资源将无法被外界访问。一旦出现这种活性问题,这将是无法接受的,没有一个正常的分布式锁会不具备自动过期机制的。

       具有实践性的锁,会拥有一个TTL,也就是过期时间。在过期时间到达后,锁的主要属性,也就是排他性会消失,另一个客户端将会获取到锁。如果两个客户端在不同的时间获取锁,但是第一个客户端由于GC或者其他调度问题导致暂停,而第二个客户端此时(由于锁超时)获取到了锁,它们在这一刻都能够访问到共享资源,这会导致发生什么呢?

       Martin认为如果分布式锁服务提供令牌机制就可以解决这个问题,也就是在获取锁时,能够得到一个(全局上)单调递增的令牌。Martin对于令牌的使用方式如下:两个客户端由于锁问题,同时访问了共享资源,但是可以将令牌设置在写事务(乐观锁)中,而只有更大的令牌的更新才能够获得通过。

       Martin的描述:修复这个问题其实很简单:你只需要在写存储时利用一个令牌即可。在这个场景下,一个单调递增的令牌即可,由锁服务提供,每当获取到锁时,会将令牌同时返回给客户端。

       注意这回要求存储服务充当了检查令牌的角色,并且拒绝所有令牌回跳的写请求。

       我认为Martin的这个观点存在以下若干问题:

  1. 虽然分布式锁可能已经过期,但是大多数情况下你还是需要它来保证排他性语义。分布式锁是在对共享资源没有很好控制的情况下一种非常有效的装置。在Martin的分析中,他假设就算锁出现过期释放,也需要有其他方式来避免竞争。我认为这是一种非常奇怪的推理方式,为什么要解决数据竞争就一定要用分布式锁,就一定要分布式锁来解决数据竞争问题。接下来我会介绍Redlock在这种人为假设场景下也能工作良好的原因;
  2. 如果你的数据存储支持令牌,且对令牌的回跳拒绝写入,那么你的数据存储就是一种线性存储。如果基于这种存储,你只需要生成自增ID即可,而Redlock就会确保这个自增ID服务能够在获得锁时返回一个自增ID。但是接下来,我会展示这是没有必要的;
  3. 可是第二点并不是一个合理的选择,在面向共享存储(比如:关系数据库等)时,它们大多数不是线性的存储,那该怎么办呢?每个Redlock实例都会生成随机的令牌,它们彼此不会冲突,如果有一个在一时刻唯一的令牌,该怎么办呢?可是使用检查并设置的方式来解决。当开始操作共享存储时,可以将令牌作为一个状态进行存储,同时操作流程遵循读取-修改-写入的方式,当然写入时就以令牌作为比对的条件;
  4. 在某些场景下,可以说,顺序的令牌是非常有用的。但是需要注意的是,由于Martin提到的GC暂停,虽然令牌被获取到了,可是由于暂停原因,导致后续操作共享资源时并不一定会遵从获取令牌(或者说锁)的顺序;
  5. 大多数情况下,锁用于访问以非事务性方式更新的资源。例如:有时我们使用分布式锁来移动文件或者与其他外部API交互等等。

self-think Martin的解法主要有两个问题:1)既然都有了令牌,那还要锁做什么;(2)所有的存储或者所有对状态更改的装置都需要支持令牌,很不现实。

对于回复4,这个观点不难理解,很正常,作者的回复有些不知所云。

对于回复5,对于非事务性的访问,或者说锁用于保护非事务性的资源,这点我持反对的观点,至少它是为了保证隔离性和原子性,是一种事务的体现。

       我想再重复一下我的观点,假设一定要有种方式来处理当锁失败的情况,这种观点是多么奇怪。事实上,如果你的系统面临数据竞争,你大可不必使用分布式锁,或者至少你不需要一个有强约束力的分布式锁,而你需要的锁可能会有时失效,但大多数时候能够以高性能的方式提供服务。但是如果你同意Martin的关于令牌的观点,那么使用锁提供了唯一VALUE也是可以做到一样的效果。

接下来看一下系统模型

       上述批评,也就是没有为每个锁提供单调递增的计数器,这种批评在自动释放的分布式锁中基本上是常见的。然而MartinRedlock有一些特别的批评,并且真正分析了算法,然后尝试推翻它。

       Redlock假设一个半同步的系统模型,其中不同的进程可以或多或少以相同的“速率”计算时间。不同的进程不需要由于绝对时间的边界偏差而做任何事情。它们只需要能够在计数5秒的情况下,可以允许10%的偏差,例如:一个进程计数了4.5秒,而另一个是5.5秒,这就够了。

       Martin还指出,Redlock要求网络消息的最大延迟,据我所知,这是不正确的(我稍后会解释他的推理有什么问题)。

       所以我们先从不同的进程不能以相同速率计时开始说起。Martin认为在一个系统中出现时钟跳跃有两个原因:

  1. 系统管理员手工调整时钟;
  2. NTPD根据收到的更新通知来更新本地时钟。

       上述两个问题是可以解决的,对于问题1,只需要不做类似操作即可,对于问题2,可以使用不会进行时间跳跃的NTPD后台服务即可,它会通过在更大时间范围内进行更改时间。

       但是我认为Martin关于Redis或者Redlock关于时间的实现需要切换到monotonic时间API上来,这个论点是正确的,因为它会或多或少的让上述问题得以解决。这个改造在过去被提到过多次,它需要在Redis的实现中引入一些复杂度,但确实是个好的提议,我会在未来的几周来实现它。切换到monotonic时间API上目的是能够更好的运行在系统上,因为它会带来避免受到时间服务器以及人为操作的影响,而如果单纯的出于解决这个问题,就算使用gettimeofday方法也是可以完成的,因为我们可以通过计算相对时间来达到目的。

self-think 实际上这个问题是针对Redis的。

       请注意,过去曾尝试过实现分布式系统,即使假设存在绑定绝对时间错误(通过使用GPS单元)的情况。Redlock不需要类似的保障,只需要不同进程在计时时,能够将10秒计数为9.5或11.2(示例中最多+/- 2秒)的能力。

       因此Redlock是能够确保正确性的吗?这个答案取决于上面的假设。如果我们使用了monotonic时间API来完善这个场景,如果没有意外的人为操作,那会怎么样呢?一个进程能够在最大容忍错误下进行可靠的计时吗?我想一定可以,而且相比较一个进程能够在磁盘日志上可靠的写入更加确定。

网络延迟

       Martin人为Redlock的问题不仅仅是在依赖于计时的假设,他说道:可是Redis不仅如此,它依赖了太多对于时间的假设:它假设所有的redis节点能够以大致相同的过期时间来保存KEY,假设网络延迟要小于节点之间过期时间的差距,当然进程暂停时间也会小于这个差距

       我们将上面的观点做一下分解:

  1. Redis节点之间存放的过期时间大致相同
  2. 网络延迟要小于节点之间过期时间的差距
  3. 进程暂停时间要小于节点之间过期时间的差距

       Martin所说关于时间跳跃的问题,我假设我们已经通过使用monotonic时间API都已经解决了。

       关于问题1,这不是一个问题,我们假设在节点之间能够以大致相同频率进行计时,除非有其他观点反驳它。

       关于问题2,有些复杂,Martin说:当然,如果你认为时间跳跃不现实,因为你认为你配置好了一个不错的NTP服务,当然这点我同意,他接着说:

  1. 客户端1在A,B,C,D,E节点上获取锁
  2. Redis实例都返回获取成功,但响应在客户端1网口开始排队时,客户端1发生fullGC,出现了暂停
  3. 锁在所有的Redis实例上过期
  4. 客户端2获取A,B,C,D,E节点上的锁
  5. 客户端1的GC完成,并收到延迟后的响应,根据响应判断客户端1获取到了锁
  6. 客户端1和2都认为获取到了锁

       如果你看了Redlock的说明,这篇文档我已经几个月没有更新它了,你能看到对于Redlock的描述:

  1. 获取当前的时间(毫秒);
  2. 接下来针对N个Redis实例使用相同的Key和随机值顺序获取锁。对于IO超时时间可以设置一个足够短的时间,比如:如果过期时间是10秒,则超时时间可以设置在5到50毫秒的范围,目的是能够在Redis实例挂掉的时候能够快速反应,使得能够与下一个Redis尽快完成交互;
  3. 如果客户端的耗时,从第一步到N个实例都调用完成,如果不超过锁的过期时间,并且在获取到锁的Redis实例数量大于等于3个,则认为该客户端获取到了锁;
  4. 如果锁获取到了,那么这个分布式锁的占用时间可以认为是初始的过期时间,比如:10秒,减去获取锁的耗时;
  5. 如果客户端没有获取到锁,比如:获取到锁的实例数量是少数,获取在步骤3中的耗时已经超过了锁的过期时间,客户端将会选择释放所有节点的锁,纵使客户端它没有获取到对应节点的锁,这出于简单性的考虑。

       注意第1步第3步,无论网络中发生怎么样的延迟,只要在这两步之间,延迟一定可以被检测出来,所以延迟如果要发生的话,一定在第3步之后。也就是获取到锁后,在第3步之后出现了锁的过期,也就是Martin最早提出的,当客户端持有一个过期锁而去操作共享资源时的问题。我再次强调,所有的分布式锁实现都会有这个问题,而使用令牌去解决显得更加不切实际。

self-think Martin不应该给出令牌的解决方式,只用说问题即可,导致作者一直追着令牌说,其实这个问题不重要,重要的是锁的语义,锁不是由持有者释放的,而是由超时释放的。

       注意步骤1和3之间发生的事情,你可以添加任意你需要的网络延迟,锁总是可以被很好的处理,如果延迟过长,则锁就会失效,Redlock能够对网络延迟有很好的免疫力。它在设计时就考虑了这些问题,我无法想象它会出现竞争问题。

self-think 这点作者是没注意还是水平低,1和3之后的任意网络操作都有可能延迟,而只要有足够的延迟时长,就会造成这个问题。感觉作者已经上头了。

       然而,Martin的博客文章也得到了多名分布式系统专家的审查,所以我不确定我是在这里遗漏了什么,还是只是Redlock的工作方式同时被许多人忽视了。我很乐意收到一些关于这一点的澄清。问题3和问题2的应对方式类似。

关于网络延迟的题外话

       一个简短的说明。如果使用一个具备自动释放的分布式锁服务,客户端尝试获取锁,服务端判定可以,但是由于客户端进程出现了暂停,导致收到服务端获取锁成功的响应有些延迟,而此时锁已经过期。但是你可以做很多事情来避免进程陷入长时间暂停,但你无法控制网络的延迟,所以在获取锁之前以及之后最好做一下时间记录,通过时间差来判定是否能够安全的进行下一步操作。

self-think 其实这么做意义不大,因为完全可以在你根据时间差判定没有问题后再进行GC暂停,所有的问题在真正调用存储服务前都可看似正常,但是在调用的一刻,出现暂停,那就会有问题。

是否使用Fsync?

       Martin谈到了Redlock使用延迟重启实例的方式。该方式要求能够或多或少地等待指定时间。重复同样的事情是没有用的。

       但是这个方式实际上是可选的。你可以通过配置Redis节点的fsync特性来保证每次处理外部的请求都会进行持久化,如果配置了,那么锁信息就一定在磁盘上存在,这也是其他强约束系统能够提供的相同能力。Redlock非常有趣的是,你可以通过通过延迟启动来选择任何磁盘对于锁服务的参与。这意味着可以使用几个Redis实例每秒处理数十万个锁,这是其他系统无法做到的。

GPS设备与本地计算机时钟

       回到系统模型,使Redlock系统模型实用的一件事是,您可以假设一个进程永远不会被系统时钟分区。请注意,这与其他使用GPS单元的半同步模型不同,因为在这种情况下可能会发生两个不明显的分区:

  1. GPSGPS网络隔开,因此无法获得修复;
  2. 进程和GPS无法交换消息,或者交换的消息出现延迟。

       上述问题可能会造成可用性或正确性问题,取决于系统是如何编排的(只有在出现设计错误时才会发生正确性问题,例如:GPS异步更新系统时间,以便在GPS不工作时,绝对时间错误可能会超过最大时延)。

       Redlock的系统模型没有依赖复杂的设备或者额外的硬件,它仅仅依赖计算机时钟,甚至一个非常便宜的时钟就可以满足,因为温度或者其他原因都可以影响到时钟计时。

结论

       我认为Martin对于使用monotonic时钟API的观点是可以接受的,RedisRedlock应该避免使用系统时钟,这点提了个醒。但是我不认可关于Redlock其他正确性的相关结论。

       如果能够收到其他专家的反馈,或者使用Jepsen(或类似)工具的测试数据,那就太好了。

self-think 作者应该自己测一下,MD还需要别人给数据,有点搞。

       非常感谢我的朋友帮我review这篇文章。