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

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

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

相关推荐

深度解读Spring框架的核心原理

深度解读Spring框架的核心原理在Java开发的世界里,提到Spring框架,就像提起一位久经沙场的老将,它几乎成了企业级应用开发的代名词。那么,这个被无数开发者膜拜的框架究竟有何独特之处?今天,我...

「Spring认证」Spring 框架概述

Spring是最流行的企业Java应用程序开发框架。全球数以百万计的开发人员使用SpringFramework来创建高性能、易于测试和可重用的代码。Spring框架是一个开源的Java...

学习Spring框架 这一篇就够了

1.spring概述1.1Spring是什么(理解)...

Spring框架双核解析:IOC与AOP的本质与实战

#Spring核心#IOC容器#AOP编程#Java框架设计...

Spring Boot与传统Spring框架的对比:探索Java开发的新境界

SpringBoot与传统Spring框架的对比:探索Java开发的新境界在Java生态系统中,Spring框架无疑是一个里程碑式的存在。从最初的简单依赖注入容器,到如今覆盖企业级开发方方面面的庞大...

Spring MVC框架源码深度剖析:从入门到精通

SpringMVC框架源码深度剖析:从入门到精通SpringMVC框架简介SpringMVC作为Spring框架的一部分,为构建Web应用程序提供了强大且灵活的支持。它遵循MVC(Model-V...

Spring框架入门

一.spring是什么?Spring是分层...

程序员必知必会技能之Spring框架基础——面向切面编程!

面向切面编程AOP(AspectOrientedProgramming)与OOP(ObjectOrientedProgramming,面向对象编程)相辅相成。AOP提供了与OOP不同的抽象软件结...

Spring Security安全框架深度解读:为你的应用穿上“钢铁铠甲”

SpringSecurity安全框架深度解读:为你的应用穿上“钢铁铠甲”在现代网络世界里,保护我们的应用程序免受各种威胁攻击至关重要。而在这个过程中,SpringSecurity框架无疑是我们最可...

Spring框架的设计哲学与实现:打造轻量级的企业级Java应用

Spring框架的设计哲学与实现:打造轻量级的企业级Java应用Spring框架自2003年诞生以来,已成为企业级Java应用开发的代名词。它不仅仅是一个框架,更是一种设计理念和哲学的体现。本文将带你...

Spring框架深度解析:从核心原理到底层实现的全方位避坑指南

一、Spring框架核心概念解析1.控制反转(IoC)与依赖注入(DI)Spring的核心思想是通过IoC容器管理对象的生命周期和依赖关系。传统开发中,对象通过new主动创建依赖对象,导致高耦合;而S...

Java框架 —— Spring简介

简介一般来说,Spring指的是SpringFramework,它提供了很多功能,例如:控制反转(IOC)、依赖注入...

Spring 框架概述,模块划分

Spring框架以控制反转(InversionofControl,IoC)和面向切面编程(Aspect-OrientedProgramming,AOP)为核心,旨在简化企业级应用开发,使开发者...

spring框架怎么实现依赖注入?

依赖注入的作用就是在使用Spring框架创建对象时,动态的将其所依赖的对象注入到Bean组件中,其实现方式通常有两种,一种是属性setter方法注入,另一种是构造方法注入。具体介绍如下:●属性set...

Spring框架详解

  Spring是一种开放源码框架,旨在解决企业应用程序开发的复杂性。一个主要优点就是它的分层体系结构,层次结构让你可以选择要用的组件,同时也为J2EE应用程序开发提供了集成框架。  Spring特征...

取消回复欢迎 发表评论: