转载

Java编程方法论-Spring WebFlux篇 01 为什么需要Spring WebFlux 上

本系列为本人Java编程方法论 响应式解读系列的 Webflux 部分,现分享出来,前置知识Rxjava2 ,Reactor的相关解读已经录制分享视频,并发布在b站,地址如下:

Rxjava源码解读与分享: www.bilibili.com/video/av345…

Reactor源码解读与分享: www.bilibili.com/video/av353…

NIO源码解读相关视频分享: www.bilibili.com/video/av432…

NIO源码解读视频相关配套文章:

BIO到NIO源码的一些事儿之BIO

BIO到NIO源码的一些事儿之NIO 上

BIO到NIO源码的一些事儿之NIO 中

BIO到NIO源码的一些事儿之NIO 下 之 Selector BIO到NIO源码的一些事儿之NIO 下 Buffer解读 上 BIO到NIO源码的一些事儿之NIO 下 Buffer解读 下

其中,Rxjava与Reactor作为本人书中内容将不对外开放,大家感兴趣可以花点时间来观看视频,本人对着两个库进行了全面彻底细致的解读,包括其中的设计理念和相关的方法论,也希望大家可以留言纠正我其中的错误。

为什么需要Spring WebFlux

在我们要面临越来越高的并发处理,传统的 Spring Web MVC 已经无法满足我们的需求,即我们需要一个无阻塞的且通过很少的硬件资源(体现就是通过很少数量的线程)的 web 框架来处理并发任务。 Servlet 3.1 确实为非阻塞I/O提供了相应API。但是,使用它时,Servlet 其余部分API的在执行时就是同步(比如 Filter )或阻塞( getParametergetPart )。 我们知道, Tomcat 这类服务器其有一个 Servlet Worker 线程池,而使用 Spring Web MVC 的话,对于请求的处理过程将会在 DispatcherServlet 中进行,而其内部默认并不会进行异步处理,所以,当有 I/O 或者耗时操作的时候,很可能会阻塞当前 Servlet 所在线程。(参考网上关于 SpringMVC 异步操作的相关博文),关于其异步改造,本人也在 RxJava2 的相关分享视频的项目实例中进行有改造,大家可回顾。而我们的目的就是将当前 Servlet 所在线程给让出来,这样可以接收更多的请求。 那两者的区别到底在什么地方, Spring WebFlux 到底有何意义可供我们迁移学习。相信大家在接触过 Tomcat 之后,都会去学习一下 Tomcat 的配置文件 server.xml ,从中我们也知道 Connector ,其主要功能是:接收连接请求,创建 RequestResponse 对象用于和请求端交换数据;然后分配线程让 Servlet 容器来处理这个请求,并把产生的 RequestResponse 对象传给 Servlet 。当 Servlet 处理完请求后,也会通过 Connector 将响应返回给客户端。 所以我们从 Connector 入手,讨论一些与 Connector 有关问题,包括 NIO/BIO 模式、线程池、连接数等。 根据协议的不同, Connector 可以分为 HTTP ConnectorAJP Connector 等,此处只讨论 HTTP Connector

Tomcat下Connector中的协议

Connector 在处理HTTP请求时,会使用不同的 protocol 。不同的 Tomcat版本 支持的 protocol 不同,其中典型的 protocol 包括 BIO、NIO和APR ( Tomcat7 中支持这 3 种, Tomcat8 增加了对 NIO2 的支持,而在 Tomcat8.5Tomcat9.0 ,则去掉了对 BIO 的支持)。

Connector 使用哪种 protocol ,可以通过 Tomcat 配置文件 server.xml 中的 <connector> 元素中的 protocol 属性进行指定,也可以使用默认值。如果没有指定 protocol ,则使用默认值 HTTP/1.1 ,其含义如下:在 Tomcat7 中,自动选取使用 BIOAPR (如果找到 APR 需要的本地库,则使用 APR ,否则使用 BIO );在 Tomcat8 中,自动选取使用 NIOAPR (如果找到 APR 需要的本地库,则使用 APR ,否则使用 NIO )。

无论是 BIO ,还是 NIOConnector 处理请求的大致流程是一样的: 在accept队列中接收连接(当客户端向服务器发送请求时,如果客户端与服务端完成三次握手建立了连接,则服务端将该连接放入accept队列);在连接中获取请求的数据,生成request;调用Servlet容器处理请求;返回response。

为了便于大家的理解,这里先明确一下连接与请求的关系:

  • 连接是 TCP 层面的(传输层),对应 socket
  • 请求是 HTTP 层面的(应用层),必须依赖于 TCP 的连接实现。
  • 一个 TCP 连接中可能传输多个 HTTP 请求。

BIO是Blocking IO,顾名思义是阻塞的IO;NIO是Non-blocking IO,则是非阻塞的IO。而APR是Apache Portable Runtime,是Apache可移植运行库,利用本地库可以实现高可扩展性、高性能;Apr是在Tomcat上运行高并发应用的首选模式,但是需要安装apr、apr-utils、tomcat-native等包。

BIO 实现的 Connector 中,请求主要是由 JioEndpoint 对象来处理。 JioEndpoint 维护了 AcceptorWorker ,通过 Acceptor 接收 socket ,然后从 Worker线程池 中找出空闲的线程处理 socket ,如果 worker线程池 没有空闲线程,则 Acceptor 将阻塞。其中 WorkerTomcat 自带的线程池,如果通过 <Executor> 配置了其他线程池,原理与 Worker 类似。

NIO 实现的 Connector 中,处理请求的主要实体是 NIOEndpoint 对象。 NIOEndpoint 中除了包含 AcceptorWorker 外,还是用了 Poller ,处理流程如下图所示:

Java编程方法论-Spring WebFlux篇 01 为什么需要Spring WebFlux 上

图中 AcceptorWorker 分别是以线程池形式存在, Poller 是一个单线程(此处是其与Netty最大的区别)。注意,与 BIO 的实现一样,这里,需要提及的是,在 server.xml 中没有配置 <Executor> ,则以 Worker线程池 运行,如果配置了 <Executor> ,则以基于 java.util.concurrent.ThreadPoolExecutor 线程池运行。

由图可知, Acceptor 接收 socket 后(这里虽然是基于 NIOconnector ,但是在接收 socket 方面还是传统的 serverSocket.accept() 方式,获得 SocketChannel 对象,然后封装在一个 tomcatorg.apache.tomcat.util.net.NIOChannel 实现类对象,并将之包装为一个 PollerEvent对象 ),并不是直接使用 Worker 中的线程处理请求,而是先将 PollerEvent对象 发送给了 Poller ,而 Poller 是实现 NIO 的关键。 AcceptorPoller 发送 包装后的请求 通过添加队列的操作实现,这里使用了典型的生产者-消费者模式。同时,在 Poller 中,维护了一个 Selector 对象;当 Poller 从队列中取出 socket 后,注册到该 Selector 中;然后通过遍历 Selector ,找出其中可读的 socket ,并使用 Worker 中的线程处理相应请求。与 BIO 类似, Worker 也可以被自定义的线程池代替。

通过上述过程可以看出,在 NIOEndpoint 处理请求的过程中,无论是 Acceptor 接收 socket ,还是线程处理请求(添加到 Poller 队列是同步的),使用的仍然是阻塞方式;但在 读取socket并交给Worker中的线程 的这个过程中,使用非阻塞的 NIO 实现,这是 NIO 模式与 BIO 模式的最主要区别(其他区别对性能影响较小)。而也是由于这个区别,在并发量较大的情形下可以给Tomcat效率带来显著提升。

目前大多数 HTTP 请求使用的是长连接( HTTP/1.1 默认 keep-alivetrue ),而长连接意味着,一个 TCPsocket 在当前请求结束后,如果没有新的请求到来, socket 不会立马释放,而是等 timeout 后再释放。如果使用 BIO读取socket并交给Worker中的线程 这个过程是阻塞的,也就意味着在 socket 等待下一个请求或等待释放的过程中,处理这个 socket 的工作线程会一直被占用,无法释放;因此Tomcat可以同时处理的socket数目不能超过最大线程数,性能受到了极大限制。而使用NIO, 读取socket并交给Worker中的线程 这个过程是非阻塞的(是由 Poller 所在线程维护的),并不会占用工作线程,因此Tomcat可以同时处理的 socket 数目不受最大线程数约束,并发性能也就大大提高,但 Poller 同时也是其性能瓶颈。

因此,随着 NIO 所实现 Connector 的引入,客户端到服务器的通信是非阻塞的,但是服务器到 servlet 的连接仍然是阻塞的,也就意味着每个请求都会阻塞一个线程,也就导致我们会看到一个线程处理一个请求的模型。 因此,随着 Servlet 容器的发展, Servlet API 也就需要非阻塞支持,也就是 Servlet 3.1+

关于 TomcatConnector 的更多深入解读,有感兴趣的可以参考本人的另一篇博文 tomcat从启动到接轨Servlet二三事

原文  https://juejin.im/post/5c6fe823e51d455b8c18d8aa
正文到此结束
Loading...