目录
什么是响应式?
响应式解决了哪些传统编程的问题
Spring对于Spring WebFlux的使用建议
响应式到底对性能提升多少?
总结
前言:作为一起学响应式微服务的第一篇文章,开始我们先学习一下什么是响应式。本文并不是一篇响应式,Spring WebFlux,Reacto的入门教程,而是向大家介绍什么是响应式编程,和传统的编程的区别,响应式编程能给你带来的益处。
Spring WebFlux跟随Spring 5在2017年发布,距今已经有四年时间了,在国外已经遍地开花,但是在国内Spring MVC还是主流。作为Spring强推的技术栈,甚至可以说将身家性命都压在响应式身上了,响应式有什么好处,响应式解决了什么问题呢?
对于响应式编程,目前大多数开发者对于其可能是一种知道但是没有用过的状态。大多数人对于响应式都是说异步的嘛,所以性能好。其实这种说法不完全对,下面就为大家简单的阐述一下。
在小马哥的《Reactive Programming 一种技术,各自表述》中对国外关于响应式的描述进行了总结:
可以看到确实和大多数人的理解一样,它是异步的,能够提升程序的性能。但是这篇文章也是几年前写的,现在对于其的描述是否有变化呢?
Spring文档关于响应式的定义
Define “Reactive”
The term, “reactive,” refers to programming models that are built around reacting to change — network components reacting to I/O events, UI controllers reacting to mouse events, and others. In that sense, non-blocking is reactive, because, instead of being blocked, we are now in the mode of reacting to notifications as operations complete or data becomes available.
There is also another important mechanism that we on the Spring team associate with “reactive” and that is non-blocking back pressure. In synchronous, imperative code, blocking calls serve as a natural form of back pressure that forces the caller to wait. In non-blocking code, it becomes important to control the rate of events so that a fast producer does not overwhelm its destination.
大意:响应式指的是对变化做出反应的程序模块,比如:对I/O事件做出反应的网络部件、对鼠标事件做出反应的UI控制器等等。在这些场景,非阻塞是反应的,因为使用它代替的阻塞,我们现在正处于对操作完成时或数据变得可用的通知做出响应的模式。---事件驱动
还有另外一个重要的机制:非阻塞背压。在同步场景下,命令式编程阻塞的调用服务调用者会被强制等待,这就形成了一个天然的背压。在非阻塞场景中,控制事件的发布速率防止生产者将消费者压垮。
Spring的文档总结就是响应式=非阻塞+背压。
在阐述这个问题之前,我们需要弄清楚传统编程和响应式编程。
传统的编程也就是命令式编程。我们大多数开发者应该都是从命令式编程开始学习编程的。命令式编程就是描述一组依次执行的步骤。命令式编程它是由一组任务组成,每次只运行一项任务,每项任务又依赖前面的任务。数据会按批次进行处理,在前一项任务还没有完成时,不能将这些数据递交给下一个任务。它的优点就是容易理解,很容易学习。
响应式编程本质是函数式和声明式的。函数式可能不用介绍熟悉JDK8的对其会有一定的认识了(JDK8中对于函数式也只是实现了部分)。关于声明式,它是定义了一组用来处理数据的任务,但是这些任务可以并行的执行。每项任务处理数据的一部分子集,并将结果交给处理流程中的下一项任务,同时继续处理数据的另一部分子集。
关于响应式编程和命令式编程,他们的优缺点可以简单概述:命令式编程容易理解也容易编写,但是它需要一个一个的执行,任务之间有依赖关系,会产生性能问题。响应式编程代码会难以理解,可以容易的实现任务并行的执行。
在《Spring实战》一书中有这样一个例子说明反应式编程和命令式编程:
将命令式编程比作水气球,反应式编程比作花园里的水管。在夏天,他们是偷袭和愉悦毫无戒备朋友的好方式,但是他们的运作方式却不同。
水气球一次性填满有效负载,并撞到目标时弄湿对方。水气球的容量有限,如果你要弄湿更多人,唯一的选择就是增加水气球的数量。
花园软管的有效负载是从水龙头到喷嘴的水流。在特定的时间点,花园软管的容量可能是有限的,但是在打水仗的过程中他的容量是无线的。只要水源源不断从龙头流到水管中,水就会源源不断的从喷嘴中喷出。同一个软管非常好扩展,你可以尽情地和更多朋友打水仗。
虽然水气球(命令式)没有什么固定的问题,但是持有软管的人(反应式)通常在伸缩性和性能方面更具有优势。
看到这里可能会认为反应式作用不大,因为在JDK1.5中就有了J.U.C包提供并发能力,命令式编程也能并行的执行任务,通过回调来实现任务完成后的通知。Spring Reactor的底层并发实现也是使用的J.U.C,并不是什么新物种,它的出现是为了避免直接使用J.U.C的原生API,在Reactor指南中有这样的一个例子:
private ExecutorService threadPool = Executors.newFixedThreadPool(8);
final List batches = new ArrayList();
Callable t = new Callable() { // *1
public T run() {
synchronized(batches) { // *2
T result = callDatabase(msg); // *3
batches.add(result);
return result;
}
}
};
Future f = threadPool.submit(t); // *4
T result = f.get() // *5
上面的代码是一个简单的使用线程池执行任务,然后阻塞等待任务执行结果的例子。Reactor指南中对于这段代码指出了下面的几个缺点:
- Callable 分配 -- 可能导致 GC 压力。
- 同步过程强制每个线程执行停-检查操作。
- 消息的消费可能比生产慢。
- 使用线程池(ThreadPool)将任务传递给目标线程 -- 通过 FutureTask 方式肯定会产生 GC 压力。
- 阻塞直至 callDatabase() 回调。
下面分析一下这些问题在Reacor中是否得到了完美的解决,或者不使用响应式编程,使用命令式编程有没有解决方案?
在Reator中执行任务也是需要创建对象,这一点没有得到很大的改善。
Reactor等响应式框架都是无状态的,所以这点可以规避。
Reactor使用背压解决这个问题,在命令式编程下没有太优雅的解决方案。
Reactor是流式处理数据,也有事件驱动模型,在发生阻塞时去执行其他任务,当等待的数据准备好后发送通知执行分配线程执行任务,不会出现阻塞等待。在命令式编程中,也可以通过封装回调来实现,JDK8中新引入的CompletableFuture能在一定场景解决这个问题,但是在命令编程下还是不可避免会发生阻塞问题。
响应式和非阻塞通常不会让应用执行的更快。(好比,一个人的胃就这么大,能同时处理的食物就那么多。)在某些情况下是可以的(比如,如果使用WebClient去并行执行远程调用)。总的来说,它需要更多的工作去做并且它会轻微的提升需要的执行时间。
响应式和非阻塞最主要的益处是用小的固定数量的线程和更小的内存。这让应用更有弹性在负载时,因为它们的规模更可预测。为了观察到这些好处,你需要一些延迟(比如,不可预测的网络延迟)。
传统的基于Servlet的Spring MVC,本质上就是阻塞和多线程。每个连接都会使用一个线程,在处理请求的时候,会在线程池中拉取一个工作者线程来处理请求。同时,请求线程是阻塞的,直到工作者线程将它唤醒。这样阻塞式的处理方式在大量请求下无法有效的扩展。如果工作者线程执行缓慢则会使情况更糟糕。响应式的Spring WebFlux使用事件轮询机制使用和CPU核心数相同的线程数处理请求,一个线程能处理很多请求,每次连接的成本更低。
如果是构建一个新项目完全可以尝试使用Spring WebFlux技术栈,它可能不会带来任何性能的提升,但是它能带来更好的伸缩性和高吞吐量。因为响应式编程在有较大延时的调用时会性能有显著的提升,可以在传统的Spring MVC中使用响应式的WebClient来进行小规模的切换,评估带来的益处。对于微服务应用存在大量的远程调用,使用响应式能够带来更大的益处。
感谢阅读,希望对你有帮助。
参考资料:
Spring官方文档
Reactor指南
《Spring实战》 第5版
《Reactive Programming 一种技术,各自表述》
如果觉得写得不错,请作者喝杯喜茶吧~