分布式锁的问题

在review导入表数据和结构处的重构时候,看到下面的逻辑,偶然发现了一个问题。

  1. 表A加写锁,加锁失败就等待。
  2. 写入数据,涉及大量的数据导入
  3. 释放锁和异常处理 一个正常的锁,但是当时看到一个很老的注释,大概意思是尝试捕获OOM的异常,将表删除和锁释放掉。 于是发现这里如果因为GC等待时间过程,被Server认为锁失效了那岂不是锁非常不安全了?而这是带有超时时间锁的通病。查找了一下资料发现这个问题很早就讨论过。

首先简单解释一下通用的分布式锁模型。

  • Client A 向锁服务提供方(Server X)申请某一个资源锁。
  • Server X 确认改资源锁是可获取的,那么就会发送消息给 Client A 通知成功获得锁。
  • Server X 确认改资源锁已经被占用,那么就会发送消息给 Client A 通知或者等待此资源锁(这里会有不同实现)
  • 为了确保锁的liveness,Client A 拿到的资源锁是有时间。也就是只能在一段时间内拥有资源锁
  • 如果需要长期持有锁,Client A需要不断向Server X发送心跳续约。

这个模型中有个天生的缺陷。Client A 与Server X之间通过心跳与租约来确认锁的状态。如果Client A没有主动释放锁的情况下,锁超过租约的期限,Server X会主动认为该资源锁已经释放并分配給其它申请者。这里资源锁的 safety 的条件成立的前提是Client A不再存在。而实际上这个前提在分布式中实在太强太难满足了。网络延迟和 GC 很容易就导致心跳超时,从而导致锁续约失败,而此时Client A还存在没有崩溃。因此会出现下列的场景。

  1. Client A 和 Client B 同时向(Server X)申请资源锁 W
  2. Client A获得了锁 W,并且持续发送心跳给锁W 续约,而 Client B一直在等待资源锁 W
  3. Client A经历了一次长时间的 Full GC,导致长时间没有发送心跳给 Server X。Server X认为锁 W 已经超期然后让 Client B获得了。
  4. Client A的 Full GC 结束并恢复正常,此时Client A自身仍然持有着锁 W。这里发送了冲突违背了锁的 safety 的要求。

对于这个问题Martin给的处理方式我认为也是有问题的,我打算再单独整理分析一下分布式锁。

神仙打架

关于这个问题有两篇对应文章, How to do distributed locking  和  Is Redlock safeantirez  是 redis 的作者,但是对于分布式锁的处理竟然有这么大的漏洞,也是蛮让人吃惊的。 下面的逻辑上 antirez 针对上述分布式锁问题的处理办法,这里主要针对网络 If you read the Redlock specification, that I hadn’t touched for months, you can see the steps to acquire the lock are:

1. Get the current time.

2. … All the steps needed to acquire the lock …

3. Get the current time, again.

4. Check *if* we are already out of time, or *if* we acquired the lock fast enough.

5. Do some work with your lock.

并且认为第3步会进行一次时间比较,如果 Client A发现自己已经超时,那么就主动放弃拥有锁。这样就确保了分布式锁定 safety 要求了。 这个算法有以下几个破绽

  1. 以来系统时间,没法排除 Client A和 Server X 关于超时时间都判定。可能Client A时钟走的慢,而 Server X走到快。
  2. Client A发生时钟跳跃的情况如何处理
  3. 级别觉得1 和 2问题太少见了,那么如何处理4和5之间关于 GC 时间导致超时。

而且在Martin文中也强调了 GC 会发生在任何时候,是无法预估的,不知道antirez怎么就想出一个这个方法的。在 Hacker News 上真是被喷惨了。

总结

深切体会到,分布式把很多不是问题的变成了问题,把小问题放大成大麻烦。最让我担心的是,这样的代码我们并没有认识到是有问题的,已经存在了许久了。不知道有多少Bug因为这个而引起的,还有多少有问题的写法。

最近的文章

分布式锁分析

分布式锁的目的是为了确保几个节点在处理一些任务的时候,同一个时间只有一个(或者最多只有一个)执行某任务。这些任务可能向共享存储中写入数据,或者执行某些计算。从宏观视角来看,使用锁无法两个目的:效率和准确。为了区别这两个目的,我们可以看一下没有锁会发生什么 效率:采用锁避免执行多次同一个任务,特别某些昂贵的操作。如果没有锁的情况下,那么结果会明显增加执行任务的代价。 准确:通过锁避免同时更改数据,从而扰乱系统状态。这里如果没有锁,那么可能结果是文件损坏,数据丢失等严重的不一致状态。两个目...…

继续阅读
更早的文章

Spark 的 Capacity 调度策略实现

Standalone 调度在 standalone模式下面,也就是 spark 自身能够支持两周调度策略 FIFO 这个是默认策略,Job 谁先提交谁的优先级高 FAIR 这个策略可以划分资源 Pool,不同的 Pool 有不同的优先级,可以指定 Job 提交到相应的 Pool 中。目前依靠 Spark 自身提供的调度策略,无法解决长任务对于短任务的阻塞问题。我们知道 CPU 在调度的时候,都会优先运行短的任务,因为这样等待的总时间是少的。同样的道理,我们希望在 Spark 上面也是如...…

继续阅读