百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

zookeeper分布式锁的坑让我踩了,现场复盘并深入分析

yuyutoo 2024-10-16 15:45 2 浏览 0 评论


在分布式系统里使用分布式锁来保证 Schedule Job 的线程安全是很常见的问题,为了保证同一时刻有且只有一个服务在运行该 Job。

我有一个 通过 spring Schedule 来调度的 Job,执行频率是 1 小时一次,使用 zookeeper 做的分布式锁,服务部署了三台,但今天遇到了一个诡异的问题,这个 Job 从昨天晚上 9点 到今天上午11点 一次都没有执行。看日志发现两台服务因为没有拿到锁而跳过,而拿到锁的机器一直处于假死状态。

01 技术背景

有三台 SpringMvc 的web服务,机器分别是 meta01、meta02、meta03,借助 apache curator 实现的 zookeeper 分布式锁,zookeeper 是三节点集群也部署在meta01、meta02、meta03。

服务启动时通过 afterPropertiesSet 向 zookeeper 注册临时顺序节点,头节点则拿到锁。服务 stop 时通过 destroy 放弃锁并停止与 zookeeper 的心跳和监听。

即:在服务启动时就决定了谁持有这把 zookeeper 锁,并一直持有,除非断开心跳。

02. 问题表象

一小时执行一次的 Job 从昨天晚上 9点 到今天上午11点 一次都没有执行。

从日志上看 meta01、meta02 当天正常去调度但不是 zookeeper 头节点,meta03 调度日志停留在昨天,然后这台机器上的服务进程确实还在,查到这里我的内心不由得很兴奋,因为前段时间出现的妖怪又出现了。

到此得出第一步结论:leader 节点假死,但是在 zookeeper 上的临时顺序节点并没有删除而造成锁未释放。

为了快速解决问题,我手动把 meta03 的锁节点删除了。

03. 问题排查

节点假死的原因是什么,是不是因为 FGC?我立刻查看了 heap 占用和 GC 情况,但发现这些都很正常。并且内存、CPU使用都是比较低的。

日志没有异常、内存、CPU负载低,GC正常,基本可以断定问题出在外部。

于是找到平台的人一起来排查,果然在监控平台上看到 meta03 这台机器这台是异常的。可以看到服务指标出现了断层,这台机器上的所有服务都死了。

查看各组件日志,发现了异常日志,显示系统时间被修改了,这时我才意识到 QA 为了测试是将系统时间改到前一天,当把系统时间改正确之后系统恢复正常。

但是为什么修改了系统时间会造成所有服务假死,zookeeper 的心跳不在了为什么节点没有删除?这是 zookeeper 或者 apache curator 的 bug 吗?一台服务不可用,其他服务拿不到锁就执行不了任务,这是我们需要探究的问题。

读时钟失败,并且 session timeout

此时 meta03 还是 leader

改回系统时间之后恢复正常,有了最新日志

得出第二步结论:假死原因是修改系统时间,造成集群时间不同步。

此时作出进一步猜想:按 zk 心跳机制,meta03 无法向 server 集群发出心跳,此时该 zk 锁的临时顺序节点应该被删除。但不巧的是,假死的 meta03 也是集群的 leader,写操作又由 leader 负责,所以节点无法删除。此时 meta01、meta02 也无法跟leader 通信,应该发起了选主流程,但是都是选自己,所以一直没有产生新的 leader,就这样一直尴尬下去,一直假死下去,等待 meta03 恢复正常。

04. 对猜想的分析

探究 zookeeper 心跳机制

前段时间看过 zookeeper 的源码,不得不说东西太多了,只看了一点皮毛。在前面的文章《 给 gRPC 写服务发现》介绍过 zookeeper的基本原理,感兴趣的可以看一下,这次重点看了心跳机制。

谈心跳机制之前先介绍一下 zookeeper 的启动流程。在其源码里有 mainClasses 这么一个文件,里面写着

也就是说 ZooKeeperMain 是其 Client 的入口,QuorumPeerMain 是 server 的启动类。ps 可以看到进程

Client 的功能包括向 server 发心跳、创建节点等,Server 端比如数据存储、服务端数据同步、选主。zoo.cfg 是服务启动配置项

在 ZooKeeperMain 类的 connectToZK 方法创建 ,ZooKeeperAdmin 类实例,在其构造方法里创建里 ClientCnxn 的实例,并调用 clientCnxn.start() 方法。

ClientCnxn 类创建了两个线程,这两个线程就是负责从 Client 向 Server 发送心跳包的。

其中,SendThread 负责将ZooKeeper的信息封装成一个Packet,发送给 Server ,并维持同 Server 的心跳,EventThread负责解析通过 SendThread 得到的Response,之后发送给Watcher::processEvent进行详细的事件处理。

1. SendThread

SendThread 是心跳线程,run 方法核心逻辑如下


  1. 建立和 Server 之间的 socket 链接
  2. 判断链接是否超时
  3. 定时发送心跳任务
  4. 将ZooKeeper指令发送给Server


▍心跳频率


代码注释翻译:1000(1秒)是为了防止竞争条件丢失而发送第二个ping,也请确保在readTimeout很小时不要发送太多ping。

以上面的 zoo.cfg 为例,sessionTimeout = 4000,readTimeout = 2666。getIdleSend() 是距离上次心跳发送的时间(now - lastSend),可以理解为心跳间隔毫秒数,得出频率 大约每 1333 毫秒一次。


▍心跳逻辑 sendPing

往 outgoingQueue 放入心跳包 Packet


