专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

如何进行分布式锁定

1.你用那把锁干什么

锁的目的是确保在可能尝试执行相同工作的几个节点中,只有一个节点实际执行此工作(至少一次只执行一个节点)。这项工作可能是将一些数据写入共享存储系统,执行一些计算,调用一些外部API,或诸如此类。在较高的层次上,您可能希望在分布式应用程序中使用锁的原因有两个:为了提高效率或保证[2]的正确性。要区分这些情况,可以询问如果锁失败会发生什么:

  • 效率:使用锁可以避免不必要地重复做同样的工作(例如一些昂贵的计算)。如果锁失败和两个节点做同样的的作品,结果是一个小成本的增加(你最终要多支付5美分给AWS)或一个小小的不便(如用户最终得到相同的电子邮件通知两次)。
  • 正确性:获取锁可以防止并发进程互相干扰并扰乱系统状态。如果锁失效,两个节点同时处理同一块数据,结果是文件损坏、数据丢失、永久不一致、给病人的药物剂量错误或其他一些严重问题。

两者都是想要锁定的有效情况,但是您需要非常清楚要处理的是哪一种。

我想说的是,如果您只是为了提高效率而使用锁,那么就没有必要花费Redlock的成本和复杂性,运行5个Redis服务器并检查大多数是否获得锁。您最好只使用一个Redis实例,或者在主实例崩溃时异步复制到一个辅助实例。

如果您使用单个Redis实例,则如果Redis节点突然断电或发生其他问题,您当然会丢掉一些锁。 但是,如果您只是将锁用作效率优化,并且崩溃不会经常发生,那没什么大不了的。 Redis大放异彩是这种“没什么大不了”的场景。 至少如果您仅依靠单个Redis实例,那么查看系统的每个人都清楚锁是近似的,并且仅用于非关键目的。

另一方面,具有5个副本和多数投票的Redlock算法乍一看,似乎适用于锁定对于正确性很重要的情况。 在以下各节中,我将争辩说它不适合该目的。 对于本文的其余部分,我们将假定您的锁对于正确性很重要,并且如果两个不同的节点同时认为它们持有相同的锁,则这是一个严重的错误。

2.用锁保护资源

让我们暂时搁置一下Redlock的细节,并讨论一般如何使用分布式锁(与所使用的特定锁定算法无关)。 请务必记住,分布式系统中的锁不像多线程应用程序中的互斥锁。 由于问题在于不同的节点和网络都可能以各种方式独立发生故障,因此这是一个更加复杂的野兽。

例如,假设您有一个应用程序,其中客户端需要更新共享存储中的文件(例如HDFS或S3)。客户端首先获取锁,然后读取文件,进行一些更改,将修改后的文件写回,最后释放锁。锁阻止两个客户端同时执行这个读-修改-写循环,这将导致丢失更新。代码可能是这样的:

// THIS CODE IS BROKEN
function writeData(filename, data) {
    var lock = lockService.acquireLock(filename);
    if (!lock) {
        throw 'Failed to acquire lock';
    }

try {
var file = storage.readFile(filename);
var updated = updateContents(file, data);
storage.writeFile(filename, updated);
} finally {
lock.release();
}


}

不幸的是,即使您具有完善的锁定服务,上面的代码也会被破坏。 下图显示了如何以损坏的数据结束:

111_1.png 在本例中,获取锁的客户端在持有锁的同时会暂停很长一段时间,这是因为垃圾收集器(GC)启动了。锁有一个超时(即它是一个租约),这总是一个好主意(否则崩溃的客户端可能会永远持有一个锁并且永远不会释放它)。但是,如果GC暂停持续的时间长于租约到期时间,并且客户端没有意识到它已经过期,那么它可能会继续进行一些不安全的更改。

这个错误不是理论上的:HBase曾经有这个问题[3,4]。 通常,GC暂停时间很短,但是有时人们已经知道“停止世界”的GC暂停会持续几分钟[5] -足够长的时间足以使租约到期。 即使是像HotSpot JVM的CMS这样的所谓的“并发”垃圾收集器,也无法与应用程序代码完全并行运行-甚至他们需要时不时地停止运行。[6]

您不能通过在写回存储之前在锁定到期时插入检查来解决此问题。 请记住,GC可以在任何时候暂停正在运行的线程,包括对您而言最大的麻烦(在最后一次检查和写入操作之间)。

而且,如果您因编程语言运行时没有长时间的GC暂停而感到自鸣得意,那么还有很多其他原因可能会导致您的进程被暂停。 也许您的进程试图读取一个尚未加载到内存中的地址,所以它遇到了页面错误,并被暂停,直到从磁盘加载页面为止。 也许您的磁盘实际上是EBS,所以读取变量会不经意间转变为通过Amazon拥塞网络发出的同步网络请求。 也许还有许多其他争用CPU的进程,而您在调度程序树中遇到了一个黑色节点。 也许有人不小心将SIGSTOP发送到该进程。 随你。 您的过程将暂停。

