Reactive编程即反应式编程,随着这些年的发展已经逐步的进入了开发者的视野当中。早在2014年社区就有人发起响应式宣言,推动着Reactive的发展:
Published on September 16 2014. (v2.0) 来自不同领域的组织正在不约而同地发现一些看起来如出一辙的软件构建模式。它们的系统更加稳健,更加有可回复性,更加灵活,并且以更好的定位来满足现代的需求。 这些变化之所以会发生,是因为近几年的应用需求出现了戏剧性的变化。仅仅在几年之前,大型应用意味着数十台服务器,数秒的响应时间,数小时的离线维护时间以及若干 GB 的数据。而在今天,应用被部署在一切场合,从移动设备到基于云的集群,这些集群运行在数以千计的多核心处 理器的之上。用户期望毫秒级的响应时间以及 100% 的正常运行时间。数据则以 PB 为单位来衡 量。昨天的软件架构已经完全无法地满足今天的需求。 我们相信,一种条理分明的系统架构方法是必要的,而且我们相信关于这种方法的所有必要方面 已经逐一地被人们认识到:我们需要的系统是响应式的,具有可回复性的,可伸缩的,以及以消息驱动的。我们将这样的系统之为响应式系统。 以响应式系统方式构建的系统更加灵活,松耦合和可扩展。这使得它们更容易被开发,而且经得起变化的考验。它们对于系统失败表现出显著的包容性,并且当失败真的发生时,它们能用优雅 的方式去应对,而不是放任灾难的发生。响应式系统是高度灵敏的,能够给用户以有效的交互式的反馈。
Reactive构建的程序代表的是异步非阻塞、函数式编程、事件驱动的思想。
相比同步,异步的目的是提高硬件资源的利用率。同步模式下线程等待I/O时进入阻塞状态(Blocked)相当于闲置,异步则可以利用CPU同步等待I/O返回的时间以避免资源消耗,从而达到更大的并发量以及更低的响应时延。
在原生JDK中,juc包提供Future、Callable、FutureTask等相关类让我们完成最基本的异步编程。但其存在如下两个典型问题:
public Future>> getData();
为了避免阻塞我们可以引入回调(Callback),从而达到真正的异步,解决上面提出的第一个问题。但应用回调的同时也容易产生回调地狱(Callback Hell),层层嵌套的回调函数往往让人产生困惑,极大的降低了代码的可读性和可维护性,仍然不完美。
为了解决上文例子的第二个问题——异步带来的可读性差,可以用函数式编程思想:把函数逻辑组织成事件流,通过对事件的编排和组合,可以用清晰的代码接口来达到对事件返回的立即响应和对失败的立即响应(Fail Fast),构建起事件驱动(Event Driven)的程序。
假设模拟一个订单的场景,我们需要连续调用三个服务的接口:用户服务,订单服务,库存服务,并且每次调用都依赖于上次调用的返回结果。如果是传统的同步阻塞代码,我们需要连续等待每个接口的I/O调用返回。而应用Vert.x等Reactive框架我们可以写出类似如下伪代码:
//下订单调用
public Result order(long uid, Product product) {
userServer.getUserInfo(uid) //用户服务,查询用户信息
.compose(user->asyncAddOrder(user, product)) //订单服务,生成订单
.compose(order->asyncWmsOccupy(order)) //库存服务,库存占用
.setHandler(result->handle(result)); //处理结果
}
应用函数式编程范式,每个compose方法异步调用,结束后都会自动回调下一个compose,而任何错误都会立即响应到setHandler中同一处理,保证我们写出优雅的异步代码。
随着Reactive思想这些年来的发展,社区中逐渐涌现出一批优秀的Reactive开源框架:如RxJava,Vert.x等。包括最近比较热的Rsocket,在协议层面上进行reactive的实践。
2011年,在VMware工作的 Tim Fox 开始开发Vert.x,后来贡献给了Eclipse基金会。Vert.x提供了一系列的异步、流式工具,它更像一个工具包,使用Vert.x可以轻松的构建轻量级的Reactive web服务器。
Vert.x采用Actor的模型简化了多线程的编程。图灵奖得主、面向对象编程之父Alan Kay曾经这样描述面向对象:我在描述“面向对象编程”时使用了“对象”这个概念。很抱歉这个概念让许多人误入歧途,他们将学习的重心放在了“对象”这个次要的方面。真正主要的方面是“消息”。 几十年前, Carl Hewitt提出了 Actor 模型,将其作为在高性能网络中处理并行任务的一种方法。如今随着硬件条件的发展,产生了很多面向对象无法解决的挑战,Actor模型的思想重新进入人们视野,并以一种新的思路解决了多线程多cpu环境下的并发问题。
Vert.x是一个事件驱动的框架,底层使用Netty作为I/O库保证性能 。Vert.x采用了简单的并发模型,所有代码都运行在单线程中,避免多线程编程可能出现的并发问题、状态共享、以及各种锁的处理,同时每个actor模型独立天然符合分布式的部署和支持高可用(HA)。
与Node.js的Eventloop类似,在Vert.x的线程模型中,同样也有Eventloop的概念。但相比于单线程的Node.js,Vert.x设置了一个Eventloop线程池,来发挥多核处理器的性能。通过在代码中定义Verticle——一种actor的实现,使业务代码和Verticle绑定,而每个Verticle会绑定到一个Eventloop线程上,只要不阻塞Eventloop线程,就可以源源不断的处理新事件从而最大程度利用计算资源。当遇到耗时的长任务时则可以交给额外的Worker线程执行,避免Eventloop线程阻塞。基于Verticle来编写的业务代码始终运行在单线程中,加上基于回调机制可以实现天然的异步无锁。
Figure 1vert.x的actor模型
Figure 2tomcat的同步线程模型
Java Chassis 是 Apache Service Comb 的一个子项目,提供了开箱即用的Java微服务开发框架SDK,并在通信部分采用了基于Vert.x的Reactive架构。目前已经在华为应用市场业务中广泛应用,在生产实践中为高达60w tps的业务并发保驾护航。通过性能对比测试,业务采用Reactive异步模式之后,TPS提升 43% 左右、RT时延降低 28% 左右,CPU占用降低 56% 左右,性能收益很大。
使用Spring MVC作为web框架的Spring cloud,其底层实际上基于 Servlet 即采用DipatcherServlet来分发请求,需要运行在Tomcat等web容器中,性能受web容器限制。虽然 Servlet 3.1以后的版本提供了对异步和NIO的支持,但也阻止不了最近几年出现的一些新的Reactive web服务器试图取代Servlet的地位,它们往往更加轻量级和灵活。
Spring也在去年推出了Spring WebFlux作为新一代的Reactive web框架以期望在异步编程中代替Spring MVC,但WebFlux目前的主要应用场景还是在网关。虽然WebFlux抛弃了Servlet,自己基于netty重写了通信部分,但在实测中其性能提升并不明显。WebFlux与Spring cloud集成构建的微服务系统需要构建起完整的流式的应用才能发挥出其优势,代表着整个系统都要求是流式编程,对开发者的要求高,且系统重构的代价大。
Java Chassis 则采用基于Vert.x的reactive服务器以及客户端,更加轻量级、线程模型更加友好,同时提供了大量天然的异步API、以及更多的协议支持。同时基于actor的线程模型在简单的单服务间调用就可以表现出明显的性能优势,不需要开发人员构建起难以调试的流式系统,就可以享受Reactive带来的性能提升。
Java Chassis原生支持纯Reactive异步模型,在异步模式下同一个Enventloop线程中完成I/O以及业务的处理。同时,actor的模型下代表着更少的线程数量可以做到更优的性能:与Tomcat的默认200个线程相比,Vert.x默认只开启两倍cpu核数的Eventloop线程。从操作系统的计算成本角度上讲,更少的线程数量意味着更少的上下文切换时间开销——在线程切换时,操作系统从用户态切换为内核态再切回用户态。同时更少的线程数让内存中的线程私有栈信息也更少。
另外,Java Chassis也为同时提供对Tomcat的兼容,通过简单配置即可部署到Tomcat。
Figure 3基于vert.x的线程模型
笔者通过在4核8g的机器压测对比测试,数据结果显示:
微服务框架\客户端 |
RestTemplete |
rpc |
spring cloud huawei(webflux reactive) |
rt:12.75ms/qps:18.20k |
\ |
spring cloud huawei(tomcat) |
rt:13.93ms/qps:14.53k |
\ |
spring cloud 原生(tomcat) |
rt:14.09ms/qps:14.30k |
\ |
java chassis(tomcat) |
rt:13.67ms/qps:15.34k |
rt:11.88ms/qps:17.56k |
java chassis(vertx reactive) |
rt:6.77ms/qps:33.62k |
rt:5.67ms/qps:42.73k |
在调用链为单服务A->B时,对比Tomcat同步模式和切换到基于Vert.x的Reactive模式,平均时延和并发处理能力都有大幅度的提升。理论上在实际生产环境中如果服务调用链更长,服务中的同步操作越多,采用Reactive模式的优势会更大。详细测试代码以及数据已经开源 https://github.com/GuoYL123/ReactiveBenchmark,有兴趣的读者可以自行了解。
Reactive模式构建的程序,不仅仅代表的是高TPS低RT(时延),其带来的更直接的受益就是同等条件下更低廉的硬件成本。Java Chassis 作为Reactive在微服务领域的先行者,已经通过在生产环境中的应用积累了大量经验。
但同时构建Reactive模式的程序也为开发者带来更高的要求,面临比同步更为复杂的编程模型,需要更好的处理好阻塞和写出更优秀的异步代码。