记录一次线上netty问题排查的过程

2,276 阅读5分钟

记录一次xxl-job遇到的非常奇怪的bug,排查了两天,才找到原因,抓狂啊~~~

src=http___n.sinaimg.cn_sinacn_w533h300_20180208_4331-fyrkuxs0828542.jpg&refer=http___n.sinaimg.webp

一.问题还原

本地在项目中引用xxl-job,本地启动xxl-job-admin, 项目启动后,能成功注册到xxl-admin上,但是在xxl-admin上触发任务,一直失败,xxl-admin只报错超时 time out, 引入的项目任务没有被触发,也不报错,如下如

image.png

项目启动成功

image.png

项目成功注册到xxl-admin上

OTTAYM(K6NWUDM0@@0%QR3R.jpg

调用任务,一直失败,只是xxl-admin报错超时,但是项目不报错,没有任务信息打印

二.原因排查

1.是否使xxl-job自身的问题

image.png 我本地启动类xxl-job的测试项目,启动后,能从xxl-admin上触发任务,所以排除 xxl-job自身的问题

2.项目中端口号是否有问题,防火墙是否开启

我继续替换了几个端口号,发现还是同样的问题,并且本地的电脑已经关闭防火墙,不存在端口被防火墙拦截的可能

3.继续测试端口问题

本质上xxl-job启动netty后,作为一个服务,等待xxl-admin的调用,其实就是等待别人发送http请求, 本人直接使用postman发送请求

image.png

而使用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代码中缺失这一行代码......哎
我要疯了

src=http___n.sinaimg.cn_sinacn_w533h300_20180208_4331-fyrkuxs0828542.jpg&refer=http___n.sinaimg.webp

四.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