Jetty无法提供服务,大量连接处于的close_wait状态

相关背景及收获

背景:(2019年10月,河北xx银行)使用了Jetty作为分布式定时任务的通信框架,由于线程池配置的不合理,造成项目启动后无法提供服务,大量连接处于的close_wait状态(由于开发过程中CPU大多是4核,对于极少机器偶发的这个问题没有在意,预生产环境服务器CPU核心数较多,复现了该问题)

收获:从解决这个问题的过程探究了Jetty的源码、理解了Jetty的原理,掌握线程池的一些配置及使用中应该避免的“坑”(问题早已解决,最近梳理一下,属予作文以记之)

所用Jetty相关版本

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-server</artifactId>
    <version>8.1.15.v20140411</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-util</artifactId>
    <version>8.1.15.v20140411</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-http</artifactId>
    <version>8.1.15.v20140411</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-io</artifactId>
    <version>8.1.15.v20140411</version>
</dependency>

示例代码复现问题

Jetty服务端示例代码

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;

public class JettyDemo {
    public static void main( String[] args ) throws Exception {
        // The Server
        Server server = new Server();
        
        // ExecutorThreadPool 是Jetty提供的一个线程池,底层还是使用java的ThreadPoolExecutor
        // 问题出现的位置就是 new ExecutorThreadPool(corePoolSize, maximumPoolSize, keepAliveTime) keepAliveTime为毫秒值
        // 当 CPU 核心数为4,corePoolSize为小于等于2会产生问题,大于2则不会出现问题
        // 当 CPU 核心数为8,corePoolSize为小于等于5会产生问题,大于5则不会出现问题
        server.setThreadPool(new ExecutorThreadPool(3, 100, 30000));

        // HTTP connector
        SelectChannelConnector connector = new SelectChannelConnector();// 非阻塞
        String ip = "127.0.0.1";
        int port = 8001;
        if (ip != null && ip.trim().length() > 0) {
            // The network interface this connector binds to as an IP
            // address or a hostname. If null or 0.0.0.0, then bind to
            // all interfaces.
            connector.setHost(ip);
        }
        connector.setPort(port);
        connector.setMaxIdleTime(30000);
        // connector.setServer(server);
        server.setConnectors(new Connector[] { connector });

        // Set a handler
        HandlerCollection handlerc = new HandlerCollection();
        handlerc.setHandlers(new HelloHandler[]{new HelloHandler()});
        server.setHandler(handlerc);
        try {
            // 服务器启动
            server.start();
            server.join(); // 阻塞到线程中止
        } catch (Exception e) {
        } finally {
        }
    }
}

HelloHandler源码

import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloHandler extends AbstractHandler
{
    final String greeting;
    String body;

    public HelloHandler()
    {
        this("Hello World");
    }

    public HelloHandler( String greeting )
    {
        this(greeting, null);
    }

    public HelloHandler( String greeting, String body )
    {
        this.greeting = greeting;
        this.body = body;
    }

    public void handle( String target,
                        Request baseRequest,
                        HttpServletRequest request,
                        HttpServletResponse response ) throws IOException,
            ServletException
    {
        response.setContentType("text/html; charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);

        PrintWriter out = response.getWriter();
        body = target;
        out.println("<h1>" + greeting + "</h1>");
        if (body != null) {
            out.println(body);
        }

        baseRequest.setHandled(true);
    }
}
  • 访问方式:直接在浏览器地址栏访问 http://127.0.0.1:8001/ 即可

复现的问题截图

每增加一个对 http://127.0.0.1:8001/ 的访问,就会增加一个完整(双向)的TCP连接,此处直接使用 windows 的 cmd 命令行窗口查看连接状态,有兴趣的可使用 wireshark 可视化工具进行查看和分析

  • 初始状态(还未通过浏览器访问8001)
  • 连接第一次建立时的状况(通过浏览器地址栏首次访问,发现一直处于阻塞状态而无法返回结果)
  • 连接第二次建立时的状况(新开一个窗口,通过浏览器地址栏再次访问,发现还是一直处于阻塞状态而无法返回结果)
  • 连接超时或者连接关闭时的状况(将这两个访问 8001 的窗口关闭)
  • 结论

根据以上截图,当增加对其端口的访问时,所有的新连接都会被阻塞,当连接关闭时,服务端就会处于 CLOSE_WAIT 的状态,客户端就会处于 FIN_WAIT_2 的状态。每多一个关闭就会增加一个这样的状态(有兴趣的可以研究一下 TCP 连接的三次握手和四次挥手)

问题定位

造成该问题的代码处(Jetty服务端)

server.setThreadPool(new ExecutorThreadPool(3, 100, 30000));

Jetty线程模型是由acceptor、selector、worker三部分组成,如果通过server.setThreadPool(executorThreadPool)的方式,则三个部分共用这个线程池,而每一个部分的线程数Jetty都有自己的计算规则(具体的计算规则会在 Jetty线程模型分析 专栏中讲)。当核心线程数设置过小时,就会造成worker分配不到线程,selector将请求分发给worker时,没有worker线程处理请求,造成selector线程一直处于阻塞状态。

ExecutorThreadPool源码

public ExecutorThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS);
}

public ExecutorThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, new LinkedBlockingQueue<Runnable>());
}

public ExecutorThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    this(new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue));
}

通过源码我们可以看出,Jetty定义的ExecutorThreadPool底层使用的仍是jdk提供的ThreadPoolExecutor。这里可能有人会有疑问:明明设置了最大线程数100,为什么线程池中不会自动增加线程?这里是因为线程池配置中LinkedBlockingQueue造成的,当创建LinkedBlockingQueue使用的是无参构造,就会导致maximumPoolSize参数(即最大线程数设置100)会失效,线程最大数等于核心线程数(这里为了避免此类问题,可以使用有参构造,如new LinkedBlockingQueue(1000);)

深入理解ThreadPoolExecutor中有这么一段
无界队列(不设定size的LinkedBlockingQueue):当线程数达到corePoolSize的仍有task进来时,会源源不断进队列,由于无界,maximumPoolSize参数会失效,线程数最大只能达到corPoolSize

总结

  • Jetty在使用时需要注意线程池的配置:需要探究Jetty的线程模型及JDK线程池的原理及使用规范
  • Jetty8和Jetty9在使用时有区别:Jetty不同版本在使用时有所不同,需要加以注意及查看源码以探究竟
  • 请求被阻塞需要关注:可以通过命令行窗口或更加友好的可视化抓包工具(如wireshark、charles等)进行分析,通过分析更好的理解TCP连接的三次握手和四次挥手,并且需要理解连接过程中的各个状态
  • JDK线程池各个参数的配置:需要掌握线程池参数中各种阻塞队列的意义及使用、各种拒绝策略的意义及使用
  • 扩展:使用ThreadPoolExecutor时,execute()和submit()用来执行任务有何区别、两种方式对于异常的处理(是否会吃掉异常)、Future模式、线程池的状态及生命周期等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰式的美式

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值