高并发下,Tomcat、HttpClient让系统瘫痪

高并发下,Tomcat、HttpClient让系统瘫痪

最近做了一个项目,需要通过http多次请求和外部系统数据交换,例如支付,地图等。但是交互过程通过http调用第三方接口响应时间慢会导致并发量下降,甚至堵死系统。
下面将从Tomcat底层原理上分析为什么http交互会导致Tomcat性能下降。

Tomcat和BIO

老版本的Tomcat底层使用BIO方式实现,就是java常用的Socket网络编程。

什么是BIO

在这里插入图片描述
BIO的实现在java.io包中。它是基于流模型实现的,交互的方式是同步、阻塞方式。也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里。

特点:
1.同步阻塞IO
2.一个请求对应一个线程
3.没有数据达到,也会阻塞
优点: 代码比较简单、直观
缺点: 同步执行导致阻塞,一个Socket使用一个线程,浪费资源,容易成为应用性能瓶颈。

正是因为BIO的特性,因此每一个客户端连接需要分配一个线程。虽然使用线程池可以让提升处理性能,但是线程分配也是有上限的不可能无限分配线程。这就导致如果系统内发起http请求返回数据等待时间较长时,并发数基本上就是分配的线程数上限。
当线程池分配的线程都在使用时,新accept的socket在调用executorService.execute时就会进入线程池的队列中等待。等到有可用线程时任务才开始执行,但是http请求的响应时间又很长,这就导致后续的socket等待的时间也开始变长,出现恶性循环。

ExecutorService executorService = Executors.newFixedThreadPool(200);
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket accept = serverSocket.accept();
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            try {
                // todo 调用servlet
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });
}

那有没有方法可以提高Tomcat的性能呢?其实新版本的Tomcat的底层有一套NIO的实现,通过配置Connector的protocol为Http11NioProtocol就可以实现NIO的方式。

Tomcat和NIO

什么是NIO

在这里插入图片描述
NIO的实现在java.nio包中,通过单个Selector监听多个Channel中的数据到达事件,俗称多路复用。这样的好处是一个线程就可以监听许多Channel的数据,相对于BIO有着显著的性能提成的。

特点:
1.同步非阻塞IO
2.利用IO多路复用技术+NIO,多个channel一个线程监听
优点: 事件监听线程只有一个主线程。数据发过来时启动另一个线程读取,主线程又可以继续监听其他Channel的事件。
同时读取线程使用线程池可以公用资源,用完还给线程池再给别的线程用。
缺点: 事件监听是异步的,在业务中数据都是通过接口回调的方式进行的。所以编程的思想和思路都要发生转变。
同时也增加了技术实现的难度。

// 创建一个selector
Selector selector = Selector.open();

// 初始化TCP连接监听通道
ServerSocketChannel serverCh = ServerSocketChannel.open();
serverCh.bind(new InetSocketAddress(8080));
serverCh.configureBlocking(false);
serverCh.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    int events = selector.select();
    if (events > 0) {
        Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
        while (selectionKeys.hasNext()) {
            SelectionKey key = selectionKeys.next();
            if (key.isAcceptable()) {
                SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();
                sc.configureBlocking(false);
                sc.register(selector, SelectionKey.OP_READ);
            } else if (key.isReadable()) {
                // todo 调用servlet
            }
            selectionKeys.remove();
        }
    }
}

问题: 这么说是不是使用Tomcat的Http11NioProtocol就万事大吉了呢?

我们以Spring Boot为例,Spring Boot底层集成了Embed Tomcat并且使用了Http11NioProtocol。

下面使用Spring Boot实现一个请求第三方接口返回数据的demo。
配置一个test接口延时1000毫秒后返回数据,来模拟第三方接口返回数据慢的情况。

@SpringBootApplication
@RestController
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public OkHttpClient okHttpClient() {
        return new OkHttpClient();
    }

    @Autowired
    private OkHttpClient okHttpClient;

    @RequestMapping("/bio")
    public byte[] bio(HttpServletRequest req) throws IOException {
        Request request = new Request.Builder().url("http://127.0.0.1:8080/test").build();
        Response response = okHttpClient.newCall(request).execute();
        byte[] bytes = response.body().bytes();
        return bytes;
    }

    @RequestMapping("/test")
    public String test() throws Exception {
        Thread.sleep(1000);
        return "ok";
    }
}

配置Tomcat线程数为500,最大排队数为0

server.port=8080
server.Tomcat.max-threads=200
server.Tomcat.accept-count=0

使用jmeter进行测试,配置jmeter并发线程数为800,每个线程循环100次,进行测试。
在这里插入图片描述

测试结束发现当jmeter并发线程逐步升高到500以上时,性能开始下降直至整个系统崩溃。似乎使用Tomcat NIO模型性能并没有提升。和BIO模型性能差不多,这是为什么呢。
在这里插入图片描述
所以我们再仔细回想一下,Tomcat使用了NIO监听数据事件,调用线程池异步执行。当代码执行到test方法时可以断点查看确实已经在线程池里了。

所以问题不是在Tomcat NIO上。那就是OKHttpClient的问题。
OkHttpClient号称是java界性能最好的HttpClient为什么性能不行呢。以下我们分析下OkHttpClient的实现原理。

OkHttpClient底层使用BIO

如果你阅读过OkHttpClient的源代码你就会发现他的底层是BIO实现的。虽然使用NIO架构的Tomcat的工作线程有500个,但是当jmeter并发数到达500数,所有的线程都在阻塞等待OkHttpClient的数据返回。
如果这个时候再有新的请求上来,Tomcat就会因为线程数就不够而拒绝服务。
在这里插入图片描述

ReactorNetty

所以要想性能获得提升,就需要使用基于NIO的httpServer和httpClient。
下面我们httpServer继续使用NIO模型的Tomcat,OkHttpClient替换成reactor-netty的HttpClient。
首先,导入maven包

<dependencies>
   <dependency>
       <groupId>io.projectreactor</groupId>
       <artifactId>reactor-core</artifactId>
       <version>3.3.1.RELEASE</version>
   </dependency>
   <dependency>
       <groupId>io.projectreactor.netty</groupId>
       <artifactId>reactor-netty</artifactId>
       <version>0.9.2.RELEASE</version>
   </dependency>
</dependencies>

代码实现如下

@SpringBootApplication
@RestController
public class DemoApplication implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @RequestMapping("/nio")
    public DeferredResult<byte[]> nio(HttpServletRequest req) throws IOException {
        DeferredResult<byte[]> result = new DeferredResult<>(0L);
        HttpClient httpClient = HttpClient.create();
        httpClient.request(HttpMethod.GET)
                .uri("http://127.0.0.1:8080/test")
                .responseSingle(new BiFunction<HttpClientResponse, ByteBufMono, Mono<byte[]>>() {
                    @Override
                    public Mono<byte[]> apply(HttpClientResponse httpClientResponse, ByteBufMono byteBufMono) {
                        return byteBufMono.asByteArray();
                    }
                })
                .doOnNext(e -> result.setResult(e))
                .doOnError(e -> result.setErrorResult(e))
                .subscribe();
        return result;
    }

    @RequestMapping("/test")
    public String test() throws Exception {
        Thread.sleep(1000);
        return "ok";
    }
}

为什么要使用DeferredResult呢。因为HttpClient数据发送和返回都是异步的。如果不使用DeferredResult,spring mvc默认你是同步调用test方法执行完成就默认返回200给客户端,并且释放HttpServletRequest和HttpServletResponse。
加了DeferredResult以后,spring mvc就知道你需要异步返回数据就会为保持和客户端的连接。
此时再次使用jmeter进行并发测试,配置参数不变。
在这里插入图片描述
对比图
BIO
在这里插入图片描述
NIO
在这里插入图片描述

总结

最后我们通过流程图再梳理一下整个的调用流程。
在这里插入图片描述
1.Tomcat监听到请求后启动线程处理业务,由于返回的是DeferredResult,所以与客户端连接保持。但是线程已经释放。
2.httpClient监听到,第三方接口返回数据时,启动线程处理数据返回客户端。结束这次http请求,释放线程。
通过流程图发生,实际上从httpClient收到数据时,后续的业务逻辑是在httpClient启动的线程上执行的,而不是在Tomcat的线程上执行的。这是和BIO模式最大的区别。

都2020年你在使用Tomcat和okhttp做业务?

微信关注我,下期带你了解最前沿的Spring WebFlux。

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值