(以下部分内容来自:李林峰的文章 https://mp.weixin.qq.com/s?__biz=MzAwMDIyNTAzMw==&mid=2651309914&idx=1&sn=91a71a3e7b25294ae55fa8628e56996e&chksm=811f5d3cb668d42a2057f345aa6bb735f65272380405f95ca58f8d6da6b3e7728e989a9e1d46&mpshare=1&scene=23&srcid=0104pwCR8l8PsUJf4w8qwjpa#rd)
很多同学对通信框架的异步和 RPC 调用的异步理解有误,比较典型的错误理解包括:
1.我使用的是 Tomcat8,因为 Tomcat8 支持 NIO,所以我基于 Tomcat 开发的 HTTP 调用都是异步的。
2.因为我们的 RPC 框架底层使用的是 Netty、Vert.X 等异步框架,所以我们的 RPC 调用天生就是异步的。
3.因为我们底层的通信框架不支持异步,所以 RPC 调用也无法异步化。
1.1.1. Tomcat 的 BIO 和 NIO
在 Tomcat6.X 版本对 NIO 提供比较完善的支持之前,作为 Web 服务器,Tomcat 以 BIO 的方式接收并处理客户端的 HTTP 请求,当并发访问量比较大时,就容易发生拥塞等性能问题,它的工作原理示意如下所示:
图 1 采用 BIO 做 HTTP 服务器的 Web 容器
传统同步阻塞通信(BIO)面临的主要问题如下:
1.性能问题:一连接一线程模型导致服务端的并发接入数和系统吞吐量受到极大限制。
2.可靠性问题:由于 I/O 操作采用同步阻塞模式,当网络拥塞或者通信对端处理缓慢会导致 I/O 线程被挂住,阻塞时间无法预测。
3.可维护性问题:I/O 线程数无法有效控制、资源无法有效共享(多线程并发问题),系统可维护性差。
从上图我们可以看出,每当有一个新的客户端接入,服务端就需要创建一个新的线程(或者重用线程池中的可用线程),每个客户端链路对应一个线程。当客户端处理缓慢或者网络有拥塞时,服务端的链路线程就会被同步阻塞,也就是说所有的 I/O 操作都可能被挂住,这会导致线程利用率非常低,同时随着客户端接入数的不断增加,服务端的 I/O 线程不断膨胀,直到无法创建新的线程。
同步阻塞 I/O 导致的问题无法在业务层规避,必须改变 I/O 模型,才能从根本上解决这个问题。
Tomcat 6.X 提供了对 NIO 的支持,通过指定 Connector 的 protocol=“org.apache.coyote.http11.Http11NioProtocol”,就可以开启 NIO 模式,采用 NIO 之后,利用 Selector 的轮询以及 I/O 操作的非阻塞特性,可以实现使用更少的 I/O 线程处理更多的客户端连接,提升吞吐量和主机的资源利用率。Tomcat 8.X 之后提供了对 NIO2.0 的支持,默认也开启了 NIO 通信模式。
1.1.2. Tomcat NIO 与 Servlet 异步
事实上,Tomcat 支持 NIO,与 Tomcat 的 HTTP 服务是否是异步的,没有必然关系,这个可以从两个层面理解:
1.HTTP 消息的读写:即便采用了 NIO,HTTP 请求和响应的消息处理仍然可能是同步阻塞的,这与协议栈的具体策略有关系。从 Tomcat 官方文档可以看到,Tomcat 6.X 版本即便采用 Http11NioProtocol,HTTP 请求消息和响应消息的读写仍然是 Blocking 的。
2.HTTP 请求和响应的生命周期管理:本质上就是 Servlet 是否支持异步,如果 Servlet 是 3.X 之前的版本,则 HTTP 协议的处理仍然是同步的,这就意味着 Tomcat 的 Connector 线程需要同时处理 HTTP 请求消息、执行 Servlet Filter、以及业务逻辑,然后将业务构造的 HTTP 响应消息发送给客户端,整个 HTTP 消息的生命周期都采用了同步处理方式。
Tomcat 与 Servlet 的版本配套关系如下所示:
Servlet**** 规范版本 | Tomcat**** 版本 | JDK**** 版本 |
---|---|---|
4.0 | 9.0.X | 8+ |
3.1 | 8.0.X | 7+ |
3.0 | 7.0.X | 6+ |
2.5 | 6.0.X | 5+ |
2.4 | 5.5.X | 1.4+ |
2.3 | 4.1.X | 1.3+ |
表 1 Tomcat 与 Servlet 的版本配套关系
pom.xml依赖:
javax.servlet
javax.servlet-api
3.1.0
provided
下面创建了三个servlet(Servlet1、Servlet2、Servlet3),然后在业务中sleep(200)来模拟耗时的操作。Servlet1异步,Servlet2同步,Servlet3直接返回结果:
Servlet1.java
package com.zhuyun.tomcat;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns="/AsyncLongRunningServlet",asyncSupported=true)
public class Servlet1 extends HttpServlet {
private static final long serialVersionUID = 1L;
public static ExecutorService pool = Executors.newFixedThreadPool(500);
public Servlet1() {
super();
}
@Override
public void destroy() {
super.destroy(); // Just puts "destroy" string in log
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("进入Servlet的时间:" + System.currentTimeMillis() + ".
");
out.flush();
//在子线程中执行业务调用,并由其负责输出响应,主线程退出
AsyncContext ctx = request.startAsync();
pool.execute(() -> {
try {
//等待十秒钟,以模拟业务方法的执行
Thread.sleep(200);
PrintWriter out2 = ctx.getResponse().getWriter();
out2.println("业务处理完毕的时间:" + System.currentTimeMillis() + ".");
out2.flush();
ctx.complete();
} catch (Exception e) {
e.printStackTrace();
}
});
out.println("结束Servlet的时间:" + System.currentTimeMillis() + ".
");
out.flush();
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
@Override
public void init() throws ServletException {
}
}
Servlet2.java
package com.zhuyun.tomcat;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Servlet2 extends HttpServlet {
private static final long serialVersionUID = 1L;
public Servlet2() {
super();
}
public void destroy() {
super.destroy(); // Just puts "destroy" string in log
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
out.println("Hello world!");
out.flush();
out.close();
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
public void init() throws ServletException {
}
}
Servlet3.java
package com.zhuyun.tomcat;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Servlet3 extends HttpServlet {
private static final long serialVersionUID = 1L;
public Servlet3() {
super();
}
public void destroy() {
super.destroy(); // Just puts "destroy" string in log
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("Hello world!");
out.flush();
out.close();
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
public void init() throws ServletException {
}
}
然后使用ab工具来测试三条API的性能情况。该测试分了两种情况:第一种情况,tomcat的执行线程数很小(例如:50);第二种情况,tomcat的执行线程数很大(例如:500)
第一种情况:tomcat的执行线程数为50
ab工具测试命令:ab -n 20000 -c 500 "http://192.168.10.16:9390/tomcat/servlet1"
Servlet1的测试结果如下:
Servlet2的测试结果如下:
Servlet3的测试结果如下:
第二种情况:tomcat的执行线程数为500
Servlet1的测试结果如下:
Servlet2的测试结果如下:
Servlet2的测试结果如下:
上述的测试结果如下:
QPS | tomcat 线程数50 | tomcat 线程数500 |
Servlet1 | 2414 | 2378 |
Servlet2 | 248 | 2354 |
Servlet3 | 35113 | 36265 |
从结果可以看出,当tomcat的执行线程数比较小时,同步和异步的QPS相差10倍;而当tomcat的执行线程数比较大时,同步和异步的QPS几乎没有差距(个人不太理解,难道将tomcat线程数设置大一些后,同步或异步不影响性能?)。如果结果响应很快(Servlet3), 那么tomcat的线程数对于QPS的影响很小。后面的Springmvc的测试结果与Servlet差不多,就不列举了。
pom依赖:
org.springframework
spring-webmvc
4.3.18.RELEASE
web.xml中加入以下内容:
springmvc
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:springmvc-servlet.xml
1
true
springmvc
/
创建一个Controllerr,内容如下:
package com.zhuyun.springmvc;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncTask;
/**
* springmvc异步处理
* @author zhouyinfei
*
*/
@Controller
public class Controllerr {
public static ExecutorService pool = Executors.newFixedThreadPool(500);
@RequestMapping("/springmvc")
@ResponseBody
public String hello(String username, String password){
return "username=" + username + ",password=" + password;
}
@RequestMapping("/helloworld1")
@ResponseBody
public String helloWorld1(){
return "Hello world!";
}
//同步阻塞
@RequestMapping("/helloworld2")
@ResponseBody
public String helloWorld2() throws InterruptedException{
Thread.sleep(200);
return "Hello world!";
}
//异步1
@RequestMapping("/helloworld3")
@ResponseBody
public DeferredResult helloWorld3(){
DeferredResult defer = new DeferredResult();
pool.submit(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
defer.setResult("Hello world!");
});
return defer;
}
//异步2
@RequestMapping("/helloworld4")
@ResponseBody
public WebAsyncTask helloWorld4(){
Callable callable = new Callable() {
public String call() throws Exception {
Thread.sleep(200);
return "Hello world!";
}
};
return new WebAsyncTask(callable);
}
}
其中,helloWorld1方法直接返回结果,helloWorld2方法同步返回结果,helloWorld3和helloWorld4是异步的两种不同方式。使用ab工具测试这几条API性能,结果与上面Servlet的结果类似。