之前的项目中需要剥离出定时任务的代码,经过调研,我们决定使用XXL-Job来实现定时任务的调度。但是在使用过过程中,出现了一个问题——
线上测试服务器的xxl-job-admin(部署调度中心)中,配置执行器的注册方式使用自动注册的时候,OnLine机器地址总是获取不正确。而当我将xxl-job-admin部署在我本地电脑上的Tomcat中的时候,使用自动注册,OnLine机器地址能正常获取。其他条件都是一样的。
光思考,是解决不了问题的,从日志中,也没有得到有效的信息。于是,我觉定看一看XXL-JOB注册流程的源码,看一看其注册的机制。
首先,我们从Spring加载开始说起。下面,我们以Spring执行器xxl-job-executor-sample-spring为例。
在Spring执行器的配置文件applicationcontext-xxl-job.xm中,配置了一个叫做xxlJobExecutor的bean。我们先看看这个bean。
我们可以看到,当Spring加载这个Bean的时候,会自动执行其初始化方法(init-method):start方法,在其结束生命周期的时候,会自动执行其销毁方法(destroy-method):destroy方法。这里,我们先看其初始化方法——start()方法的源码:
这里,start()方法中,顺序执行了另外五个方法,从作者给的注释中结合源代码,我们可以知道,每个方法的作用。因为我们主要剖析注册机制以及其流程,所以,我们这里着重研究initExecutorServer(port, ip, appName, accessToken)这个方法。
在initExecutorServer(port, ip, appName, accessToken)这个方法中,传入了四个参数,这四个参数的取值是通过上面配置的bean的property属性获取的。其中,ip 属性尤为重要,简单的项目我们一般默认为 空 (手动设置ip会绑定Host,后文会提到)。在整个过程中,ip获取非常重要,一定要注意。下面我们看看initExecutorServer(port, ip, appName, accessToken)这个方法的源码:
很明显,这里其实就把port、ip、appName三个属性又传递到serverFactory.start(port, ip, appName)方法中了。传递过去的参数值中,如果之前执行器组件的配置中没有配置ip(默认也不配置),则这里ip仍为null;下面我们看看serverFactory.start(port, ip, appName)方法的源码:
很显然,这里参数继续传递到 JettyServer的start()方法中。如果前文没有配置ip这个属性,这里传过来的ip仍为null。下面我们研究下 JettyServer中start()方法的源码:
首先,我们可以看,第一个红色方框标记。如果前面 配置中心 组件的配置中正确配置了ip这个属性,则ip在整个传递过程中不为null,则执行connector.setHost(ip);这句逻辑,这里就是上文提到的【手动设置IP时将会绑定Host】,这段逻辑就是绑定Host。
当然,一般情况下,前面不会配置ip,则就会执行下面第二个黄色方框标记部分——把参数传递到ExecutorRegistryThread.getInstance().start(port, ip, appName);这个start()方法中。其中,ip仍为null。下面看一看ExecutorRegistryThread.getInstance().start(port, ip, appName);方法的源码:
我们首先看第一个红色方框部分。如果执行器组件Bean中配置了ip,则这里会组装ip和port为executorAddress(例如:127.0.0.1:8080 这种格式);如果执行器组件中没有配置ip,则传递到这里的ip仍为null。就会执行IpUtil.getIpPort(port)逻辑,返回值即为executorAddress。下面我们看看IpUtil.getIpPort(port)的逻辑:
这里就是直接调用本类的另一个方法getIp()来获取ip,看看getIp()源码:
在getIp()方法中,首先调用本类的getAddress()方法,如果getAddress()方法返回的是null,则再调用InetAddress类的getHostAddress()方法。首先,我们看看getAddress()方法的源码:
这里,又调用了getFirstValidAddress()方法,看看源码:
首先,调用getLocalHost()方法得到一个结果,如果其验证通过,是一个合法的IP地址,则直接认为其是本地的IP地址,将其返回。但是,这个方法,有坑!下面会进行分析!如果调用getLocalHost方法得到的结果验证没有通过,得到的结果不是一个合法的IP地址,则执行下面红色方框中的逻辑——枚举本地所有网卡,并返回第一个合法的IP地址作为本地地址。下面,我们看看验证逻辑源码:
这里代码很简单,就不详加描述了。下面,我们剖析下getFirstValidAddress()源码(从这里往上第二张图)中的InetAddress.getLocalHost()这个方法。我们先看看官方API:
刚开始测试的时候,这里InetAddress.getLocalHost()返回了一个错误的IP地址。为什么这个方法会返回一个错误的地址?因为这个方法的原理是通过 获取本机的hostname,然后对此hostname做解析,从而获取IP地址的。那么问题来了,在Linux操作系统下,如果在本机的/etc/hosts文件里对这个主机名 指向了一个错误的IP地址,那么InetAddress.getLocalHost就会返回这个错误的IP地址。当然如果你的hostname是到DNS 去解析的,碰巧DNS上的信息也是错的,也同样是悲惨结局。
然而,在这次测试中,我们遇到的问题还不仅如此。我们在配置执行器时,我们选择自动注册,结果 调度中心 的 执行器管理 条目下,发现我们配置的执行器的 OnLine机器地址(就是贯穿整个过程的ip) 是这个地址 【120.xxx.xxx.176:90xx】,90xx是我们自己配置的端口。但是,在测试机上,我们使用 ifconfig -a 查看本机网卡,并没有发现ip为【120.xxx.xxx.176】的ip。我们使用 hostname 命令查看本机配置的hostname,结果显示的是 xxxxx.cn(xxxxx是被我隐藏的地址),然后我执行 ping xxxxx.cn,果然出现了【64 bytes from 120.xxx.xxx.176: icmp_seq=1 ttl=115 time=34.4 ms】这样一句话。我们查看 /etc/hosts文件,并没有发现其中有 xxxxx.cn 相关的配置。同时,在/etc/sysconfig/network文件里面,有这样一条 HOSTNAME=ts3。也就是说,在本太机器上,定义的HOSTNAME并不是 xxxxx.cn,同时,使用hostname 命令 得到的 也不是 network 文件中定义的HOSTNAME !
这又是如何呢?通过查询资料,我发现,修改HOSTNAME后,需要重启网卡。具体原因,不详细讲,要从Linux内核代码开始说起。所以,我猜测,本机之前曾经在/etc/sysconfig/network文件中配置了HOSTNAME=xxxxx.cn,并且在/etc/hosts文件中为该hostname分配了ip 120.xxx.xxx.176;后来又修改了HOSTNAME,并且删除了hosts文件中 xxxxx.cn 相关的IP,但是,这么做了后,并没有重启网卡,所以系统默认得到的还是原来的信息。
所以,我们使用InetAddress.getLocalHost()方法得到的ip是原来的xxxx.cn对应的ip,而这个IP通过了验证,所以就没有再执行后面逻辑——枚举本地所有网卡,并返回第一个合法的IP地址。
到这里,我们就得到了我们需要的IP,也就下图中的executorAddress得到了。然后就执行下图中第二个红色方框中的逻辑:
首先,我们分析一下代码执行的流程。首先,新创建一个线程registryThread,并且该线程为守护线程。然后该线程执行逻辑——while循环,我们一直 toStop = false,也就是该端逻辑一直执行,直至注册成功。然后注意金黄色方框中的逻辑——我们已知RegistryConfig.BEAT_TIMEOUT = 30,也就是while循环每循环一次,就休眠30秒,然后再执行。——这就是所谓的心跳注册机制,每30秒进行一次注册,直至注册成功。我们再看看具体的执行逻辑:
首先将注册必需的信息组装为一个registryParam实体,然后执行具体的注册逻辑:adminBiz.registry(registryParam),我们看看adminBiz.registry()方法的源码:
其实就是把相关信息写入到数据库中。至此,注册流程就走完了。
博主也是初入职场的小菜鸟,也在不断的学习之中,希望和大家一起努力,有问题一起解决。能帮到大家的话,我也觉得十分荣幸。