如果您仍然不相信我有关进程暂停的信息,请考虑在网络中,在达到存储服务之前,文件写入请求可能会延迟。 诸如以太网和IP之类的分组网络可能会任意延迟数据包,而它们确实做到了[7]:在GitHub的一次著名事件中,数据包在网络中被延迟了大约90秒[8]。 这意味着应用程序进程可以发送写请求,并且当租约已经到期时,它可能在一分钟后到达存储服务器。

即使在管理良好的网络中,这种事情也可能发生。您不能对计时做任何假设,这就是为什么无论您使用什么锁服务,上面的代码从根本上都是不安全的。

3.用栅栏使锁更安全

解决此问题的方法实际上非常简单:您需要在对存储服务的每个写入请求中都包含隔离标记。在这种情况下,防护令牌只是每次客户获取锁时都会增加(例如,由锁服务增加)的数字。 如下图所示:

111_2.png

客户端1获得了租约并获得了33的令牌,但随后进入了长暂停状态,租约到期。 客户端2获得租约,获得令牌34(该数字始终在增加),然后将其写操作(包括令牌34)发送到存储服务。随后,客户端1恢复工作并将其写操作发送到存储服务 ,包括其令牌值33。但是,存储服务器会记住它已经处理了具有较高令牌号的写入(34),因此它拒绝了具有令牌33的请求.

请注意,这要求存储服务器在检查令牌中发挥积极作用,并拒绝令牌已倒退的所有写操作。 但是,一旦知道了窍门,这并不是特别困难。 并且只要锁服务生成严格单调递增的令牌,就可以使锁安全。 例如,如果您将ZooKeeper用作锁定服务,则可以将zxid或znode版本号用作防护令牌,并且状态良好[3]。

但是,这导致我们遇到Redlock的第一个大问题:它没有任何生成围栏令牌的功能。 该算法不会产生任何保证每次客户端获取锁都会增加的数字。 这意味着即使该算法在其他方面是完美的,也将无法安全使用,因为在一个客户端暂停或其数据包被延迟的情况下,您无法防止客户端之间的竞争状态。

