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 等相关类让我们完成最基本的异步编程。但其存在如下两个典型问题:
获得返回结果依然需要阻塞 get。
大量应用 Future 写出类似如下代码,可读性差且不优雅。
为了避免阻塞我们可以引入回调(Callback),从而达到真正的异步,解决上面提出的第一个问题。但应用回调的同时也容易产生回调地狱(Callback Hell),层层嵌套的回调函数往往让人产生困惑,极大的降低了代码的可读性和可维护性,仍然不完美。
函数式编程:
为了解决上文例子的第二个问题——异步带来的可读性差,可以用函数式编程思想:把函数逻辑组织成事件流,通过对事件的编排和组合,可以用清晰的代码接口来达到对事件返回的立即响应和对失败的立即响应(Fail Fast),构建起事件驱动(Event Driven)的程序。
假设模拟一个订单的场景,我们需要连续调用三个服务的接口:用户服务,订单服务,库存服务,并且每次调用都依赖于上次调用的返回结果。如果是传统的同步阻塞代码,我们需要连续等待每个接口的 I/O 调用返回。而应用 Vert.x 等 Reactive 框架我们可以写出类似如下伪代码:
应用 Reactive 模式,每个 compose 方法异步调用,结束后都会自动回调下一个 compose,而任何错误都会立即响应到 setHandler 中同一处理,保证我们写出优雅的异步代码。
Vert.x:
随着 Reactive 思想这些年来的发展,社区中逐渐涌现出一批优秀的 Reactive 开源框架:如 RxJava(http:/reactivex.io/),Vert.x(https:/vertx.io/)等。
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 来编写的业务代码始终运行在单线程中,加上基于回调机制可以实现天然的异步无锁。
vert.x 的 actor 模型
tomcat 的同步线程模型
Java Chassis 的 Reactive 实践
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 容器中。虽然 Servlet 3.1 以后的版本提供了对异步和 NIO 的支持,但也阻止不了最近几年出现的一些新的 Reactive web 服务器试图取代 Servlet 的地位,它们往往更加轻量级也更加灵活。Spring 也在去年推出了 Spring WebFlux 作为新一代的 Reactive web 框架可以做到代替 Spring MVC,但目前的主要应用场景还是在网关,与 Spring cloud 集成后的表现并没有特别抢眼。
Java Chassis 则采用基于 Vert.x 的 reactive 服务器,更加轻量级、线程模型更加友好,还提供了大量天然的异步 API、以及更多的协议支持,同时性能表现也更加出色。
Java Chassis 原生支持纯 Reactive 异步模型,在异步模式下同一个 Enventloop 线程中完成 I/O 以及业务的处理。同时,actor 的模型下代表着更少的线程数量可以做到更优的性能:与 Tomcat 的默认 200 个线程相比,Vert.x 默认只开启两倍 cpu 核数的 Eventloop 线程。从操作系统的计算成本角度上讲,更少的线程数量意味着更少的上下文切换时间开销——在线程切换时,操作系统从用户态切换为内核态再切回用户态。同时更少的线程数让内存中的线程私有栈信息也更少。
另外,Java Chassis 也为一些特殊需求的开发者提供了对 Tomcat 的兼容,通过简单配置即可部署到 Tomcat。
笔者通过在 4 核 8g 的机器压测对比测试,数据结果显示:
在调用链为单服务 A->B 时,对比 Tomcat 同步模式和切换到基于 Vert.x 的 Reactive 模式,平均时延和并发处理能力都有大幅度的提升。理论上在实际生产环境中如果服务调用链更长,服务中的同步操作越多,采用 Reactive 模式的优势会更大。详细测试代码以及数据已经开源,有兴趣的读者可以自行了解。
结论
Reactive 模式构建的程序,不仅仅代表的是高 TPS 低 RT(时延),其带来的更直接的受益就是同等条件下更低廉的硬件成本。Java Chassis 作为 Reactive 在微服务领域的先行者,已经通过在生产环境中的应用积累了大量经验。
但同时构建 Reactive 模式的程序也为开发者带来更高的要求,面临比同步更为复杂的编程模型,需要更好的处理好阻塞和写出更优秀的异步代码。
评论 1 条评论