**GC 不是导致线程暂停的唯一原因啊,朋友们。**发生这种情况的原因有很多的,你看看长发哥书里举的例子:
上面的内容总结起来,就是就算锁服务是正常的,但是由于锁是有持有时间的,由于客户端阻塞、长时间的 GC 或者网络原因,导致共享资源被一个以上的客户端同时访问了。
其实上面长发哥在书里直接说了:这是不正确的实现。
你多品一品,上面的图是不是有点像由于 Redis 锁的过期时间设置的不合理,导致前一个任务还没执行完成,但是锁的时间到期了,后一个任务也申请到了锁。
对于这种场景,Redission 其实有自己的看门狗机制。但是不在这次 Redlock 的讨论范围内,所以这里就不描述了。
长发哥提出的解决方案是什么呢?
他称为:fencing token。
长发哥认为使用锁和租约机制来保护资源的并发访问时,必须确保因为异常原因,导致锁过期的那个节点不能影响其他正常的部分,要实现这一目标,可以采用一直相当简单的 fencing(栅栏)。
假设每次锁服务在授予锁或者租约时,还会同时返回一个 fencing 令牌,该令牌每次授予都会递增。
然后,要求客户端每次向存储系统发送写请求时,都必须包含所持有的 fencing 令牌。存储系统需要对令牌进行校验,发现如果已经处理过更高令牌的请求,则拒绝执行该请求。
比如下面的图片:
1.客户端 1 获得一个具有超时时间的锁的同时得到了令牌号 33,但随后陷入了一个长时间的暂停直到锁到期。
2.这时客户端2已经获得了锁和令牌号 34 ,然后发送写请求(以及令牌号 34 )到存储服务。
3.接下来客户端 1 恢复过来,并以令牌号 33 来尝试写入,存储服务器由于记录了最近已经完成了更高令牌号(34 ),因此拒绝令牌号 33 的写请求。
这种版本号的机制,让我不禁想起了 Zookeeper。当使用 ZK 做锁服务时,可以用事务标识 zxid 或节点版本 cversion 来充当 fencing 令牌,这两个都可以满足单调递增的要求。
在长发哥的这种机制中,实际上就是要求资源本身必须主动检查请求所持令牌信息,如果发现已经处理过更高令牌的请求,要拒绝持有低令牌的所有写请求。
但是,不是所有的资源都是数据库里面的数据,我们可以通过版本号去支持额外的令牌检查的,那么对于不支持额外的令牌检查资源,我们也可以借助这种思想绕过这个限制,比如对于访问文件存储服务的情况,我们可以将令牌嵌入到文件名中。
总之,为了避免在锁保护之外发生请求处理,需要进行额外的检查机制。
长发哥在书中也说到了:在服务端检查令牌可能看起来有点复杂,但是这其实是推荐的正确的做法:系统服务不能假定所有的客户端都表现的符合预期。从安全角度讲,服务端必须防范这种来自客户端的滥用。
这个就类似于我们作为后端开发人员,也不能相信来自前端或者其他接口过来的数据,必须对其进行校验。
到这里长发哥铺垫完成了,开始转头指向 RedLock,他认为 Redlock 是一个严重依赖系统时钟的分布式锁。
他举了一个例子:
1.客户端 1 从 Redis 节点 A, B, C 成功获取了锁。由于网络问题,无法访问 D 和 E。
2.节点 C 上的时钟发生了向前跳跃,导致它上面维护的锁过期了。
3.客户端 2 从 Redis 节点 C, D, E 成功获取了同一个资源的锁。由于网络问题,无法访问 A 和 B。 现在,客户端 1 和客户端 2 都认为自己持有了锁。
这样的场景是可能出现的,因为 Redlock 严重依赖系统时钟,所以一旦系统的时间变得不准确了,那么该算法的安全性也就得不到保障了。
长发哥举这个例子其实是为了辅佐他前面提出的观点:一个好的分布式算法应该是基于异步模型的,算法的安全性不应该依赖与任何记时假设,就是不能把时间作为安全保障的。在异步模型中,程序暂停、消息在网络中延迟甚至丢失、系统时间错误这些因素都不应该影响它的安全性,只能影响到它的活性。
用大白话说,就是在极其极端的情况下,分布式系统顶天了也就是在有限的时间内不能给出结果而已,而不能给出一个错误的结果。
这样的算法实际上是存在的,比如 Paxos、Raft。很明显,按照这个标准, Redlock 的安全级别是不够的。
而对于卷发哥提出的延迟启动方案,长发哥还是一棒子打死:你延迟启动咋的?延迟启动还不是依赖于合理准确的时间度量。
可能是长发哥觉得举这个时钟跳跃的例子不够好的,大家都可能认为时钟跳跃是不现实的,因为对正确配置NTP就能摆正时钟非常有信心。
在这种情况下,他举了一个进程暂停可能导致算法失败的示例:
1.客户端 1 向 Redis 节点 A, B, C, D, E 发起锁请求。
2.各个 Redis 节点已经把请求结果返回给了客户端 1,但客户端 1 在收到请求结果之前进入了长时间的 GC 阶段。
3.长时间的 GC,导致在所有的 Redis 节点上,锁过期了。
4.客户端 2 在 A, B, C, D, E 上申请并获取到了锁。
5.客户端 1 从 GC 阶段中恢复,收到了前面第 2 步来自各个 Redis 节点的请求结果。客户端 1 认为自己成功获取到了锁。
6.客户端 1 和客户端 2 现在都认为自己持有了锁。
其实只要十分清楚 Redlock 的加锁过程,我们就知道,这种情况其实对于 Redlock 是没有影响的,因为在第 5 步,客户端 1 从 GC 阶段中恢复过来以后,在 Redlock 算法中,(我们前面 Redlock 简介的时候提到的第四步)如果取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间。
所以客户端1通过这个检查发现锁已经过期了,不会再认为自己成功获取到锁了。
而随后卷发哥的回击中也提到了这点。
但是,细细想来,我觉得长发哥的意图不在于此。抛开上面的问题来讲,他更想突出的是,一个锁在客户端拿到后,还没使用就过期了,这是不好的。从客户端的角度来看,就是这玩意不靠谱啊,你给我一把锁,我还没用呢,你就过期了?
除了上面说的这些点外,长发哥还提出了一个算是自己的经验之谈吧:
我们获取锁的用途是什么?
在他看来不外乎两个方面,效率和正确性。他分别描述如下:
如果是为了效率,那么就是要协调各个客户端,避免他们做重复的工作。这种场景下,即使锁偶尔失效了,只是可能出现两个客户端完成了同样的工作,其结果是成本略有增加(您最终向 AWS 支付的费用比原本多5美分),或者带来不便(例如,用户最终两次收到相同的电子邮件通知)。
如果是为了正确性,那么在任何情况下都不允许锁失效的情况发生,因为一旦发生,就可能意味着数据不一致,数据丢失,文件损坏,或者其它严重的问题。(比如个患者注射了两倍的药剂)
最后,长发哥得出的结论是:neither fish nor fowl(不伦不类)
对于提升效率的场景下,使用分布式锁,允许锁的偶尔失效,那么使用单 Redis 节点的锁方案就足够了,简单而且效率高。用 Redlock 太重。
对于正确性要求高的场景下,它是依赖于时间的,不是一个足够强的算法。Redlock并没有保住正确性。
那应该使用什么技术呢?
长发哥认为,应该考虑类似 Zookeeper 的方案,或者支持事务的数据库。
卷发哥回击
长发哥发出《How to do distributed locking》这篇文章的第二天,卷发哥就进行了回击,发布了名为《Is Redlock safe?》的文章。
要说大佬不愧是大佬,卷发哥的回击条理清楚,行文流畅。他总结后认为长发哥觉得 Redlock 不安全主要分为两个方面:
1.带有自动过期功能的分布式锁,需要一种方法(fencing机制)来避免客户端在过期时间后使用锁时出现问题,从而对共享资源进行真正的互斥保护。长发哥说Redlock没有这种机制。
2.长发哥说,无论问题“1”如何解决,该算法本质上都是不安全的,因为它对系统模型进行了记时假设,而这些假设在实际系统中是无法保证的。
对于第一个点,卷发哥列了5大点来反驳这个问题,其中一个重要的观点是他认为虽然 Redlock 没有提供类似于fencing机制那样的单调递增的令牌,但是也有一个随机串,把这个随机串当做token,也可以达到同样的效果啊。当需要和共享资源交互的时候,我们检查一下这个token是否发生了变化,如果没有再执行“获取-修改-写回”的操作。
最终得出的结论是一个灵魂反问:既然在锁失效的情况下已经存在一种fencing机制能继续保持资源的互斥访问了,那为什么还要使用一个分布式锁并且还要求它提供那么强的安全性保证呢?
然而第二个问题,对于网络延迟或者 GC 暂停,我们前面分析过,对 Redlock 的安全性并不会产生影响,说明卷发哥在设计的时候其实是考虑过时间因素带来的问题的。
但是如果是长发哥提出的时钟发生跳跃,很明显,卷发哥知道如果时钟发生跳跃, Redlock 的安全性就得不到保障,这是他的命门。
但是对于长发哥写时钟跳跃的时候提出的两个例子:
1.运维人员手动修改了系统时钟。
2.从NTP服务收到了一个大的时钟更新事件。
卷发哥进行了回击:
第一点这个运维人员手动修改时钟,属于人为因素,这个我也没办法啊,人家就是要搞你,怎么办?加强管理,不要这样做。
第二点从NTP服务收到一个大的时钟更新,对于这个问题,需要通过运维来保证。需要将大的时间更新到服务器的时候,应当采取少量多次的方式。多次修改,每次更新时间尽量小。
关于这个地方的争论,就看你是信长发哥的时间一定会跳跃,还是信卷发哥的时间跳跃我们也是可以处理的。
关于时钟跳跃,有一篇文章可以看看,也是这次神仙打架导致的产物:
文章得出的最终结论是:时钟跳跃是存在的。
其实我们大家应该都经历过时钟跳跃的情况,你还记得2016年的最后一天,当时有个“闰秒”的概念吗?导致2017年1月1日出现了07:59:60的奇观。
打架的焦点
经过这样的一来一回,其实双方打架的焦点就很明确了,就是大延迟对分布式锁带来的影响。
而对于大延迟给Redlock带来的影响,就是长发哥分析的那样,锁到期了,业务还没执行完。卷发哥认为这种影响不单单针对 Redlock ,其他具有自动释放锁的分布式锁也是存在一样的问题。
而关于大延迟的问题,我在某社交平台上找到了两位神仙的下面的对话:
卷发哥问:我想知道,在我发文回复之后,我们能否在一点上达成一致,就是大的消息延迟不会给Redlock的运行造成损害。
长发哥答:对于客户端和锁服务器之间的消息延迟,我同意你的观点。但客户端和被访问资源之间的延迟还是有问题的。
所以通过卷发哥的回击文章和某社交平台的记录,他是同意大的系统时钟跳跃会造成 Redlock 失效的。在这一点上,他与长发哥的观点的不同在于,他认为在实际系统中是可以通过好的运维方式避免大的时钟跳跃的。
所以到这里,两位神仙好像又达到了一个平衡,实现了争论上的求同存异。
打架总结
作为一个互联网行业的从业者,也是分布式系统的使用者,读完他们的文章以及由此文章衍生出来的知识点后,受益良多,于是写下此文作为学习总结,也与大家分享。本文还有很多不足之处,还请各位海涵。
如同文章开篇说的,这场争论没有最后的赢家。很明显卷发哥是没有说服长发哥的,因为在长发哥2017年出版的《数据密集型应用系统设计》一书中,专门有一小节的名称叫做:不可靠的时钟
其实在这场争论的最后,长发哥对这场争论进行了一个非常感性的总结,他说:
下面翻译来自:https://www.jianshu.com/p/dd66bdd18a56
对我来说最重要的一点在于:我并不在乎在这场辩论中谁对谁错 —— 我只关心从其他人的工作中学到的东西,以便我们能够避免重蹈覆辙,并让未来更加美好。前人已经为我们创造出了许多伟大的成果:站在巨人的肩膀上,我们得以构建更棒的软件。
对于任何想法,务必要详加检验,通过论证以及检查它们是否经得住别人的详细审查。那是学习过程的一部分。但目标应该是为了获得知识,而不应该是为了说服别人相信你自己是对的。有时候,那只不过意味着停下来,好好地想一想。
吃瓜网友的收获
这里的吃瓜网友就是指我啦。
写这篇文章我的收获还是挺大的,首先我买了长发哥的《数据密集型应用系统设计》一书,读了几节,发现这书是真的不错,豆瓣评分9.6,推荐。
其次完成了这周的周更任务,虽然写的很艰难,从周六中午,写到周日凌晨3点。。。
然后还吃到了另外的一个瓜,可谓是瓜中瓜。
这周五的时候 Redis 官网不是出现了短暂的宕机吗,宕机其实也没啥稀奇的,但是页面上显示的是连不上 Redis 。这就有点意思了。
我在写这篇文章的时候,在卷发哥的某社交平台上发现了这个:
我关心的并不是 OOM,而是卷发哥居然让 Redis 官网运行在一台一个月仅 5 美元,内存只有 1G 的虚拟机上。哈哈哈,震惊,这瓜味道不错。
最后,由于卷发哥是个意大利人,由于最近疫情,四川专家组驰援意大利的事,big thank 中国人。其实这个网友的回答挺好的:投桃报李。
疫情早点过去吧,世界和平。
最后说一句(求关注)
我写到这里的时候,不知不觉已经凌晨3点多了,但是因为一直跟着这两位大神的激烈讨论,我的思维异常的清晰。
写完之后我也说不出谁对谁错。我觉得对于系统的设计,每个人的出发点都不一样,没有完美的架构,没有普适的架构,但是在完美和普适能平衡的很好的架构,就是好的架构。
瞟了一眼文章字数,快突破了1.2w字。可能又是一篇写了没人看的劝退文吧,但是没有关系,只要有一个人看了我的文章觉得有帮助就行。
点个赞吧,写文章很累的,不要白嫖我,需要一点正反馈。
才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。(我每篇技术文章都有这句话,我是认真的说的。)
感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。
我是why技术,一个不是大佬,但是喜欢分享,又暖又有料的四川好男人。
参考链接
1.http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
2.https://redis.io/topics/distlock
3.http://antirez.com/news/101
4.https://www.jianshu.com/p/dd66bdd18a56
5.《数据密集型应用系统设计》