对于我来说,还不知道如何更改Redlock算法以开始生成隔离令牌。 它使用的唯一随机值不能提供所需的单调性。 仅在一个Redis节点上保留一个计数器是不够的,因为该节点可能会失败。 在多个节点上保留计数器将意味着它们将不同步。 您可能需要一个共识算法才能生成围栏令牌。 (如果仅增加一个计数器很简单。

4.用时间来解决共识

Redlock无法生成隔离令牌的事实应该已经成为在正确性取决于锁定的情况下不使用它的充分理由。 但是还有一些其他问题值得讨论。

在学术文献中,这种算法最实用的系统模型是带有不可靠故障检测器的异步模型[9]。 用简单的英语来说,这意味着该算法不对时序做任何假设:进程可能会暂停任意时间长度,数据包可能会在网络中被任意延迟,时钟可能会被错误地错误–尽管如此,该算法仍有望正确执行 事情。 鉴于我们上面讨论的内容,这些是非常合理的假设。

算法可以使用时钟的唯一目的是生成超时,以避免在节点发生故障时永远等待。 但是超时不必一定是准确的:仅仅因为一个请求超时,并不意味着另一个节点肯定是宕机的-也可能是网络中存在较大的延迟,或者您的本地时钟 是错的。 当用作故障检测器时,超时仅是某些错误的猜测。 (如果可以的话,分布式算法将完全没有时钟,但是共识变得不可能[10]。获取锁就像比较设置操作,需要达成共识[11]。)

请注意,Redis使用gettimeofday而非单调时钟来确定密钥的到期时间。 gettimeofday的手册页明确指出,它返回的时间受到系统时间的不连续跳跃–也就是说,它可能突然向前跳跃几分钟,甚至跳回时间(例如,如果时钟由NTP步进, 它与NTP服务器的差异太大,或者时钟是由管理员手动调整的)。 因此,如果系统时钟在做怪异的事情,则很容易发生Redis中的密钥过期比预期快得多或慢得多的情况。

对于异步模型中的算法而言,这不是一个大问题:这些算法通常可确保始终保持其安全性,而无需进行任何时序假设[12]。 只有活动属性取决于超时或其他故障检测器。 用简单的英语来说,这意味着,即使系统中的时序无处不在(进程暂停,网络延迟,时钟向前和向后跳动),算法的性能也可能会陷入困境,但是该算法永远不会 错误的决定。

但是,Redlock不是这样的。 它的安全性取决于许多时序假设:它假设所有Redis节点在过期前大约保持正确的时间长度; 与有效期相比,网络延迟小; 而且该过程的暂停时间比有效期短得多。

5.用不好的时机打破Redlock

让我们看一些示例,以证明Redlock对时序假设的依赖。 假设系统有五个Redis节点(A,B,C,D和E)和两个客户端(1和2)。 如果Redis节点之一上的时钟向前跳怎么办?

  • 客户端1获取节点A,B,C的锁定。由于网络问题,无法访问D和E
  • 节点C上的时钟向前跳,导致锁过期。
  • 客户端2获得对节点C,D,E的锁定。由于网络问题,无法访问A和B。
  • 现在,客户1和2都认为他们持有该锁。

如果C在将锁保留到磁盘之前崩溃并立即重新启动,则可能会发生类似的问题。 因此,Redlock文档建议将崩溃的节点的重新启动至少延迟最长寿命的锁的生存时间。 但是,此重新启动延迟再次依赖于合理准确的时间度量,并且如果时钟跳变将失败。

好吧,也许你认为时钟跳变是不现实的,因为你对正确地配置NTP只会转轮时钟非常有信心。在这种情况下,让我们看一个进程暂停如何导致算法失败的例子.

  • 客户端1请求锁定节点A,B,C,D,E。
  • 在发送对客户端1的响应时,客户端1进入了停止状态GC。
  • 锁在所有Redis节点上失效。
  • 客户端2获取对节点A,B,C,D,E的锁定。
  • 客户端1完成GC,并接收来自Redis节点的响应,表明它已成功获取了锁(进程暂停后,它们已保存在客户端1的内核网络缓冲区中)。
  • 现在,客户1和2都认为他们持有该锁。

请注意,即使Redis是用C编写的,因此没有GC,但这对我们没有帮助:任何客户端可能会遇到GC暂停的系统都存在此问题。 您只能通过阻止客户端1在客户端2获得锁之后执行该锁下的任何操作来确保此安全,例如使用上述防护方法。

较长的网络延迟会产生与过程暂停相同的效果。 这可能取决于您的TCP用户超时时间-如果您将超时时间大大短于Redis TTL,则可能会忽略延迟的网络数据包,但是为了确保这一点,我们必须详细研究TCP。 此外,随着超时,我们又恢复了时间测量的准确性!

6.Redlock的同步假设

这些示例表明,只有在假设使用同步系统模型时,即具有以下属性的系统,Redlock才能正常工作:

  • 有界的网络延迟(您可以保证数据包始终在一定的最大延迟内到达),
  • 有界的进程暂停(换句话说,硬实时约束,通常只出现在汽车安全气囊系统之类的系统中)
  • 有界时钟错误(祈祷你没有从一个坏的NTP服务器获得你的时间)。

请注意,同步模型并不意味着时钟完全同步:这意味着您假设网络延迟,暂停和时钟漂移的已知上限是固定的[12]。 Redlock假设相对于锁的生存时间而言,延迟,暂停和漂移都较小; 如果计时问题变得与生存时间一样大,则该算法将失败。

在行为良好的数据中心环境中,大多数时间都将满足时序假设–这被称为部分同步系统[12]。 但这足够好吗? 一旦违反这些时序假设,Redlock可能会违反其安全属性,例如 在另一个客户过期之前将其授予租约。 如果您依靠锁来确保正确性,那么“大多数时候”是不够的–您需要始终保持正确性。

有大量证据表明,在大多数实际的系统环境中假设同步系统模型是不安全的[7,8]。不断提醒自己关于GitHub事件的90秒包延迟。Redlock不太可能通过Jepsen测试。

另一方面,为部分同步系统模型(或带有故障检测器的异步模型)设计的一致算法实际上有工作的机会。Raft、Viewstamped复制、Zab和Paxos都属于这一类。这种算法必须放弃所有的时间假设。这很难:人们很容易想当然地认为网络、进程和时钟比实际情况更可靠。但是在分布式系统的混乱现实中,你必须非常小心你的假设。

7.小结

我认为Redlock算法是一个错误的选择,因为它“既不是鱼也不是禽”:对于效率优化锁来说,它不必要地繁重且昂贵,但是对于正确性取决于锁的情况,它不够安全。

特别是,该算法对时间和系统时钟做了危险的假设(本质上假设一个同步系统具有有限的网络延迟和有限的操作执行时间),如果这些假设不满足,它就违反了安全特性。此外,它缺乏生成隔离令牌(保护系统免受网络或暂停进程中的长时间延迟)的功能。

如果您仅在尽力而为的基础上需要锁(作为效率优化,而不是为了正确性),我建议您为Redis坚持使用简单的单节点锁算法(不存在条件集的情况下获取锁, 原子的delete-if-value-matches来释放锁),并在代码中非常清楚地证明锁只是近似的,有时可能会失败。 不必理会由五个Redis节点组成的集群。

另一方面,如果为了正确性需要使用锁,请不要使用Redlock。相反,请使用适当的协商一致的系统,比如ZooKeeper,可能是通过实现锁的某个管理员菜谱实现的。(至少,使用具有合理事务保证的数据库。)并且,请强制对锁下的所有资源访问使用隔离令牌。

正如我开始所说的,如果正确使用,Redis是一个非常好的工具。以上这些都不会降低Redis的预期用途。萨尔瓦托多年来一直致力于这个项目,它的成功当之无愧。但是每个工具都有局限性,了解这些局限性并相应地进行计划是很重要的。

8.来源

本文摘自how-to-do-distributed-locking

本文使用 tech.souyunku.com 排版

文章永久链接:https://tech.souyunku.com/41024

未经允许不得转载:搜云库技术团队 » 如何进行分布式锁定

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们