Reactive Stack系列(一):响应式编程从入门到放弃

为了详细介绍下基于Spring Framework 5 & Spring Boot 2 的WebFlux的响应式编程,先画下如下逻辑图,后文将以逻辑图箭头方向逐一解释关于响应式编程的点点滴滴。

roadmap.png


1. Spring Framework5

自 2013 年12月Spring Framework4.0.0发布以后,时隔接近4年Spring才迎来了下一个大版本,这其中引入的新特性中, 最受人关注的主要围绕在两个方面,即响应式编程+全异步非阻塞,知乎上的回答有人戏称这是Spring堵上未来的一击,react spring+JDK9像是一种新的Java语言。

1.1 The Reactive Manifesto/Rx Java

自从大名鼎鼎的响应式宣言/The Reactive Manifestohttps://www.reactivemanifesto.org)提出以来,现代web应用构建在沿着其提出的思路一步一步演进,并且不是一时趋势。其主要内容翻译成中文如下图:

响应式宣言.jpg

我们需要系统具备以下特质:即时响应性(Responsive)、回弹性(Resilient)、弹性(Elastic)以及消息驱动(Message Driven)。 对于这样的系统,我们称之为反应式系统(Reactive System)。


阅读对应的响应式宣言,我们会发现核心思想就是通过回弹性(Resilient)、弹性(Elastic)以及消息驱动(Message Driven)三种手段来实现系统高可用的健壮易维护系统。


其中对三种形式手段做了详细介绍:

回弹性:系统在出现失败时依然保持即时响应性。 这不仅适用于高可用的、 任务关键型系统——任何不具备回弹性的系统都将会在发生失败之后丢失即时响应性。 回弹性是通过复制、 遏制、 隔离以及委托来实现的。 失败的扩散被遏制在了每个[组件](/glossary.zh-cn.md#组件)内部, 与其他组件相互隔离, 从而确保系统某部分的失败不会危及整个系统,并能独立恢复。 每个组件的恢复都被委托给了另一个(外部的)组件, 此外,在必要时可以通过复制来保证高可用性。 (因此)组件的客户端不再承担组件失败的处理。

弹性: 系统在不断变化的工作负载之下依然保持即时响应性。 反应式系统可以对输入(负载)的速率变化做出反应,比如通过增加或者减少被分配用于服务这些输入(负载)的资源。 这意味着设计上并没有争用点和中央瓶颈, 得以进行组件的分片或者复制, 并在它们之间分布输入(负载)。 通过提供相关的实时性能指标, 反应式系统能支持预测式以及反应式的伸缩算法。 这些系统可以在常规的硬件以及软件平台上实现成本高效的弹性

消息驱动:反应式系统依赖异步的消息传递,从而确保了松耦合、隔离、位置透明的组件之间有着明确边界。 这一边界还提供了将失败作为消息委托出去的手段。 使用显式的消息传递,可以通过在系统中塑造并监视消息流队列, 并在必要时应用回压, 从而实现负载管理、 弹性以及流量控制。 使用位置透明的消息传递作为通信的手段, 使得跨集群或者在单个主机中使用相同的结构成分和语义来管理失败成为了可能。 非阻塞的通信使得接收者可以只在活动时才消耗资源, 从而减少系统开销。


ReactiveX项目(Reactive Extension)是基于响应式异步编程的一个跨语言项目,其中Rx Java为Java版本的对应实现,在Spring项目中,其基于Reative Streams 实现了一套对应的响应式编程实现(Flux/Mono).


1.2 None-Blcoking/Tomcat 8 & Netty

非阻塞的概念由来已久,其不仅仅在HTTP通讯中涉及,在其他读写中也普遍涉及,Netty给出了一个非常优雅的实现方式,其隐藏了我们在JDK7种常见的一些类似Selector/Channel等复杂概念,可以快速简单的构建出一个非阻塞Web服务器。对于Tomcat而言,其非阻塞模型主要针对我们常见的大量连接情况,传统的BIO性能拖累主要集中在大量的连接请求和工作线程数据绑定导致无法弹性削峰填谷,而最新的Tomcat版本是用轮询的方式减少对连接请求的资源消耗的问题,其把对应的请求接收后放入队列中,等待后续处理。以Tomcat为例,其NIO性能提升关键如下图所示(基于Servlet3.1),分别是BIO和NIO性能区别关键:

BIO.png

NIO.png

CP.png

2. Spring WebFlux

WebFlux,简而言之,是一套Spring Team认为未来需要代替Spring MVC体系的web框架,其构建于响应式编程规范之上,提供流式非阻塞具体实现。在官网提供的各种资料中,目前我们可以认为Spring MVC和Spring WebFlux是两套平行的架构体系。在Spring Boot2.0.0版本以及后续中,我们通过Spring Initializer引入对应的web模块或者web reactive 模块,即可发现其分别对应着Spring MVC和Spring WebFlux。

MVC_WebFlux.png

2.1 Spring MVC VS Spring WebFlux

首选放出官方对比图:

MVC_VS_WebFlux.png

这张图代表了以MVC的Servlet Stack和以WebFlux为代表的Reactive Stack的框架对比图,从各个层面做了对比,具体解释如下:

@Controller/@RequestMapping VS Router Functions:我们知道MVC体系在SpringBoot中, 只要在Controller层加标注@RestController以及对应方法加上@GetMapping/@PostMapping方法即可在启动Tomcat容器以后自动根据Annotation来加载对应的mapping关系。在Reactive Stack中,我们需要在启动的时候通过Lambda风格的Router Functions统一把所有的Web入口注册一遍(虽然我觉得很怪,但是目前就是这么实现的)。

spring-webmvc VS spring-webflux模块:MVC体系中我们通常是命令式语法,WebFlux体系中,我们需要结合流式写法加上基于对应Router Functions注册的方法对流式处理对返回结果做一定的转化(详细见后续例子)。

Servlet API VS HTTP/Reactive Streams:MVC体系目前可以基于Servlet3.1实现异步通信,但是实现流式通信的实现较复杂。WebFlux天生基于流式异步通信,编程方式较友好。

Servlet Container Vs Reactive Stream Contrainers:WebFlux天生构建于异步流式非阻塞模式,所以它适用于对应的特定支持Web容器。

具体写法举例如下截图解释:

Router Functions.png

以查询某人继续解释,根据上图我们会去PersonHandler里面的getPerson方法构建返回结果。

Hander.png

可以看出,Handler类相当于我们MVC体系中的Service层,MVC中的Controller层集合相当于上面举例的RounterFunctions中不断构建的入口列表。


根据gradle.build中引入的starter组件,我们分别注释掉对应的一部分starter组件,如下:

implementation()
implementation()


可以分别尝试用Tomcat和Netty启动服务器,reactive stack默认使用netty启动,我们可以观察对应的Idea启动日志,以及启动对应的Visual VM来观察对应的线程,截图如下:需要注意的是,其中,reactor-http-nio线程池是由程序根据系统的CPU数量来决定的。

Tomcat如下:

Tomcat.png

tomcat_vm.png


Reactive Stack中,Netty如下:

Netty.png

netty_vm.png


从截图中我们可以清楚看到对应的线程工作状态。看到这里,大部分人应该认识到,对于Reactive Stack来说,我们并不需要去管理线程池,程序是在根据系统资源在决定我们应该创建多少线程,替我们管理线程的生命周期,线程的调度。也就是说,Reactive Stack中,我们基本可以不用去管ThreadPoolExecutor。站在更高的角度看,这一层是很有深意的,需要我们去仔细思考这样到底带来了哪些好处。


2.2 Spring WebFlux的几个特性介绍以及和Spring MVC对应特性的横向对比。

通过2.1小节,我们基本知道了在SpringBoot中,如何引入WebFlux,以及对应的写法实践和Spring MVC的对比,接下来介绍下对应的响应式特性(及优缺点)。

由于不用关心线程池,加上Reactive模式的核心是没有全局单点,这决定了一件事,任何一个请求在WebFlux框架中走一圈,耗时应该是相同的(假设我们所有的组件都是None-Blocking的,尤其是数据库层),如下图:

ReactiveX.png

这是ReactiveX官网上的一张图,它的寓意就是:假设我们现在看到每个颜色的圆点都是一个HTTP请求,那么从他们落入我们的网卡开始到离开我们的网卡,对于开发者来说,我们如果正常编码,这些请求的耗时是完全相同的,这一点很重要,其实是对应着响应式宣言的“弹性(Elastic)”,这进而可以让我们确定一点:系统的性能瓶颈是可预测的。这点的重要性在于,当我们预测可预见的未来访问请求增加1000倍,我们的部署资源增加1000倍即可,这即对应了响应式宣言中的弹性。由于线程资源的固定,不存在频繁的线程切换/生成/销毁等等,所有的性能类似于固定成通过水流的管道,那么根据水流的大小,我们就可以确定管道的数量。


根据上一段,进而要提到一个概念;背压(BackPressure),如果当前流入管道的水流速率超过了管道规定的上限,那么上游的发送源会收到反向的发送压力,网上有一张形象的图片就是消防员拿着消防栓喷水灭火时,被反作用力压的往后退。“背压”是一种现象,通常来说,解决背压的方式有两种:一种是返回源头错误告警,一种是忽略请求。


流:在WebFlux中,我们的请求可以通过Flux<T>类型来返回一系列的数据,而通过我们的WebClient客户端 + application/json+stream 模式,我们完成数据的持续流式传输,例如客户端请求1W个客户的具体信息->服务端通过Luttuce每秒去Redis获取100个客户,那么对应的返回数据会在100秒内持续的返回对应的客户端。通过这种方式,我们可以做到网络均衡的传输。


完全非阻塞调用链:

DATA_Blocking.png

根据最新的GA版本,我们的系统调用大部分时间是可以实现完全的非阻塞系统调用的-->关系型数据库(MySQL)除外。因为原来的 Spring 事务管理(Spring Data JPA)都是基于 ThreadLocal 传递事务的,其本质是基于阻塞IO模型,不是异步的。但Reactive是要求异步的,不同线程里面 ThreadLocal 肯定取不到值了。根据最新的进展,好消息是目前的R2DBC项目已经实现postgresql的非阻塞实现,相信在不久的将来,MySQL也会最终加入Reactive Stack的拼图。


2.3 Reactive Stack 性能测试

先想一想,结果会是怎样?答案是,和Servlet Stack(MVC)没多大差距,甚至可能还弱一点。Google上给出的大部分测试结果已经证明了这一点,为什么呢?想了一下,应该是因为以下几点:

  • Spring MVC是一个经过时间和实践检验的成熟体系,没有短板,在相同的把CPU,内存,IO等资源吃满的情况下,只要合理的控制好Thread对CPU时间片的浪费,MVC完全是可以做到最优解的。

  • WebFlux的线程管理模式,包括背压在内的一系列特性,其实熟悉线程管理来说,就相当于是ScheduledThreadPoolEXecutor根据对应参数设置,并且对应的设置好rejectHandler policy。

  • DefferredResult,ResponseBodyEmmiter,SseEmmiter等类让MVC体系也可以毫无压力完成异步以及流等高性能响应方式,从终极实践方式来说,两者并无本质上的差异。


3 总结与展望

说了这么多,好像发现WebFlux也没啥?Spring 堵上未来的一击会成功吗?诚然,就官方目前的说法也是,不太适合大规模应用,但是小范围的改造是OK的。


但是,回到最开始,让我们再看看响应式宣言。Spring WebFlux费尽心思搞了这么一套和MVC平行的体系架构,其本质思想已经基本默默地在反映着响应式宣言。即时响应性,弹性,回弹性,消息驱动。这整个一套技术栈,其实是在构建一套可预测的,更加健壮的,开发人员更加少干预的Web框架,从而让大家专注于业务层面的实现,进一步忽略/屏蔽底层系统的细节。


如果说Spring Cloud是从【宏观系统层面的开发】角度在实践健壮的高可用系统+系统运维,K8S在【DEV OPS】层面实践更好的系统运维,Service Mesh在【基础设施层(infra)】实践健壮的高可用系统+系统运维,那么WebFlux(包括整个Reactive Stack体系的其他成员)就是从【微观项目层面的开发】角度在实践健壮的高可用系统+系统运维。或多或少,它们都从各个维度在朝着“更少的人治”角度去努力。

猜你喜欢

转载自blog.51cto.com/14054453/2328133