记录一次xxl-job遇到的非常奇怪的bug,排查了两天,才找到原因,抓狂啊~~~
一.问题还原
本地在项目中引用xxl-job,本地启动xxl-job-admin, 项目启动后,能成功注册到xxl-admin上,但是在xxl-admin上触发任务,一直失败,xxl-admin只报错超时 time out, 引入的项目任务没有被触发,也不报错,如下如
项目启动成功
项目成功注册到xxl-admin上
调用任务,一直失败,只是xxl-admin报错超时,但是项目不报错,没有任务信息打印
二.原因排查
1.是否使xxl-job自身的问题
我本地启动类xxl-job的测试项目,启动后,能从xxl-admin上触发任务,所以排除 xxl-job自身的问题
2.项目中端口号是否有问题,防火墙是否开启
我继续替换了几个端口号,发现还是同样的问题,并且本地的电脑已经关闭防火墙,不存在端口被防火墙拦截的可能
3.继续测试端口问题
本质上xxl-job启动netty后,作为一个服务,等待xxl-admin的调用,其实就是等待别人发送http请求, 本人直接使用postman发送请求
而使用telnet 127.0.0.1 8888 后,按下回车后,netty中的hanler却能触发, 说明这个netty服务是启动成功的,而且是可以接受客户端连接的,但是项目启动后确一直失败
4.总结
通过上面的问题排查,说明netty服务是启动成功的,而且是可以接受客户端连接的,但是确无法接受http的请求,思路明确了,就只能进入源码查找问题了
三.源码排查
本人前端时间看过netty的源码和xxl-job的源码,所以对这块还是比较熟悉的,不太懂的朋友可以看下我之前写的文章
接着找到xxl-job中启动netty的核心代码
thread = new Thread(new Runnable() {
@Override
public void run() {
// param
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor(
0,
200,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(2000),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "xxl-job, EmbedServer bizThreadPool-" + r.hashCode());
}
},
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
}
});
try {
// start server
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
channel.pipeline()
.addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // beat 3N, close if idle
.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // merge request & reponse to FULL
.addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
}
})
.childOption(ChannelOption.SO_KEEPALIVE, true);
// bind
ChannelFuture future = bootstrap.bind(port).sync();
logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);
// start registry
startRegistry(appname, address);
// wait util stop
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
logger.info(">>>>>>>>>>> xxl-job remoting server stop.");
} catch (Exception e) {
logger.error(">>>>>>>>>>> xxl-job remoting server error.", e);
} finally {
// stop
try {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
thread.setDaemon(true); // daemon, service jvm, user thread leave >>> daemon leave >>> jvm leave
thread.start();
}
上面最重要的是往channle的pipline中添加四个handler
channel.pipeline()
// 心跳
.addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // beat 3N, close if idle
// http解码get请求
.addLast(new HttpServerCodec())
// http解码post请求
.addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // merge request & reponse to FULL
// 自定义处理类
.addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
经过debug后,发现channle的handler中只有三个处理器(去除默认的head和tail节点后,没有发现HttpObjectAggregator)
备注> 这一块其实还没有那么容易好找到原因, 因为我之前重点放在EmbedHttpServerHandler的read0()方法,
EmbedHttpServerHandler的channelRead方法,会调用这里
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
//正常的应该走这里
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
//但是项目中一直走这里,说明这里是有问题的
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
通过前面的if (acceptInboundMessage(msg))这个方法,正常的这个msg的类型应该是
HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 0, cap: 0, components=0))
但是通过debug后,发现msg的类型一直是DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
所有我通过判断类型不一致,从而进一步判断可能是前面的编码的handler有问题
而进一步debug后,发现项目中的pipline缺少HttpObjectAggregator
通过打断点,看到这一行前面不是红色实心而是一个灰色圆圈,说明确实这一行代码没有执行,
然后我找到本地的maven仓库中的这个xxl-job-core的依赖源码
channel.pipeline()
.addLast(new ChannelHandler[]{new IdleStateHandler(0L, 0L, 90L, TimeUnit.SECONDS)})
.addLast(new ChannelHandler[]{new HttpServerCodec()})
// 发现确实没有添加`HttpObjectAggregator这个handler, 草草草!!!!!!!
.addLast(new ChannelHandler[]{new EmbedHttpServerHandler(EmbedServer.this.executorBiz, accessToken, bizThreadPool)});
草草草!!!啊,不知道本地引入的jar包中,为什么这一行缺失了,从而导致无法将msg的类型从DefaultHttpRequest转换为HttpObjectAggregator$AggregatedFullHttpRequest对象,从而导致netty自定义的handler无法走channelRead0(ctx, imsg)方法
到这里,这个问题的原因算是排查清除了, 哎,我前期一直以为是版本问题,自己替换netty版本,xxl-job
版本,springboot的版本,搞了一天,还是没有查到问题,因为项目也不报错,也一直没有往netty这块去想,浪费了很多的时间,结果原因就是本地仓库中的xxl-job-core代码中缺失这一行代码......哎
我要疯了
四.netty的# HttpObjectAggregator简要介绍
netty处理http请求,一般都会添加
// 解码成HttpRequest
pipeline.addLast(new HttpServerCodec());
// 解码成FullHttpRequest
pipeline.addLast(new HttpObjectAggregator(1024*10));
简单来说,如果仅把byte解码成HttpRequest的话,解析请求时会丢失一些信息(这个说法很不具体)。
所以我百度了一波,找到了讲得比较清楚的解释:
当我们用POST方式请求服务器的时候,对应的参数信息是保存在message body中的,如果只是单纯的用HttpServerCodec是无法完全的解析Http POST请求的,因为HttpServerCodec只能获取uri中参数,所以需要加上HttpObjectAggregator。
Http的Get,POST
Get请求包括两个部分:
- request line(包括method,request uri,protocol version))
- header
基本样式:
GET /?name=XXG&age=23 HTTP/1.1 -----> request line
------------------------------------------------------------------
Host: 127.0.0.1:8007
Connection: keep-alive
Cache-Control: max-age=0 -----> header
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
POST请求包括三个部分
- request line(包括method,request uri,protocol version))
- header
- message body
基本样式:
GET / HTTP/1.1 -----> request line
------------------------------------------------------------------
Host: 127.0.0.1:8007
Connection: keep-alive
Content-Length: 15
Cache-Control: max-age=0 -----> header
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
------------------------------------------------------------------
name=XXG&age=23 ------>message body
HttpObjectAggregator
从上可以看出,当我们用POST方式请求服务器的时候,对应的参数信息是保存在message body中的,如果只是单纯的用HttpServerCodec是无法完全的解析Http POST请求的,因为HttpServerCodec只能获取uri中参数,所以需要加上HttpObjectAggregator