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模式、线程池的状态及生命周期等