Spring Webflux 介绍
Spring Webflux在spring 5引进来的而且他支持响应式编程。他使用的是异步编程模型。使用响应式编程可以让应用程序具有牛逼的伸缩性,以支持一段时间内比较大的负载请求。
和Spring MVC相比他解决了什么问题
Spring MVC使用的是同步编程模型,每一个请求都会匹配一个线程,该线程负责将拿到的结果返回给该请求的socket连接里。当应用程序发起一次网络调用,比如从DB里获取数据、从其他应用获取结果、从文件中进行读写操作,在这种情况下,该线程不得不进行同步等待这结果。请求线程会被阻塞掉,CPU在这段时间也没有什么利用率。这就是为什么这种模型需要使用线程池来进行处理请求。这种模型在处理请求量不大的情况下可以工作得很好,但是在请求量非常大的情况下,就会导致响应很慢或者无响应。这无疑会影响线上的业务。
针对这样的应用程序,响应式编程就有用武之地了。使用这种方式,请求线程将所需的请求数据发送到网络,并不是等待响应,而是会得到一个回调函数,该函数将在这个阻塞任务完成后执行调用。这种方式可以让请求的线程马上可以去处理另外的请求。如果使用得当,线程就不会被阻塞并且线程可以充分的利用CPU.这里的适当方式指的是,对于这个模型,每个线程都需要有响应式的行为,因此数据库驱动程序、服务间通信、web服务器等也应该使用响应式线程。
内嵌Server容器
Spring Webflux默认是将Netty做为他们的内部server容器。除了支持Netty外,他还支持Tomcat,Jetty,Undertow或者其他Servlet3.1+的容器。这里需要注意的是,Netty和Undertow是非servlet,而Tomcat和Jetty是众所周知的servlet容器。
在介绍Spring Webflux之前,我们先讲讲SpringMVC,后者用的Tomcat作为内嵌服务容器也就是"每个请求一个线程模型".而Webflux,Netty用的是"Event Loop模型"。虽然Webflux也支持在Tomcat中运行,但是仅支持实现了Servlet3.1及以后的API版本。
小提示:
Servlet 3引入了异步编程的特性,Servlet 3.1另外引入了异步I/O,允许所有操作异步。
EventLoop
EventLoop是一个非阻塞I/O线程(NIO),它持续运行不会阻塞,并从一系列的socket通道接收新的请求。如果有多个EventLoop,那么每个EventLoop被分配给一组socket通道,所有的EventLoop都在一个EventLoopGroup下管理。
下面的图展示了EventLoop是怎么运行的:
这里展示的是EventLoop它在服务器端的用法。但当它在客户端时,它的工作方式是一样的,它向另一个服务器发送请求,例如I/O请求。
a.所有请求都在一个统一的Socket上接收,该Socket与一个称为SocketChannel的通道相关联。
b.每个SocketChannels都会有相关联的EventLoop线程,因此对该Socket/SocketChannels的所有请求都被移交给相同的EventLoop。
c. EventLoop上的请求通过一个通道管道,这些请求会在已经配置的入站Channel处理器或WebFilters来进行对应的处理。
d.在此之后,EventLoop执行应用程序自定义的代码。
e.在它的处理完成后,EventLoop会再执行配置好的出站Channel处理器进行处理。
f.最后,EventLoop将处理完的数据放回到之前对应的SocketChannel/Socket中。
g.在循环中重复步骤a到步骤f。
这是一个简单的用例,这肯定会让资源的使用最少化,因为他使用的是单线程,但这并不能解决真正的问题。
如果应用中的EventLoop由下面的原因导致阻塞会怎样?
1.CPU高度密集型工作
2.数据库的读写操作
3.文件的读写操作
4.调用网络上的其他应用
在上面的场景中,EventLoop 将会被会阻塞在d步骤中,我们将遇到和之前一样的情况,我们的应用将很快变得响应很慢或者无响应状态。创建额外的EventLoop不是解决方案,如前所述,一系列的Socket被绑定到单个EventLoop。因此,在已经被阻塞的EventLoop中的Socket是不能够让其他的EventLoop来帮助处理的。
小提示:
只有当应用程序运行在多CPU平台上时,才应该创建多个EventLoop,因为EventLoop必须保持一直运行,一个CPU不能保持多个EventLoop同时运行。默认情况下,应用程序开始时有多少个eventloop,就有多少个CPU(CPU数量应和EventLoop保持一致)。
这就是NIO EventLoop真正强大的地方,它背后原理极其简单。应用程序应该将请求委托给另一个线程,并通过异步回调返回结果,以解除EventLoop对新的请求处理的阻塞。所以EventLoop的工作步骤修改为如下(注:前面的情况就是所一个线程搞定所有的事,新的方案就是收到请求后,将业务上的处理交给业务线程池去理,这样效率更高):
a.步骤a到步骤c 保证一致
b.EventLoop将请求委托给新的线程进行处理
i.工作线程执行这些比较费时的任务
ii.完成后,它将响应改成一个任务,并将其添加到' ScheduledTaskQueue '中
c.EventLoop轮询ScheduledTaskQueue中的任务
i.如果有,通过任务的Runnable#run方法执行步骤e到步骤f
ii.否则,继续轮询SocketChannel上的新请求
这些工作线程可以由开发人员创建,也可以从Reactor、RxJava或其他响应库中选择“Scheduler”策略。请记住临时使用这些线程,以保持资源利用率最小。
当应用程序有多个api,其中一些api由于网络调用或CPU高密集型工作而变慢时,这种方法很有帮助。在这种情况下,应用程序仍然可以部分可用并响应用户请求。
理想情况下,要使您的应用程序完全具有响应性,不应该有任何单个线程可能阻塞的情况。到目前为止,我们能够解除阻塞我们的请求处理线程,即EventLoop。但是我们的工作线程仍然在处理阻塞任务,当更多这样的请求到来时,我们不能无限期地增加这些线程数,因为这也可能导致管理大型线程的新问题,这可能严重影响CPU利用率和内存使用。在这种情况下,我们需要高效且有策略地使用工作线程。
对于上面提到的大多数阻塞情况,最好全部用响应式方法来处理。我们必须选择为DB调用提供异步操作。另外,我们有响应式Http客户端,可以通过网络调用另一个应用,如Spring Webflux响应式web客户端,它基本使用Reactor Netty库,即EventLoop模型。因此,如果我们的应用程序使用网络和响应式的web客户端,那么EventLoop资源将被共享。
优点:
1.请求处理线程比较轻量级
2.硬件资源得到最佳利用
3.单个EventLoop可以在http客户端和请求处理之间共享
4.单线程可以处理多个Socket上的请求,例如来自不同客户端的请求
5.在无限流响应(infinite stream response)的情况下,该模型为背压处理提供支持。
每个请求一个线程模型
自从引入同步Servlet编程以来,一个请求对应一个线程模型就在实践中得到了广泛的应用。Servlet容器采用此模型来处理传入的请求。由于请求处理是同步的,因此该模型需要许多线程来处理请求,从而提高资源利用率。没有考虑可能阻塞这些线程的网络I/O。因此,对于可扩展性来说,资源通常需要扩展,但这就增加了系统成本。
如果Spring Webflux实现了Servlet 3.1+ api,那么使用响应式Spring Webflux也可以选择使用Servlet容器。Tomcat是最常用的Servlet容器,它也支持响应式编程。
当您Tomcat上使用Spring Webflux时,应用程序从线程池(例如10个)获取固定线程数来处理请求。来自Socket的请求被分配给此线程池中的线程。这里需要注意的是,Socket与线程之间不存在永久的绑定。
如果任何请求的线程在操作I/O时被阻塞了,该请求仍然可以被线程池中的其他线程处理。但是如果越来越多这种请求会导致所有的线程都被阻塞住。到目前为止,这与我们在同步处理中使用方式相同。但是在响应式方法中,它允许用户将请求委托给EventLoop中另一个工作线程池处理。通过这种方式,请求处理线程变得可用,应用程序一直可以保持对Client端的响应。
下面是在该模型中处理请求时执行的步骤:
1.所有请求都在唯一的Socket上接收,该Socket与一个称为SocketChannel的通道进行关联。
2.请求会被交给一个线程池里的一个线程进行处理。
3.线程上的请求需要经过之前配置好的的处理程序(如过滤器、servlet)进行处理。
4.请求线程可以将请求委托给工作线程或响应式web客户端(Webclient),同时在Controller中执行任何阻塞代码。
5.在它的完成后,工作线程或Webclient(EventLoop)将结果返回给相关的Socket。
同样,通过选择响应式客户端(如响应式web客户端)和响应式DB驱动程序,使应用程序完全具有响应式,最小线程就可以为系统提供有效的可伸缩性。
优点:
1.支持Servlet的api
2.如果任何请求线程阻塞,它只阻塞单个客户端套接字,而不像EventLoop中那样阻塞一系列套接字
3.对硬件资源的充分利用
性能比较
为了进行比较,我们创建了一个使用Spring Webflux的示例响应式Spring应用程序,故意延迟了100ms。然后将请求委托给另一个工作线程,以解除请求处理线程的阻塞。
为了进行比较,使用了以下配置:
- Netty单个EventLoop的配置属性reactor.net . ioworkercount =1
- 为Tomcat配置单个请求处理线属性 server.tomcat.max-threads=1
- AWS EC2实例,配置为t2。micro (1GB RAM, 1CPU)
- Jmeter测试脚本有100个用户,每1秒增加个,执行了10分钟
最大CPU使用率:
Tomcat :37.6%
Netty:34.6%
结果:Tomcat使用cup比Netty多8.67052%
性能结果:
Tomcat 90%平均响应时间 : 114ms
Netty90%平均响应时间 : 109ms
结果:Tomcat的结果比Netty响应时长增加了4.58716%
原文:https://singhkaushal.medium.com/spring-webflux-eventloop-vs-thread-per-request-model-a42d07ee8502