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

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

yuyutoo 2024-10-16 15:45 8 浏览 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_)

相关推荐

网络规划建设原来也可以这么简单!

废话少说,直接上干货。天气炎热,请各位看官老爷静心阅读。整体思路下图是关于网络建设的所有相关领域,接下来我为大家逐一讲解。网络分层...

网络规划设计师笔记-第 1 章 计算机网络原理

计算机网络原理1.1计算机网络概论(P1-10)...

别输在远见上,网工这样做职业规划,比啥都强

01职业中的规划,人生中的buff“职业规划“这个词,其实对很多年轻人,包括曾经年轻的我来说,都不屑一提。...

网络规划设计师学习中(个人自学笔记分享1),有一起学习的吗?

网络规划设计师,上午考试内容学习:第一章:计算机网络概述(上部分):如果你也在一起学习,那么我们来一起学习吧!坚持1年,争取明年一次性通过!...

在微服务中使用 ASP.NET Core 实现事件溯源和 CQRS

概述:事件溯源和命令查询责任分离(CQRS)已成为解决微服务设计的复杂性的强大架构模式。基本CQRS表示形式在本文中,我们将探讨ASP.NETCore如何使你能够将事件溯源和CQRS...

一个基于ASP.NET Core完全开源的CMS 解决方案

...

用 Nginx 部署 ASP.NET Core 应用程序

用Nginx部署ASP.NETCore应用程序步骤如下:在Linux中安装.NETCore运行时和Nginx:...

Asp.net Core启动流程讲解(一)(asp.net core 入门)

asp.netcore默认项目包括项目根目录级的Startup.cs、Program.cs、appsettings.json(appsettings.Development.json)launch...

十天学会ASP之第五天(十天学会asp教程)

学习目的:学会数据库的基本操作1(写入记录)数据库的基本操作无非是:查询记录,写入记录,删除记录,修改记录。今天我们先学习写入记录。先建立一个表单:<formname="form1"met...

ASP.NET Core 的 WebApplication 类

ASP.NETCore提供了3个主机类(Host)。这些类用于配置应用、管理生命周期和启动Web服务。...

ASP.NET Core中的键控依赖注入(.net依赖注入原理)

大家好,我是深山踏红叶,今天我们来聊一聊ASP.NETCore中的FromKeyedServices,它是在.Net8中引入的。这一特性允许通过键(如字符串或枚举)来注册和检索依赖注入(D...

Asp.net常用方法及request和response-a

asp.net教程asp.net常用方法:1、Request.UrlReferrer请求的来源,可以根据这个判断从百度搜的哪个关键词、防下载盗链、防图片盗链,可以伪造(比如迅雷)。(使用全局一般处理...

ASP.NET Core EFCore 属性配置与DbContext 详解

...

asp.net常考面试题(aspnet题库)

asp.net常考面试题一,列举ASP.Net页面之间传递值的几种方式?1,使用QueryString,如:......?id=1;response.Redirect()......2,使用Sessi...

在Windows系统搭建.NET Core环境并创建运行ASP.NET网站

微软于6月27日在红帽DevNation峰会上正式发布了.NETCore1.0、ASP.NET1.0和EntityFrameworkCore1.0,其将全部支持Windows、OSX和...

取消回复欢迎 发表评论: