Spring WebFlux并发模型


介绍

主要说明Spring 5.0响应式编程底层的线程模型,并且对比Spring WebFlux的运行容器:Reactor NettyTomcat

响应式编程的动机

典型的Web应用程序包含几个复杂的交互部分 ,并且他们之间大部分会相互阻塞:比如数据库调用阻塞I/O线程,但是,其他请求是独立的,可以同时执行,也可以并行执行。

比如,多个用户请求可以被多个线程同时处理,在如今的多核处理器上有着明显的优势,这种并发模型也被称为 thread-per-request model (一个请求对应一个线程):

如上图所示,每个线程一次只能处理一个请求。

虽然基于线程的并发模型为我们解决了部分问题,但它并不能解决我们在单个线程内进行的大多数交互仍在阻塞的事实。此外,就上下文切换而言,线程过多导致在Java中实现并发的本机线程的成本很高。

随着Web应用程序面临越来越多的请求,thread-per-request model 模型并不能满足这种并发量。

因此,需要一个种新的并发模型,该模型可以以相对较少的线程数来处理越来越多的请求。这是采用响应式编程的主要动机之一。

响应式编程的并发模型

响应式编程的核心是时间驱动,基于数据流的改变来做出相应的处理。因此,在完全无阻塞的环境中,这可以以更高的资源利用率(CPU)实现更高的并发。

响应式编程在使用线程实现并发的方式与传统方式有着很大的区别,响应式编程带来的本质区别是异步。

换句话说,程序处理请求的方式从一系列同步操作转为异步事件流

比如:在响应式编程里,调用数据库操作不会阻塞调用方线程,而是返回一个可以被其他线程订阅的Publisher对象,Subscriber可以在数据操作完成后得到通知(回掉)。

最重要的是,响应式编程并不强调生成和使用哪些线程事件。重点是将程序构造为异步事件流

PublisherSubscriber并不一定需要在同一个线程中,这有助于更好地利用可用线程,从而提高整体并发性。

EventLoop线程

EventLoop 事件循环是实现以更少的线程实现更高的并发性的关键:

上图是事件循环的抽象设计,响应式编程的核心思想:

  • EventLoop以单线程持续运行
  • EventLoop按序从事件队列取出事件进行处理并且在注册相关回掉函数之后立即返回
  • 阻塞的操作比如:数据库调用、外部服务调用,会触发相关操作完成的回掉
  • EventLoop可以触发操作完成通知的回调并将结果发送回原始调用方

EventLoop模型已经有许多的实现,比如: Node.js, NettyNgnix,比起传统的web server( Apache HTTP Server, Tomcat, JBoss)拥有更好的可扩展性

Spring WebFlux 响应式编程

Spring WebFluxSpring在5.0之后推出的响应式web技术栈:

可以看到的是:Spring WebFlux和传统的web框架是平行的关系,并不是来替代Spring MVC

特性:

  • Spring WebFlux通过函数式路由扩展了传统的注解(@Controller)开发模型
  • Spring WebFluxReactive Streams API适配了不同的HTTP运行时环境
  • 因此,能够支持许多响应式运行时环境包括:Servlet 3.1+ 容器Tomcat, UndertowReactor Netty
  • 同时还新增了WebClient,一个响应式、非阻塞、链式API的Http客户端

不同运行时环境的线程模型

响应式应用往往只使用少数几个线程,并充分利用它们。但是,线程的数量和性质取决于选择的实际Reactive Stream API运行时环境。

Spring WebFlux已经适配了通过一个名为HttpHandler的接口适配了不同的运行时环境。这个类只有一个方法,对不同的服务器API(例如Reactor NettyServlet 3.1 APIUndertow API)进行抽象。

虽然Reactor NettyWebFlux应用程序中的默认服务器,但是仅声明一个正确的依赖关系即可切换到任何其他受支持的服务器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-reactor-netty</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>

尽管可以通过多种方式观察在JVM中创建的线程,也可以通过如下方式简单获取:

public void printThreads(){
      Thread.getAllStackTraces()
          .keySet()
          .stream()
          .collect(Collectors.toList());
}

Reactor Netty

Reactor NettyWebFlux的默认运行时Server,不添加其他依赖,启动一个Spring WebFlux应用查看默认创建的线程信息:

除去JVM的相关线程,在一台四核的机器上能看到以上线程信息,Netty产生了一堆用于请求处理的工作线程,这些通常不超过可用的CPU核心数。

Netty使用Event Driving(事件循环) 模型为响应式异步提供高度可扩展的并发能力。

EventLoopGroup管理一个或多个连续运行的EventLoop。因此,不建议创建比可用CPU核心数量更多的EventLoop

EventLoopGroup还为每个新创建的Channel分配一个EventLoop。因此,在Channel的生存期内,所有操作均由同一线程执行。

Apache Tomcat

Spring WebFlux还支持传统的Servlet容器比如 Apache Tomcat

WebFlux依赖 Servlet 3.1 +的非阻塞I/O API,使用Servlet容器时需要通过底层适配,所以不能直接使用Servlet API

使用Tomcat作为运行时环境线程信息:

可以看到,与Netty有着很大区别:Tomcat会启动更多的工作线程,默认时是10

Tomcat 5及更高版本在其Connector组件中支持NIO,该组件主要负责接收请求。

Tomcat核心是Connector组件实现以支持NIO的线程模型。作为NioEndpoint模块的一部分,它由Acceptor,Poller和Worker组成:

Tomcat产生一个或多个用于AcceptorPoller和Worker的线程,通常有一个专用于Worker的线程池。

WebClient 线程模型

WebClient作为响应式Http ClientSpring WebFlux的一部分,可以在需要基于Restful的通信时使用它,能够创建端到端的全链路响应式应用

使用WebClient

使用WebClient非常简单,作为Spring WebFlux一部分,不需要倒入额外依赖:

创建一个简单的RestAPI :

@GetMapping("/index")
public Mono<String> getIndex() {
    return Mono.just("Hello World!");
}

使用WebClient来进行响应式调用:

WebClient.create("http://localhost:8080/index").get()
  .retrieve()
  .bodyToMono(String.class)
  .doOnNext(s -> printThreads());

WebClient线程模型

WebClient也是使用event loop model来实现并发,并且依赖底层的运行时环境。

如果使用Reactor NettyWebClient将和Netty Server共用EventLoop,所以当使用WebClient时的线程情况和Reactor Netty中的一样,并不会创建额外的线程。

WebClient也支持Servlet 3.1+ 的运行时环境比如:Jetty,此时应用的线程情况就非常不同了。

当使用Jetty时,WebClient必须要创建自己的EventLoop

在某些情况下,为客户端和服务器使用单独的线程池可以提供更好的性能。WebFlux默认情况下共用EventLoop,如果需要获得更好的性能可以单独为WebClient创建线程池。


文章作者: Ubi-potato
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ubi-potato !
评论
  目录