性能文章>面试官:你给我讲一讲,Dubbo暴力停机,消费者是如何感知服务下线的?>

面试官:你给我讲一讲,Dubbo暴力停机,消费者是如何感知服务下线的?原创

312815

**本文首发于公众号【看点代码再上班】,建议关注公众号,及时阅读最新文章。**

一定要读的原文:https://mp.weixin.qq.com/s?__biz=MzIwMDEzO……

 

大家好,我是tin,这是我的第21篇原创文章

编辑

(从原文拷贝过来,每张图片下都带了编辑两字,去还去不掉,影响阅读体验,太烦了……)

 

上一篇我们讲到了Dubbo服务正常重启下线时是如何优雅停机的,其中有一个环节就非常重要:

 

通知注册中心下线服务。

 

重启的服务因为是主动关闭Spring容器,所以有时间也有主动权去告知注册中心“我要下线了”。

 

但是,对于暴力停机,比如kill -9或者机器宕机,Dubbo服务又是如何通知到注册中心的呢?

 

要想知道真正原因,得从注册中心的心跳机制聊起。今天就结合zk注册中心一起看一看,先上一个目录:

目录

一、zookeeper配置参数

二、zookeeper的数据存储结构

三、zookeeper与dubbo的心跳感应

1. client定时发送ping

2. server定时检测client

四、zk server摘除宕机client节点

五、dubbo消费者摘除宕机client节点

六、结语


 

一、zookeeper配置参数

我们先来看一看zookeeper的配置参数。还记得如何安装zookeeper么?这里有一份攻略(都大同小异):

https://zhuanlan.zhihu.com/p/466902641

 

zookeeper的安装文件目录结构也比较简单,如下图:

编辑

 

  • bin目录:zookeeper支持的运行命令集合

编辑

 

这些命令集合分为windows系统命令和linux系统命令。

 

以 .cmd 结尾的命令,即是在windows环境上运行使用的命令,以.sh 结尾的命令,即是在linux环境上运行使用的命令。

 

  • conf目录:存放配置文件,包括日志配置、zookeeper启动配置等

编辑

 

在运行zk之前,必须配置 .cfg。注意,默认是没有zoo.cfg文件的,但是有zoo_sample.cfg文件,需要重命名为zoo.cfg。

 

zoo.cfg文件比较重要,zookeeper的心跳间隔参数就在此中,下面是此文件中一些主要参数说明:

1.tickTime:client和server间通信心跳时间,单位是毫秒

zookeeper 客户端与服务器之间建立长连接,并通过心跳机制保持通信,每个 tickTime 时间间隔就会发送一个心跳。tickTime以毫秒为单位,默认是2000毫秒。

tickTime=2000

 

2.initLimit:LF初始通信时限

这是一个zookeeper集群参数,表示集群中的follower服务器(F)与leader服务器(L)之间初始连接时能容忍的最多心跳数(tickTime的数量)。

initLimit=5

 

3.syncLimit:LF同步通信时限

这是一个zookeeper集群参数,表示集群中的follower服务器与leader服务器之间请求和应答之间能容忍的最多心跳数(tickTime的数量)。

syncLimit=2

 

4.dataDir:zookeeper用于保存内存数据库的快照的目录

zookeeper保存数据的目录,默认情况下,zookeeper将写数据的事务日志文件也保存在这个目录里。

dataDir=/tmp/zookeeper

 

5.clientPort:客户端连接的socket端口

客户端连接server的端口,即对外服务端口,一般设置为2181。

clientPort=2181

 

  • docs目录:帮助文档

编辑

 

包括一些官方说明文档,比如zookeeperStarted.html文件,打开可以看到zookeeper的安装启动向导:

编辑

 

  • lib目录:zookeeper需要依赖的jar包

 

因为zookeeper是用Java开发的,zookeeper依赖的jar包都会放在这个目录中。

 

二、zookeeper的数据存储结构

zookeeper存储数据的核心数据结构是一个DataTree,源码是采用一个Map<String, DataNode>数据结构,其中key是path,DataNode是真正保存数据的核心数据结构。

 

类似文件系统的目录结构进行存储,目录树中的每个节点被称为Znode,Znode既可以存储数据,也可以拥有自己的子节点。

 

比如保存dubbo服务信息的结构如下:

编辑

 

总共分为四层,从上到下,分别为group分组、服务全限定名接口、分类、服务节点地址。

 

上图右侧即是节点Znode的关系依赖,在最底层的是服务节点url地址,格式形如dubbo://com.xx.xx:xxxx?xx=xx。

 

比如我本地起的服务,最终保存到zookeeper的服务提供者节点信息如下:

dubbo://192.168.31.19:30058/com.tin.example.dubbo.demo.facade.GiftFacade?anyhost=true&application=demo-provider&class=com.tin.example.dubbo.demo.impl.GiftFacadeImpl&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.tin.example.dubbo.demo.facade.GiftFacade&mapping-type=metadata&mapping.type=metadata&metadata-type=remote&methods=give&pid=25334&release=2.7.15&serialization=jackson&service.name=ServiceBean:/com.tin.example.dubbo.demo.facade.GiftFacade&side=provider&telnet=ls,ps,cd,pwd,trace,count,invoke,select,status,log,help,clear,exit,shutdown&timeout=500&timestamp=1650163947650

 

当然zookeeper保存的url是进行encode编码后的url(以上是我decode后的结果),如下:

编辑

 

三、zookeeper与dubbo的心跳感应

前面配置说明有说到zookeeper的心跳参数tickTime,不错,它是zookeeper在server端检测client存活的时间间隔。

 

我们把zookeeper源码下载下来一看究竟。因为我的dubbo版本是2.7.15,依赖的zookeeper版本是3.4.13。

编辑

 

所以我下载zookeeper3.4.13版本。

 

zookeeper源码地址,有需自取:https://github.com/apache/zookeeper/tree/branch-3.4.13

 

在zookeeper的client端(比如集成到dubbo内的zookeeper-client),则定时向zookeeper的server发送ping包,上报自身的健康状态。

 

注意,这里的client端的ping包发送和server端间隔tickTime进行存活检测不一样。

 

client端的ping包发送是轮询隔一段时间后向server端发送ping请求,其意义是告诉server端,我还活着。

 

而server端的tickTime间隔时间进行存活检测意义是检测client连接对象是否已经无效,如果已经无效则将连接对象进行清除关闭。

 

1. client定时发送ping

重点逻辑在org.apache.zookeeper.ClientCnxn.SendThread#run中。

 

SendThread是ClientCnxn的一个内部类,一个SendThread也即是一个线程,它继承了ZookeeperThread,而ZookeeperThread继承了java.lang.Thread。

编辑

 

很多框架源码都很喜欢使用内部类,JDK源码也经常看到这样的用法。在我看来,这种用法用好了一定程度上更忠于面向对象编程。

 

现在的我写业务代码也时常会使用内部类,因为它让逻辑更内聚到一个类内。

 

ClientCnxn是一个非常重要的一个类,它在zk client启动的时候生成,主责管理客户端的套接字io,且它维护着可用的zk服务器列表。

编辑

 

回到SendThread的run方法,其内部是一个while循环。我们进入到源码state.isConnected()片段,如下:

编辑

 

一开始看①处的注释说避免当读超时值设置过小时ping发送过于频繁的问题,但一直不明白是如何做到的。

 

既然是为了避免readTimeout设置过小导致频繁发送ping包,为什么还要readTimeout/2?

 

认真看了很久才恍然大悟,其实是以下这行代码的功劳:((clientCnxnSocket.getIdleSend() > 1000) ? 1000 : 0)。

编辑

 

 

②处代码是①处的一个补充,就是当客户端超过MAX_SEND_PING_INTERVAL时间(默认10s)没有发送读写请求时,也会向server端发送ping包。

 

③的sendPing()方法就是向服务端发送一个空包:

编辑

 

④重置last send时间this.lastSend = now。

 

2. server定时检测client

在zk的安装包目录下,有一个包bin。前面也有讲过,这个目录都是一些命令文件,其中就有一个zkServer.sh文件。

 

打开zkServer.sh文件,找到zk服务器启动类。

编辑

 

可以看到,org.apache.zookeeper.server.quorum.QuorumPeerMain类是zk服务器启动类,它里面有一个main方法。

 

编辑

接着是调用org.apache.zookeeper.server.quorum.QuorumPeerMain#initializeAndRun方法,

编辑

 

initializeAndRun()方法内又分为集群模式和单机模式启动,分别对应①和②处,因为我们本节只为讨论server是如何和client保持连接的,所以我们看②处代码,当然①处代码也是一样的。

 

ZookeeperServerMain最终会实例化一个ZookeeperServer,最后也即是一个zk服务器进程对应一个ZookeeperServer,如下图:

编辑

 

new出来的ZookeeperServer由zk的一个启动工厂类负责启动:

编辑

 

工厂类统一调用ZookeeperServer的startup方法:

编辑

 

org.apache.zookeeper.server.ZooKeeperServer#startupWithServerState方法逻辑很清晰,其中第一步就是初始化SessionTrackerImpl类,见下图①方法内部实现:

编辑

 

SessionTrackerImpl是一个线程类,它继承了Thread。

编辑

 

最后tickTime参数传到ExpireQueue内,别名也即expirationInterval属性。

编辑

 

SessionTrackerImpl是一个线程类,自然最重要的逻辑也就是在run()方法内了:

编辑

 

在while循环内,①处是不停地取出wait_time,而这个wait_time则和tickTime(也即是expirationTime)有关,如下:

编辑

 

expirationTime是一个long型的包装类,其value即是由expirationInterval计算而来,如下:

编辑

 

接下来,经过一定的waitTime后,在②处,逐个取出过期的session(zk客户端)并删除。这就是server定时检测client的原理。

 

四、zk server摘除宕机client节点

zk server摘除宕机的client分为两部分。

 

第一部分是删除和client之间的session连接。

 

第二部分是删除zk临时节点。

 

经过上一节的介绍和源码分析,在我们看到SessionTrackerImpl类的run()方法时,已经看到zk Server处理过期session的逻辑,如下:

编辑

 

上图②处代码即是删除过期session的源码,我们先看看sessionExpiryQueue是怎么管理这些客户端的。

 

其内部是一个hash数据结构:

编辑
  • key是expirationTime;

  • value是一个Set,存放对应此时间过期的session。

 

ExpiryQueue会定期更新expiryMap,当client的心跳ping包传输过来的时候会再更新对应的Session的expirationTime。

 

所以,zk是通过remove expiryMap中key值为expirationTime的value,即达到删除过期session的目的。

编辑

 

除了关闭session,还要清除zk node。

 

打开expirer.expire(s)方法,一直看进去,在close方法提交了一个关闭session的事务请求:

编辑

 

提交close session请求后,这个请求就进入zk的处理链中。最后到达FinalRequestProcessor这个处理器。

编辑

 

在处理链后半段比较重要的类是DataTree,delete node的源码即在其内。

 

看下这个方法:

org.apache.zookeeper.server.DataTree#processTxn(org.apache.zookeeper.txn.TxnHeader, org.apache.jute.Record, boolean)

编辑

 

在killSession内最终会调用DataTree.deleteNode方法,删除node节点。

 

到此zk server完成了临时节点的删除。

 

五、dubbo消费者摘除宕机client节点

dubbo服务提供者在zk上注册了临时节点,消费者监听该临时节点。一旦临时节点有修改,zk就会通知消费者,消费者进行处理。

 

对于服务提供者宕机的情况,上文已说明zk server自动删除临时节点的逻辑,接下来我们看看消费者又是如何做到摘除client节点的。

 

分两部分,第一部分是client启动时向对应的临时节点注册监听器;第二部分是zk节点有变更时,client接收对应的event并做出相应的处理动作。

 

dubbo启动时,会初始化一个zk注册器ZookeeperRegistry(dubbo源码),用于服务提供者服务注册和服务消费者服务订阅。

 

下面着重讲服务订阅部分,对应doSubscribe方法源码。

编辑

 

public void doSubscribe(final URL url, final NotifyListener listener) {        if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {        // ① 全量service订阅逻辑        } else {        // ② 部分类别订阅逻辑        }    }

 

以上①处是服务治理(dubbo-admin)需要用到,订阅所有的service,因为我们是消费者,那么只需要进入②,如下图:

编辑

 

②处代码作用是为当前分类节点添加“子节点列表变更的”watcher监听,zkListener是RegistryChildListenerImpl,通过debug也证明了这点: 

编辑

 

RegistryChildListenerImpl的listener属性最终值类型是RegistryDirectory。

 

所以,当zk节点有变更时,最终会回调到RegistryDirectory#notify方法。

 

RegistryDirectory#notify源码如下:

编辑

 

以上④即实现consumer本地摘除提供者节点!同理,摘除宕机client节点也就在这里啦!

 

我们做一个测试,比如我本地启动一个provider,同时启动一个consumer。

 

那么,在我的zk服务器上是可以查得到这两个节点的:

编辑

 

同样,在本地通过jps命令也可以看到:

编辑

 

接下来,我通过执行kill -9,把ProviderApplication这个进程删除:

编辑

 

然后,本地consumer在RegistryDirectory#notify打个断点,等待一会后,代码执行到断点处了:

编辑

 

因为我的服务只有一个provider,断点最终进入到org.apache.dubbo.registry.integration.RegistryDirectory#destroyAllInvokers方法,其内部实现destroy invoker,最终实现摘除provider节点,如下:

编辑

 

好啦,现在consumer也走完摘除client节点逻辑啦~

 

六、结语

我是tin,一个在努力让自己变得更优秀的普通工程师。自己阅历有限、学识浅薄,如有发现文章不妥之处,非常欢迎加我提出,我一定细心推敲并加以修改。

 

看到这里请安排个“三连”(分享、点赞、在看)再走吧,坚持创作不容易,你的正反馈是我坚持输出的最强大动力,谢谢!

编辑

​​

分类:
标签:
请先登录,再评论

有启发~

2天前

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr