xxl-job滥用netty导致的问题和解决方案
netty作为一种高性能的网络编程框架,在很多开源项目中大放异彩,十分亮眼,但是在有些项目中却被滥用,导致使用者使用起来非常的难受。
本篇文章将会讲解xxl-job作为一款分布式任务调度系统是如何滥用netty的,导致了怎样的后果以及如何修改源码解决这些问题。
笔者使用的是2.3.0版本的xxl-job,也是当前的最新版本;下面所有的代码修改全部基于2.3.0版本的xxl-job源代码
https://github.com/xuxueli/xxl-job/tree/2.3.0
其中,xxl-job-admin对应着项目:https://github.com/xuxueli/xxl-job/tree/2.3.0/xxl-job-admin
spring-boot项目对应着示例项目:https://github.com/xuxueli/xxl-job/tree/master/xxl-job-executor-samples/xxl-job-executor-sample-springboot
一、xxl-job存在的多端口问题
关于xxl-job如何使用的问题,可以参考我的另外一篇文章:分布式任务调度系统:xxl-job
现在java开发基本上已经离不开spring boot了吧,我在spring boot中集成了xxl-job-core组件并且已经能够正常使用,但是一旦部署到测试环境就不行了,这是因为测试环境使用了docker,spring boot集成xxl-job-core组件之后会额外开启9999端口号给xxl-job-admin调用使用,如果docker不开启宿主机到docker的端口映射,xxl-job-admin自然就会调用失败。这导致了以下问题:
- 每个spring boot程序都要开两个端口号,意味着同时运行着两个服务进行端口监听,浪费计算和内存资源
- 如果使用docker部署,需要再额外做宿主机和容器的9999端口号的映射,否则外部的xxl-job-admin将无法访问。
那如果两个不同的服务都集成了xxl-job,但是部署在同一台机器上,又会发生什么呢?答案是如果不指定特定端口号,两个服务肯定都要使用9999端口号,势必会端口冲突,但是xxl-job已经想到了9999端口号被占用的情况,如果9999端口号被占用,则会端口号加一再重试。
xxl-job-core组件额外开启9999端口号到底合不合理?
举个例子:spring boot程序集成swagger-ui是很常见的操作吧,也没见swagger-ui再额外开启端口号啊,我认为是不合理的。但是,我认为作者这样做也有他的考虑---并非所有程序都是spring-boot的程序,也有使用其它框架的程序,使用独立的netty server作为客户端能够保证在使用java的任意xxl-job客户端都能稳定的向xxl-job-admin提供服务。然而java开发者们绝大多数情况下都是使用spirng-boot构建程序,在这种情况下,作者偷懒没有构建专门在spirng boot框架下使用的xxl-job-core,而是想了个类似万金油的蠢招解决问题,让所有在spring-boot框架下的开发者都一起难受,实在是令人费解。
二、源码追踪
一切的起点要从spring-boot程序集成xxl-job-core说起,集成方式很简单,只需要成功创建一个XxlJobSpringExecutor
Bean对象即可。
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
在XxlJobSpringExecutor
对象创建完成之后会做一些xxl-job初始化的操作,包含连接xxl-job-admin以及启动netty server。
展开XxlJobSpringExecutor
源码,可以看到它实现了SmartInitializingSingleton
接口,这就意味着Bean对象创建完成之后会回调afterSingletonsInstantiated
接口
// start
@Override
public void afterSingletonsInstantiated() {
// init JobHandler Repository
/*initJobHandlerRepository(applicationContext);*/
// init JobHandler Repository (for method)
initJobHandlerMethodRepository(applicationContext);
// refresh GlueFactory
GlueFactory.refreshInstance(1);
// super start
try {
super.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
在super.start();
这行代码中,会调用父类XxlJobExecutor
的start方法做初始化
public void start() throws Exception {
// init logpath
XxlJobFileAppender.initLogPath(logPath);
// init invoker, admin-client
initAdminBizList(adminAddresses, accessToken);
// init JobLogFileCleanThread
JobLogFileCleanThread.getInstance().start(logRetentionDays);
// init TriggerCallbackThread
TriggerCallbackThread.getInstance().start();
// init executor-server
initEmbedServer(address, ip, port, appname, accessToken);
}
在initEmbedServer(address, ip, port, appname, accessToken);
这行代码做开启netty-server的操作
private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {
// fill ip port
port = port>0?port: NetUtil.findAvailablePort(9999);
ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
// generate address
if (address==null || address.trim().length()==0) {
String ip_port_address = IpUtil.getIpPort(ip, port); // registry-address:default use address to registry , otherwise use ip:port if address is null
address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
}
// accessToken
if (accessToken==null || accessToken.trim().length()==0) {
logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
}
// start
embedServer = new EmbedServer();
embedServer.start(address, port, appname, accessToken);
}
可以看到这里会创建EmbedServer对象,并且使用start方法开启netty-server,在这里就能看到熟悉的一大坨了
除了开启读写空闲检测之外,就只做了一件事:开启http服务,也就是说,xxl-job-admin是通过http请求调用客户端的接口触发客户端的任务调度的。最终处理方法在EmbedHttpServerHandler
类中,顺着EmbedHttpServerHandler
类的方法找,可以最终找到处理的方法com.xxl.job.core.server.EmbedServer.EmbedHttpServerHandler#process
private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
// valid
if (HttpMethod.POST != httpMethod) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
}
if (uri==null || uri.trim().length()==0) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
}
if (accessToken!=null
&& accessToken.trim().length()>0
&& !accessToken.equals(accessTokenReq)) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
}
// services mapping
try {
if ("/beat".equals(uri)) {
return executorBiz.beat();
} else if ("/idleBeat".equals(uri)) {
IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
return executorBiz.idleBeat(idleBeatParam);
} else if ("/run".equals(uri)) {
TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
return executorBiz.run(triggerParam);
} else if ("/kill".equals(uri)) {
KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
return executorBiz.kill(killParam);
} else if ("/log".equals(uri)) {
LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
return executorBiz.log(logParam);
} else {
return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
}
}
从这段代码的逻辑可以看到
- 只接受POST请求
- 如果有token,则会校验token
- 只提供/beat、/idelBeat、/run、/kill、/log 五个接口,所有请求的处理都会委托给executorBiz处理。
最后,netty将executorBiz处理结果写回xxl-job-admin,然后请求就结束了。这里netty扮演的角色非常简单,我认为可以使用spring-mvc非常容易的替换掉它的功能。
三、使用spring-mvc替换netty的功能
1.新增spring-mvc代码
这里要修改xxl-job-core的源代码,首先,加入spring-mvc的依赖
<!-- spring-web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
<scope>provided</scope>
</dependency>
然后新增Controller文件
package com.xxl.job.core.controller;
import com.xxl.job.core.biz.impl.ExecutorBizImpl;
import com.xxl.job.core.biz.model.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author kdyzm
* @date 2021/5/7
*/
@RestController
public class XxlJobController {
@PostMapping("/beat")
public ReturnT<String> beat() {
return new ExecutorBizImpl().beat();
}
@PostMapping("/idleBeat")
public ReturnT<String> idleBeat(@RequestBody IdleBeatParam param) {
return new ExecutorBizImpl().idleBeat(param);
}
@PostMapping("/run")
public ReturnT<String> run(@RequestBody TriggerParam param) {
return new ExecutorBizImpl().run(param);
}
@PostMapping("/kill")
public ReturnT<String> kill(@RequestBody KillParam param) {
return new ExecutorBizImpl().kill(param);
}
@PostMapping("/log")
public ReturnT<LogResult> log(@RequestBody LogParam param) {
return new ExecutorBizImpl().log(param);
}
}
2.删除老代码&移除netty依赖
之后,就要删除老的代码了,修改com.xxl.job.core.server.EmbedServer#start
方法,清空所有代码,新增
// start registry
startRegistry(appname, address);
然后删除EmbedServer
类中的以下两个变量及相关的引用
private ExecutorBiz executorBiz;
private Thread thread;
之后删除netty的依赖
<!-- ********************** embed server: netty + gson ********************** -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty-all.version}</version>
</dependency>
将报错的代码全部删除,之后就可以编译成功了,当然这还不行。
3.修改注册到xxl-job-admin的端口号
注册的ip地址可以不用改,但是端口号要取spring-boot程序的端口号。
因为要复用springk-boot容器的端口号,所以这里注册的端口号要和它保持一致,修改com.xxl.job.core.executor.XxlJobExecutor#initEmbedServer
方法,注释掉
port = port > 0 ? port : NetUtil.findAvailablePort(9999);
然后修改spring-boot的配置文件,xxl-job的端口号配置改成server.port
server.port=8081
xxl.job.executor.port=${server.port}
在创建XxlJobSpringExecutor
Bean对象的时候将改值传递给它。
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
4.将xxl-job-core改造成spring-boot-starter
上面改造完了之后已经将逻辑变更为使用spring-mvc,但是spring-boot程序还没有办法扫描到xxl-job-core中的controller,可以手动扫描包,这里推荐使用spring-boot-starter,这样只需要将xxl-job-core加入classpath,就可以自动生效。
在 com.xxl.job.core.config包下新建Config类
package com.xxl.job.core.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author kdyzm
* @date 2021/5/7
*/
@Configuration
@ComponentScan(basePackages = {"com.xxl.job.core.controller"})
public class Config {
}
在src/main/resources/META-INF
文件夹下新建spring.factories
文件,文件内容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxl.job.core.config.Config
这样就改造完了。
四、测试
重启xxl-job-executor-sample-springboot项目,查看注册到xxl-job-admin上的信息
可以看到端口号已经不是默认的9999,而是和spring-boot程序保持一致的端口号,然后执行默认的job
可以看到已经执行成功,在查看日志详情
日志也一切正常,表示一切都改造成功了。
完整的代码修改:https://github.com/kdyzm/xxl-job/commit/449ee5c7bbb659356af25b164c251f960b9a6891
xxl-job滥用netty导致的问题和解决方案的更多相关文章
- table表格某一td内容太多导致样式混乱的解决方案
对于有很多条目的数据,通常采用table元素来快速实现,某一个td的内容太多的话就会导致样式混乱难看. 解决方案 要让table的宽度固定可以给table元素设置table-layout:fixed; ...
- MySQL中char(36)被认为是GUID导致的BUG及解决方案
MySQL中char(36)被认为是GUID导致的BUG及解决方案 有时候在使用Toad或在程序中,偶尔会遇到如下的错误: System.FormatException GUID 应包含带 4 个短划 ...
- libcrypto.so.1.0.0内容丢失导致sshd无法运行解决方案
libcrypto.so.10丢失导致sshd无法运行解决方案 服务器由于掉电开启系统后无法远程ssh,重启sshd服务弹如下图的错误: /usr/sbin/sshd:error while load ...
- mongodb因非法关闭导致无法启动的解决方案
mongodb因非法关闭导致无法启动的解决方案 1.删除数据库目录的.lock文件 2.输入命令 mongod --repair 3.重启
- vscode在执行 npm任务的时候,会先执行package的name@version 然后命令名 加 当前路径,问题是我的引入路径e是小写的,会导致调试错误,解决方案:没找到,先手书吧
vscode在执行 npm任务的时候,会先执行package的name@version 然后命令名 加 当前路径,问题是我的引入路径e是小写的,会导致调试错误,解决方案:没找到 Executing t ...
- Node出错导致运行崩溃的解决方案
许多人都有这样一种映像,NodeJS比较快: 但是因为其是单线程,所以它不稳定,有点不安全,不适合处理复杂业务: 它比较适合对并发要求比较高,而且简单的业务场景. 在Express的作者的TJ Hol ...
- 客服端调用自定义宿主的WCF报错"没有终结点在侦听可以接受消息的http://localhost:8085/mex。这通常是由于不正确的地址或者 SOAP 操作导致的错误"的解决方案。
没有终结点在侦听可以接受消息的http://localhost:8085/mex.这通常是由于不正确的地址或者 SOAP 操作导致的错误. 这个错误是由于没有启动元数据交换终结点(MEX)导致的.在宿 ...
- 单元测试报connection is allready closed导致dailybuild中断的解决方案——类加载机制的应用
现象; 前段时间在dailybuild过程中,经常遇到connection is allready closed错误,特别是在dailybuild高峰期. 分析定位: 这个错误是的起因比较多,这里的情 ...
- linux 关于Apache默认编码错误 导致网站乱码的解决方案
Apache默认编码UTF-8在解析A网站的时候没有任何问题,当运行B网站时出现的"蝌蚪文"乱码问题 最近经常有同学在使用LAMP/WAMP时,遇到这样的编码错误问题: A网站 ...
随机推荐
- InterJ idea 回滚提交的代码
如果你要回滚到这一次提交 ctrl shift k 提交 选force push 那么你的代码就回滚到你所想要的这次提交记录了
- 如何在 Istio 中支持 Dubbo、Thrift、Redis 以及任何七层协议?
赵化冰,腾讯云高级工程师,Istio Member,ServiceMesher管理委员,Istio 项目贡献者, Aerika 项目创建者 ,热衷于开源.网络和云计算.目前主要从事服务网格的开源和研发 ...
- AtCoder Beginner Contest 186
A Brick int n, m; int main() { scanf("%d%d", &n, &m); printf("%d\n", n / ...
- FreeBSD 乃至开源界中的孔乙己 再论苦难哲学之一
在许多狂热的FreeBSD 粉丝里,他们甚至不允许别人把FreeBSD写作freebsd,要和你强调,F和BSD都是大写的.还说这是什么尊重之类的东西.大抵和孔乙己的茴香豆的茴的有四种写法一样吧:&q ...
- 这个Bug的排查之路,真的太有趣了。
这是why哥的第 92 篇原创文章 在<深入理解Java虚拟机>一书中有这样一段代码: public class VolatileTest { public static volat ...
- linux软件deb打包及开机管理员自启动
环境:Ubuntu 18.04/16.04 Qt:5.12.6 一 deb打包 1.建立目录结构 2.目录内容 1) 子目录DC520: Get以上内容步骤: (1) 创建目录DC520(自己软 ...
- 练习使用Unicorn、Capstone
Unicorn是一个轻量级的多平台,多体系结构的CPU仿真器框架.官网:http://www.unicorn-engine.org/ Capstone是一个轻量级的多平台,多体系结构的反汇编框架.官网 ...
- 【java框架】SpringBoot(4)--SpringBoot实现异步、邮件、定时任务
1.SpringBoot整合任务机制 1.1.SpringBoot实现异步方法 日常开发中涉及很多界面与后端的交互响应,都不是同步的,基于SpringBoot为我们提供了注解方式实现异步方法.使得前端 ...
- DAOS 分布式异步对象存储|架构设计
分布式异步对象存储 (DAOS) 是一个开源的对象存储系统,专为大规模分布式非易失性内存 (NVM, Non-Volatile Memory) 设计,利用了SCM(Storage-Class Memo ...
- .Net Core 3.1浏览器后端服务(五) 引入定时任务Quartz.Net
一.前言 近期项目中遇到一些需求,需要定时写入数据库,定时刷新缓存的问题,因此需要引入任务调度机制. 我的选择是使用 Quartz.Net,使用的版本是 3.2.4 这里强调一点:3.x的版本与2.x ...