▍Client 与 Server 长连接

clientCnxnSocket 是 Client 和 Server 建立的 Socket 长连接,而这个实例是上面的 getClientCnxnSocket() 创建的。源码如下,默认选择 NIO 方式(ClientCnxnSocketNIO 类)建立 Socket 连接。

在 ClientCnxnSocketNIO connect 方法中,Client 与 Server 建立类 Socket 连接。


session 超时

在SendThread::run中,可以看到针对链接是否建立分别有readTimeout和connetTimeout 两种超时时间,一旦发现链接超时,则抛出异常,终止 SendThread。上面提到来 readTimeout = 2666。

在没有超时的情况下,如果判断距离上次心跳时间超过了1/2个超时时间,会再次发送心跳数据,避免访问超时。


▍doTransport

通过 ClientCnxnSocketNIO 向 Server 发送指令

sendThread.primeConnection() 核心逻辑如下,可以看到在这里并没有真正向 Server 发送,而是 先放入 Queue 异步发送的。

SendThread run 方法消费 outgoinQueue 发送的心跳


2. EventThread

EventThread 线程逻辑就简单的多,就是处理 finishPacket 放到 waitingEvents 的事件。

在EventThread中通过processEvent对队列中的事件进行消费,并分发给不同的Watcher。

EventThread 线程并非本次问题的关键点,这里不再详细分析介绍。

结论:zookeeper 的心跳机制是从 ClientCnxn 的 SendThread 线程发出去的,既然系统假死,心跳肯定是没有了。而 meta03 的 EventThread 也因假死处理不了任何事件,所以就删除不了节点。

3. 细数 zookeeper 分布式锁缺陷:

  • 加锁性能低
  • 锁释放惊群效应
  • 多主或无主。leader 出现 FGC 或者其他假死情况时,心跳暂停触发选主,若选举出新leader,但老leader依然认为自己是leader就出现多主(脑裂)。无主即为上文的为空。

以上问题基本都是因为其严格的 CP 设计,任何一种分布式锁都有优缺点,当我们选择一款产品的时候要明确的知道,并接受它的缺点,为它的缺点负责。

公众号:看起来很美(kanqilaihenmei_)

相关推荐

jQuery VS AngularJS 你更钟爱哪个?

在这一次的Web开发教程中,我会尽力解答有关于jQuery和AngularJS的两个非常常见的问题,即jQuery和AngularJS之间的区别是什么?也就是说jQueryVSAngularJS?...

Jquery实时校验,指定长度的「负小数」,小数位未满末尾补0

在可以输入【负小数】的输入框获取到焦点时,移除千位分隔符,在输入数据时,实时校验输入内容是否正确,失去焦点后,添加千位分隔符格式化数字。同时小数位未满时末尾补0。HTML代码...

如何在pbootCMS前台调用自定义表单?pbootCMS自定义调用代码示例

要在pbootCMS前台调用自定义表单,您需要在后台创建表单并为其添加字段,然后在前台模板文件中添加相关代码,如提交按钮和表单验证代码。您还可以自定义表单数据的存储位置、添加文件上传字段、日期选择器、...

编程技巧:Jquery实时验证,指定长度的「负小数」

为了保障【负小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【负小数】的方法。HTML代码<inputtype="text"class="forc...

一篇文章带你用jquery mobile设计颜色拾取器

【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...

编程技巧:Jquery实时验证,指定长度的「正小数」

为了保障【正小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【正小数】的方法。HTML做成方法<inputtype="text"class="fo...

jquery.validate检查数组全部验证

问题:html中有多个name[],每个参数都要进行验证是否为空,这个时候直接用required:true话,不能全部验证,只要这个数组中有一个有值就可以通过的。解决方法使用addmethod...

Vue进阶(幺叁肆):npm查看包版本信息

第一种方式npmviewjqueryversions这种方式可以查看npm服务器上所有的...

layui中使用lay-verify进行条件校验

一、layui的校验很简单,主要有以下步骤:1.在form表单内加上class="layui-form"2.在提交按钮上加上lay-submit3.在想要校验的标签,加上lay-...

jQuery是什么?如何使用? jquery是什么功能组件

jQuery于2006年1月由JohnResig在BarCampNYC首次发布。它目前由TimmyWilson领导,并由一组开发人员维护。jQuery是一个JavaScript库,它简化了客户...

django框架的表单form的理解和用法-9

表单呈现...

jquery对上传文件的检测判断 jquery实现文件上传

总体思路:在前端使用jquery对上传文件做部分初步的判断,验证通过的文件利用ajaxFileUpload上传到服务器端,并将文件的存储路径保存到数据库。<asp:FileUploadI...

Nodejs之MEAN栈开发(四)-- form验证及图片上传

这一节增加推荐图书的提交和删除功能,来学习node的form提交以及node的图片上传功能。开始之前需要源码同学可以先在git上fork:https://github.com/stoneniqiu/R...

大数据开发基础之JAVA jquery 大数据java实战

上一篇我们讲解了JAVAscript的基础知识、特点及基本语法以及组成及基本用途,本期就给大家带来了JAVAweb的第二个知识点jquery,大数据开发基础之JAVAjquery,这是本篇文章的主要...

推荐四个开源的jQuery可视化表单设计器

jquery开源在线表单拖拉设计器formBuilder(推荐)jQueryformBuilder是一个开源的WEB在线html表单设计器,开发人员可以通过拖拉实现一个可视化的表单。支持表单常用控件...

取消回复欢迎 发表评论: