RedLock红锁安全性争论(上)
通过前面的学习可以了解到Redis到底如何实现单实例的分布式锁,也可以基于高可用的特性出发采用Redis集群实现分布式锁,但是集群的故障转移就会带来锁安全性的问题,Redis的作者antirez基于这个场景的缺陷提出了RedLock红锁解决安全性问题。
RedLock这个概念提出不久就引起了讨论的热潮,剑桥著名分布式研究员Martin针对红锁提出了不同的看法,Martin认为RedLock还是存在一定安全性问题,给出的结论是
neither fish nor fowl 不伦不类
原文可以参考https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
Redis的作者antirez针对Martin提出的安全性问题也做出了一一解答,更加细致的分析了RedLock的更多细节,原文可以参考http://antirez.com/news/101
antirez和Martin都是分布式方面的专家,两位专家对RedLock的不一样角度的解读对我们开发者而言简直就是神仙打架,从他们的分析中我们能更加清楚为什么分布式锁难以把控,下面根据两位分布式大佬的博客做一个简要分析。
Martin对RedLock的质疑
Martin的文章前面主要阐述以下观点
分布式锁的用途
Martin文章中指出在使用分布式锁之前先要了解你使用分布式锁的目的,可能有如下两个
-
效率:使用分布式锁可以避免让你做重复的事情,锁失效的后果是重复做了几次工作,如发了两次重复的邮件,这类微不足道的影响。
-
正确性:锁可以防止并发进程相互干扰,如果锁失效,多个进程会操作同一条数据,造成业务错乱、数据丢失等威胁到系统安全运行,这类影响巨大。
如果是因为效率那么使用单实例的分布式锁就足够了,就算出现宕机或者主从切换带来的安全性问题,这也没什么太大影响,没必要使用RedLock它太重了。
如果是因为正确性,也没必要使用RedLock因为RedLock一样存在安全性问题。
分布式锁可能遇到的问题
Martin文章中指出相对于单机的互斥锁如Java中的Lock或者synchronized,分布式锁更像一个复杂的野兽,存在很多意想不到的情况,这就是分布式的三座大山NPC
-
N:Network Delay,网络延迟
-
P:Process Pause,进程暂停(如Java中的GC线程,GC在运行过程中会导致应用程序停顿这就是_stop-the-world_)
-
C:Clock Drift,时钟漂移(指两个电脑间时间流速基本相同的情况下,两个电脑或者两个进程间,时间的差值,Martin指出时钟漂移出现的场景有运维人员手动调整系统时间或同步NTP时间出现跳跃)
Martin用下图说明了进程暂停GC对分布式锁的影响,加锁步骤如下
-
Client1去分布式锁系统请求锁(get lease),成功。
-
Client1持有锁这时触发GC线程,造成应用程序停顿。
-
应用程序停顿过程中,Client1持有的锁到达过期时间。
-
Client2去请求锁,成功。
-
Client2开始操作共享数据。
-
Client1应用程序暂停结束,但是未意识到锁过期,也去操作共享数据,这时业务错乱。
==注意:作者特意提到不是说没有GC的开发语言就不会发生上述问题,这里只是以GC举例,能产生同样影响的还有网络延迟和时钟漂移。==
依靠时钟的判断不可靠
分布式系统的异步模型
在讨论这个问题前Martin指出一个优秀的分布式系统应该基于异步模型,简单概括就是不对时间做任何假设,进程可能暂停任意时间长度,数据包可能在网络包中任意延迟,或者本地时钟本来就不正确,一个好的分布式系统不会因为这些因素影响锁的安全性,只可能影响到它的活性,也就是说在极端情况下优秀的分布式锁顶多是不能在有限的时间内给出结果,但不会给出一个错误的结果,这样的算法是真实存在的如Raft、Viewstamped Replication、Zab 和 Paxos等等。
时钟不靠谱的场景
假设Redis的实例有A、B、C、D、E五个,客户端1和2,加锁时有如下场景
-
客户端1获取实例A、B、C的锁后,由于网络问题无法访问实例D、E。
-
实例C因为时钟漂移向前跳导致锁过期。
-
客户端2获取实例C、D、E的锁后,由于网络问题无法访问实例A、B。
-
客户端1和客户端2都持有相同的锁
如果实例C在持久化前宕机也会产生这种情况,所以才会提出延迟重启这个概念,官方建议将延迟重启的时间间隔设置大于锁过期时间,但这种延迟重启一样需要依赖时间,如果时钟漂移就会出现安全性问题。
上面一个场景足以说明RedLock强依赖时钟导致的安全性问题,Martin又从GC方面提出另外一种不安全的场景,当然我们需要注意的是这种场景假设是有误的,后面antirez也进行了反驳,我们可以简单参考
Martin说如果你认为时钟跳跃不现实,或者你有信心通过正确配置NTP来保证时钟同步,那么可以看看下面GC带来的另外一个安全性问题。
-
客户端1请求锁定实例A、B、C、D、E。
-
在等待实例返回锁定结果(这时加锁已经成功)的过程中,发生GC,进入stop-the-world。
-
GC过程中所有实例的锁过期。
-
客户端2请求锁定实例A、B、C、D、E。
-
客户端1GC完成,并收到实例A、B、C、D、E的响应,表明已经成功获取锁(当进程暂停时它们被保存到客户端1的内核网络缓存区中)。
-
客户端1和2同时持有锁。
为什么说这个假设不正确,因为RedLock明确指出,当客户端开始加锁时会得到现有时间戳T1,所有实例加锁完成后会得到时间戳T2,RedLock会比较T2-T1的差值,如果大于锁的有效期那么将为所有的实例执行解锁操作,而在上面猜想的第3点就可以得出T2-T1会大于锁的有效期,也就是说客户端1加锁会失败。
提出fecing token保证准确性
Martin在分析到RedLock这些分布式锁的问题后,提出了fecing token的方案,简单来讲就是在对存储服务的每一个写入请求加入一个防护令牌,防护令牌是一个数字,每次客户端获取锁时都会将防护令牌数字递增。总结起来就是如下三个步骤
-
客户端在向锁服务获取锁时,锁服务需要提供一个递增的token值。
-
客户端需要携带这个token操作共享资源。
-
共享资源可以根据token拒绝后来者。
这样就能保证每个客户端获取的锁一定是有效的,如果客户端携带旧的令牌将拒绝这个客户端的操作。