我所了解的目前主流分布式job就是elastic-job、xxl-job和PowerJob。ejob太重,pjob开源太晚,xxljob评价又不错,中庸地选型了xxl-job。
本文提供xxl-job优雅停止停止执行器即客户端tomcat时没有及时剔除服务,不停报错500的临时解决办法。(最近较忙积累了一堆内容,没空写博客,由于github上有道友翻出了这个问题,我就先写一下这个内容,后续还有一篇关于xxl-job企微告警的内容)
如果把它看做是个人行为的开源框架那确实是没话说——起立致敬,要是站在分布式job唯一解决方案的角度去看待它,我觉得是不够理想的。如果是单体项目qz就足够了,然而xxl-job在分布式的支持上,我认为还不够好,目前仍有非常多的瑕疵,远远没达到可以停止维护的程度。由于是个人项目,观察github上的情况,可以发现作者基本已经停止维护了,项目状态就是open-issue很多,解决问题的人很少,换成中间件的话,这种程度基本意味着项目要凉。
(题外话——这就是国内的情况,大多有能力的程序员把时间花在较为功利性的行为上,要么走面霸路专注于底层的吹牛逼,要么会用就行类尝遍所有的潮流框架级技术,鲜少有人会去为这类开源框架贡献一份力,做些脚踏实地的事,社区活跃只有提问,没有解决,也就造成作者的力不从心,开源项目也就处于停摆状态——没有什么大bug,凑合着也能用)
网上评测的都是营销号复制来复制去,我甚至怀疑他们连github都没登过,这也就造成一搜xxl-job,搜索结果——好东西,什么bug都没有,而真正商用的人,就跟藏宝贝一样,改造的内容不会开源出来,也不会提出问题。
执行器重启也就是tomcat重启时,如果你直接杀死tomcat,xxl-job不会认为执行器下线的,会在一段时间内不停调用这台tomcat的执行器,并且调度失败,500报错,如果你配置了告警,那完蛋了,邮箱轰炸,钉钉轰炸,企微轰炸,被运维立马拖出去暴打。
后面发现xxl-job是提供执行器下线接口的
(
==> com.xxl.job.admin.controller.JobApiController
==> @RequestMapping("/{uri}")
==> } else if (“registryRemove”.equals(uri))
)
,并且在客户端,只要是优雅停机,客户端就会调用xxl-job-admin端的停机。但是,随即又发现服务端的停机仍然存在问题。
这是21年5月17日我在github上提出的issue,后面发现2020年8月20日有个类似的issue,就明白凉了凉了,要么重新选型,要么自己解决吧。
issue2422:https://github.com/xuxueli/xxl-job/issues/2422
类似issue
issue1917:https://github.com/xuxueli/xxl-job/issues/1917
v-2.3.0
目前是 客户端优雅停机时会调用destroy方法 方法里调用api api/registryRemove 通知调度中心
xxl-job-admin里的registryRemove方法只删除了xxl_job_registry表的内容 剔除服务仍需等待线程registryMonitorThread(BEAT_TIMEOUT = 30) 默认为30s执行才会真正剔除执行器,会导致大概0-30s的时间分流某执行器全是500报错
JobRegistryHelper里预留了freshGroupRegistryInfo方法,我将registryMonitorThread内容放入大大降低了报错,只会损失执行registryMonitorThread内容期间的调用,是否计划有更优雅的方法。
从某种程度来说既然调用api/registryRemove肯定意味着客观停机,应即时剔除服务,执行器上线延迟可以接受,执行器下线延迟不太接受。
如果你没配置告警无所谓这个报错,后续可能也会出现慢sql,类似这个issue
issue2415:https://github.com/xuxueli/xxl-job/issues/2415
我帮助了这个题主找到了慢sql的原因,并提供了解决方案。题主项目调度log非200即失败日志太多,就会产生慢sql(应该是告警方法的sql查询)。
难过的是——费力帮忙解决,连声道谢都没有,拜托,我们也是时薪近百的人,帮你解决没给钱就算了,说声谢谢是应该的吧,最后更是没有把引起问题的原因发出来供大家开源参考。鄙视一下认为理所应当的伸手党行为,这也是很多开源项目作者寒心的原因,人家没收你一分钱,你只会责骂项目有问题,却没有帮助作者让开源项目做得更好,所以即使xxl-job存在很多问题,我是不敢说一声不好,只能提出问题,我自己有解决的发一份供大家参考。
保证job客户端优雅停机时,执行器可以及时剔除服务,防止无效调度。
我处理这个问题的心态是能解决就好,坐等官方方案(就目前来看,短期内不太可能解决),花太多时间的话宁可选型其它框架,哈哈,也是属于白嫖党,不愿花太多精力。
找到
com.xxl.job.admin.core.thread.JobRegistryHelper
拉到最下面,有这么一个方法
private void freshGroupRegistryInfo(RegistryParam registryParam){
// Under consideration, prevent affecting core tables
}
翻译:正在考虑,防止影响核心表
说明作者也是有在考虑这个问题,但是还没实现预留了一个方法。
我给大家一个临时解决方案。
很简单,将61行处// for monitor ==》while (!toStop) { 里的部分内容复制过来即可。
即——
private void freshGroupRegistryInfo(RegistryParam registryParam){
// Under consideration, prevent affecting core tables
try {
// auto registry group
List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
if (groupList!=null && !groupList.isEmpty()) {
// remove dead address (admin/executor)
List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
if (ids!=null && ids.size()>0) {
XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids);
}
// fresh online address (admin/executor)
HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
if (list != null) {
for (XxlJobRegistry item: list) {
if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
String appname = item.getRegistryKey();
List<String> registryList = appAddressMap.get(appname);
if (registryList == null) {
registryList = new ArrayList<String>();
}
if (!registryList.contains(item.getRegistryValue())) {
registryList.add(item.getRegistryValue());
}
appAddressMap.put(appname, registryList);
}
}
}
// fresh group address
for (XxlJobGroup group: groupList) {
List<String> registryList = appAddressMap.get(group.getAppname());
String addressListStr = null;
if (registryList!=null && !registryList.isEmpty()) {
Collections.sort(registryList);
StringBuilder addressListSB = new StringBuilder();
for (String item:registryList) {
addressListSB.append(item).append(",");
}
addressListStr = addressListSB.toString();
addressListStr = addressListStr.substring(0, addressListStr.length()-1);
}
group.setAddressList(addressListStr);
group.setUpdateTime(new Date());
XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
}
}
} catch (Exception e) {
logger.error(">>>>>>>>>>> xxl-job, job registry remove thread error:{}", e);
}
}
如果你是暴力关闭服务tomcat类似kill -9 类型,可以选择手动调用下线接口。
在关闭tomcat前调用该接口,过个几秒后再杀死服务的tomcat。
curl.exe -H "Content-Type:application/json"
-H "XXL-JOB-ACCESS-TOKEN:你的鉴权token"
-X POST --data
"{\"registryGroup\": \"EXECUTOR\",\"registryKey\": \"xxl-job-executor-zzdserver\",\"registryValue\": \"http://执行器ip:port/\"}"
http://服务端ip:port/api/registryRemove
上面的做法当然是不推荐的,服务最好是优雅停机,我是选型actuator来优雅停机的(actuator内容这里不介绍,后续会写一篇jenkins的文章集成actuator),在优雅停机的时候,spring自然会调用执行器的销毁方法,销毁方法内包含上面那个接口的调用,内容也会更多,类似停止新调度,执行完旧调度最后关闭,不过作者写的现有内容好像有问题,好像是先杀再停止新调度,我在引入xxl-job的销毁后,shutdown接口无法很快关闭,基本上都是等到超时。
bat内容如下,sh其实类似,可供参考
rem ****** 重启前准备 ********
@echo off
set "FILE_HOME=C:\Program Files\JenkinsFileA"
rem 进入d盘
d:
D:\...\curl.exe -X POST http://服务ip:8085/actuator/shutdown
rem 延迟20s
ping -n 20 1271
for /F "tokens=5" %%i in ( 'netstat -ano ^| findstr :8080' ) do taskkill /f /pid %%i /t
xcopy /e /y "%FILE_HOME%\config\*" "D:\...\A\config\"
rem 复制jar到运行目录
copy "%FILE_HOME%\***.jar" "D:\...\A\***.jar"
schtasks /Run /TN "deploy-restart-a"
echo good bye
@echo on
这样重启我们自己的服务的时候,xxl-job就基本不会500报错,只是偶尔的报kill job,这也是漏洞,由于出现概率较低可以接受,所以我没有去解决。如果你有更好的方案,欢迎提供。