第二章 理解Reactive微服务和Vert.x

微服务并不是一个新的东西。它源自1970年代的研究,最近火了起来是因为微服务可以让我们更快速地改变、更方便地实现价值,提高灵活性。微服务源自actor-based系统、服务设计、自动化系统、domain驱动设计和分布式系统。它细粒度的模块化设计必然促使开发者们构建分布式系统。你肯定发现了,分布式系统很困难;因为它们容易出问题,运行缓慢,并且被CAP和FLP理论所限制。换句话说,它们的构建和运维都特别复杂。为了解决这个问题,reactive便出现了。

** 30多年的演变 **
Actor模型在1973年被 C. Hewitt, P. Bishop, and R.Steiger 提出。自主计算,一个在2001年创造的术语,用于描述分布式系统的自管理特性(自愈性、自由化性等)

但是,究竟什么是reactive?Reactive这个词如今被加上了新的含义。牛津词典定义reactive为“给刺激信号一个反馈”。因此,reactive程序会响应刺激信号,并且根据收到的信号来调整自己的行为。但是,这种响应性和适应性对编程来说是一种挑战,因为它们意味着计算流程不是被程序员而是刺激信号所控制。这一章,我们将会了解Vert.x是如何帮助你来实现reactive的:

  • Reactive编程——一种开发模型;其专注于数据流向、对变化的反馈,以及传播它们
  • Reactive系统——一种架构风格;其基于异步消息,来构建响应式的、鲁邦的分布式系统

Reactive微服务系统是由若干个reactive微服务组成。因为异步特点,微服务的实现具有挑战。Reactive编程可以减少其复杂性。现在我们来看下是如何做到的。

Reactive 编程

Figure2-1 Reactive编程关注于数据流及对其的响应

Reactive编程是一种开发模型,它以数据流和数据传播为驱动。在reactive编程中,刺激信号是数据的转移,叫做streams。有很多方法可以实现reactive编程模型,在本文中,我们使用Reactive Extensions (http://reactivex.io/) ;其中,streams被叫做observables,消费者对observables进行订阅并且对其值响应(如Figure 2-1所示)。

为了更具体得说明这个概念,我们来看一个例子;该例子使用了RxJava(https://github.com/ReactiveX/RxJava) ,一个实现了Reactive Extensions的java库。该例子位于代码目录的 reactive-programming 目录中。

例程1

在这个例子中,代码对一个Observable进行了观测(subscribe),当其值在数据流中发生转移时,代码会发现。订阅者可以接受三种类型的事件。onNext会在新值出现时被调用;onError会在流(stream)发生错误或者抛出异常时调用;onComplete会在流结束时被调用,而对于无边界流(unbounded stream)这种情况是不会发生的。RxJava包含一个运算集来生产、转移、协调Observable们,例如map可以将一个值转移到另一个中,flatMap可以产生一个Observable或者约束另一个异步动作,如下图所示:

例程2
  • Observables是有限或无限流,其包含一系列的值
  • Singles是只包含一个值的流,通常是一个运算(或操作)的异步结果,跟future或promise类似
  • Completables是不包含任何数据的流,不过其展示一个运算是否完成或已经失败

** RxJava 2 **
RxJava 2.x 最近已经发布了,但是本书中仍然使用了上一个版本(RxJava 1.x). 新的版本概念和老版本保持一致。2.x新增了两种stream。Observable表示不支持背压(back-pressure)的stream,而Flowable表示支持背压的Observable。2.x还引入了Maybe类型,表示可以包含0个或者1个值或者一个error的stream.

那我们可以用RxJava做什么呢?例如,我们可以用其描述一串异步行为并使其井井有条。假如你想下载一个文件,处理完毕,然后上传;下载和上传动作是异步的。为了实现这一串动作,你可以使用类似下面的代码:

例程3

你还可以管理和编排异步任务。例如,为了结合异步操作的处理结果,你可以使用zip操作符将两个不懂流的结果合并起来:

例程4

这些操作符给了你很强的power:你可以优雅地有条不紊地管理异步任务和数据流。那么这和reactive微服务有什么关系?回答这个问题,我们现在来了解以下reactive系统。

** Reactive Streams **
你或许已经听说过reactive流(http://www.reactivestreams.org/) 。Reactove流是为了给有背压异步流的处理一个标准。它提供了很小的一套接口和规范,用于描述非阻塞背压的异步数据流的处理;它并没有定义流操作符,而是主要用做一个互操作层(an interoperability layer). 这一倡议被很多组织支持,包括Netflix, Lightbend, and Red Hat等。

Reactive 系统

Reactive编程是一个开发模型,而reactive系统则是一种分布式系统的架构风格 (http://www.reactivemanifesto.org/) . 它是一套规范,用于实现响应性,使系统能够在出现错误或在压力之下,也能够及时地进行响应。

为了构建这样一个系统,reactive系统使用了消息驱动的方法。所有的构建通过异步消息的发送和接收来交互。为了结构发送者和接收者,构建都将消息发送到虚拟地址,同时也像虚拟地址注册以接收消息。地址(address)是消息目的地的代号,例如一个不透明字符串(opaque string)或一个URL. 若干个接收者可以注册到同一个地址;消息投递的逻辑由底层的实现决定。发送者不会阻塞着等待回复;它们可能会稍后才接收到回复,但是在此之前,它可以同样接收和发送。这个异步特征对你应用的实现尤其重要。

使用异步消息传递的交互方式,reactive系统会有两个重要的特征:

  • 伸缩性——可以横向伸缩(scale out/in)
  • 恢复性——可以处理错误并且恢复

伸缩性来自消息传递的解耦。消息被发送到一个地址之后,可以被一组消费者按照一种负载均衡方法消费。当reactive系统遇到负载高峰时,它可以创造出新的消费者,并在此之后销毁它们。

恢复性来自处理消息的非阻塞特征,以及每个组件和可替换特征。首先,这种消息交互模式允许组件在其本地处理错误;得益于异步特征,组件不需要等待消息,因此当一个组件发生错误时,其他组件仍然会正常工作。其次,可替换的特征是另一个关键因素;当一个处理消息的组件发生错误后,消息可以可以传递给在相同地址注册的其他组件。

这两个特点给系统带来了响应性这一特征。系统可以自己根据负载的轻重进行调整,并且在高负载或出现错误的情况下,依然可以响应请求。对于构建微服务系统和那些不受调用者控制的系统来说,这些特征是根本的需求;在不停止服务的情况下,能够添加若干节点来均衡负载或处理错误,对这些系统来说非常必要。下一节我们来看一下Vert.x是怎么解决这些问题的。

Reactive微服务

在构建一个微服务系统(也是分布式系统)的时候,每一个服务都可能发生变化、失败、变得缓慢,或者直接被收回。这些事件一定不能影响到整个系统的行为;你的系统必须能够拥抱变化和处理错误;它可以在次级的模式下运行,但是它必须能够保持响应请求。

为保证这一特性,reactive微服务系统是由reactive微服务组成的。这些微服务有下面四个特征:

  • 自治性
  • 异步性
  • 恢复性
  • 伸缩性

Reactive微服务是可自治的。他们可以根据周围的服务是否可用来调整自己的行为。自治性往往伴随着孤立性;Reactive微服务可以在本地处理错误、独立地完成任务,并在必要时和其他服务合作。它们使用异步消息传递的机制和其他服务沟通;它们也会接收消息并且对其作出回应。

得益于异步消息机制,reactive微服务可以处理错误并根据情况调整自己的行为。错误不会被扩散,而是在靠近错误源头的地方被处理掉。当一个微服务挂掉之后,它的消费者微服务要能够处理错误并避免扩散。这一孤立原则是避免错误逐层上浮而毁掉整个系统的关键。可恢复性不只是关于处理错误,它还涉及到自愈性;一个reactive微服务应该能够从错误中恢复并且对错误进行补救。

最后,reactive微服务必须是可伸缩的,这样系统才可以根据负载情况来调整节点数量。这一特性意味着将会有一系列的限制,比如不能有在内存中的状态,要能够在必要时同步状态信息,或者要能够将消息路由到状态信息相同的节点。

什么是Vert.x?

Vert.x是一个用于构建reactive和分布式系统的工具箱,其使用了异步非阻塞编程模型。因为它是一个工具箱而非框架,因此你可以像使用其他任何library一样使用Vert.x。它并不会限制你怎样架构你的系统,你可以任意使用。Vert.x非常灵活,你可以把它当做一个独立的程序,也可以嵌入其它更大的应用中。

从开发者的角度来说,Vert.x是一系列JAR包。每一个Vert.x模块都是一个JAR文件,你可以将其加入你的Classpath中。Vert.x提供了很多模块来帮你构建你想要的系统,例如HTTP服务端和客户端、消息模块、或者更底层的模块例如TCP和UDP等。你可以自由选择这些模块,连同Vert.x Core(Vert.x的核心模块)一起,来构建你的系统。下图完整展示了Vert.x的生态。

Figure 2-2 Vert.x生态系统

Vert.x还为构建微服务系统提供了很棒的工具。Vert.x在微服务流行之前就在推行这种方式。它的设计和实现都是为了提供一种构建微服务系统的、直观有效的方法。除此之外,你还可以使用Vert.x构建reactive的微服务;当使用Vert.x构建微服务的时候,微服务会自然地带上一个核心特征:所有事情都是异步的。

异步开发模式

用Vert.x构建的所有应用都是异步的,是事件驱动并且非阻塞的。当有趣的事件发生时,你的应用会被通知。我们来看一个实实在在的例子;Vert.x提供一种创建HTTP服务器的简单方式,这个HTTP服务器在每次接收到一个HTTP请求的时候被通知:


例程5

这个例子中,我们让一个requestHandler接收HTTP请求(事件)并且返回"hell Vert.x"。Handler是一个函数,当事件发生时,它会被调用。在我们的例子中,handler代码会在每次请求进来时被调用执行。要注意的是,Handler并不会返回一个结果,但是它可以提供一个结果;这个结果是怎样被提供的,这个要看是哪种交互行为。在上面的代码段中,它只是向一个HTTP response写入了结果。这个Handler后面跟了一个方法令其监听8080端口。调用这个HTTP服务它会返回一个简单的response:

例程6

除了极少数的例外,Vert.x的所有API都不会阻塞线程。如果结果可以被马上提供,那么就直接返回;否则,就会使用Handler来接收事件。当一个事件做好被处理的准备,或者异步操作的结果已经计算完成时,Handler就会被通知。

传统的编程方式中,你会写下如下的代码:

例程7

在这段代码中,你在等待计算结果。当换成异步非阻塞开发模型时,你会创建一个Handler,在结果计算完成时调用:

例程8

在上面代码中,compute函数不再返回一个结果,因此你不用等待结果计算完成或返回。你传给它一个Handler,在结果准备好的时候调用就可以了。

得益于这种非阻塞开发模型,你可以使用很少的线程处理高并发工作。绝大多数情况,Vert.x会用一个叫做event loop的线程来调用你的handler们。下图描述了Event loop,它消费一个事件队列,并且将事件分发到对应的Handler中。


Figure 2-3 消息循环线程

基于消息循环的线程模型有一个很大的优点:它简化了并发。因为只有一个线程存在,因此你永远都只被一个线程调用而不存在并发的情况。但是,同样它也有一个很重要的规矩,你一定要遵守:

Vert.x的黄金规则

不要阻塞消息循环

因为没有阻塞,一个消息循环线程可以短时间内分发巨量的事件,这个模式就叫做reactor模式(reactor pattern, https://en.wikipedia.org/wiki/Reactor_pattern).

我们想一下,如果你不遵守准则会怎样。在上面的代码段中,请求的handler都会一直从一个消息循环线程中被调用。所以,如果HTTP请求不是迅速响应用户而是阻塞,那么其他的请求就不能够及时处理,在队列中等待消息循环线程被释放。这样的话,你就失去了Vert.x的可扩展性和高效的优势。那么,究竟什么会被阻塞呢?最为明显的例子是JDBC数据库访问,它们天生就是会阻塞的。长时间的计算也会阻塞,例如,一段计算圆周率小数点后200000位数字的代码一定会阻塞。不要担心,Vert.x提供了处理阻塞性代码的工具。

在一个标准的reactor模型的实现中,只会有一个消息循环线程不停地将到来的消息分发到对应的handler中去。单线程问题很简单,它只能同时运行在一个CPU核上。Vert.x于此不同,每个Vert.x实例都维护了若干个消息循环线程,我们称此为multireactor模式,如下图所示。

Figure2-4 多reactor模式

图中,事件被发到不同的消息循环线程中。不过,如果一个Handler被一个消息循环线程调用,那么它就会一直被这个线程继续调用,以确保reactor模式的并发性。如果像上图一样,你有若干个消息循环,那么负载就可以分摊到不同的CPU核心上。那么在之前的HTTP例子中怎样体现呢?Vert.x会进行一次socket listener的注册,然后将请求分发到不同的消息循环队列中。

Verticles —— 构建的砖石

Vert.x给你提供了构建应用和代码的自由。它同时也为轻易实现应用提供所需的基础砖石,连同简单、可扩展、类似actor的部署方式和并发模型一起为我们所用。Verticles是被Vert.x部署和运行的代码块。一个如微服务的应用,是由运行在同一个Vert.x实例上的若干verticle组成的。一个verticle通常会创建服务器或客户端、注册一组Handler,以及封装一部分系统的业务处理逻辑。

标准的verticle会在Vert.x的消息循环中被执行,并且永远不会阻塞。Vert.x保证了每一个verticle都会只被同一个线程执行而不会有并发发生,从而避免同步工作。Java中,一个verticle是一个继承自AbstractVerticle的类:

例程 11

** Worker Verticle **
和标准的verticle不同,worker verticle不是在消息循环中执行的,这就意味着他们可以执行阻塞代码。但是,这会限制你的可扩展性。

Verticle可以访问vertx成员变量(是由AbstractVerticle类提供的)来创建服务器和客户端,以及和其他的verticle交互。Verticle还可以部署其他的verticle,对它们进行配置,并设置创建实例的数量。这些实例会和不同的消息循环线程绑定,Vert.x通过这些实例来均衡负载。

从Callbacks到Observables

如前面几节所讲,Vert.x开发模式使用回调方法。在组织管理多个异步动作时,这种基于回调的开发模式容易产生复杂的代码。例如,我们来看一下我们怎样从数据库中查询出数据。首先,我们需要和数据库简历连接,然后将查询请求发送给数据库,接着处理返回结果,最后释放连接。所有的这些步骤都应该是异步的,使用回调函数和Vert.x的JDBC客户端,你会写出类似下面这段代码:

例程12 回调函数实现数据库访问

虽然还可以维护,但是这个例子显示出回调函数会让代码迅速变得不可读。你还可以使用Vert.x的Future来处理异步操作。和Java的Future不同,Vert.x的Fureture是非阻塞的。Future提供高层的操作来构建一系列的动作或并发处理动作。典型地,就像上面的例程一样,我们使用future来构建一系列的异步动作:

例程13 Future构建数据库访问

虽然Future使得代码更加有自说明性,但是我们把所有数据行一次查询出然后再做处理。这样会导致查询结果巨大并且查询耗时严重。同时,你也不需要等所有数据查询出来之后才进行处理,我们可以拿到一行数据就处理一行。幸运的是,Vert.x提供了解决这个开发难题的答案,给你一套使用reactive编程开发模型来实现reactive微服务的方法。Vert.x提供RxJava API:

  • 结合和协调异步任务
  • 以输入流的方式响应接收的消息

我们使用RxJava API重写上面这段代码:

例程14-1

例程14-2

为了提高可读性,reactive编程允许你订阅结果的stream,在它们准备好时立刻处理。使用Vert.x你可以自行选择开发模型。在本书中,我们同时使用回调函数和RxJava.

让我们开始Coding!

是时候开始动手实践了。我们使用Apache Maven和Vert.x的Maven插件来开发第一个Vert.x应用,但是你可以使用你喜欢的任何工具(Gradle,Apache Maven,Apache Ant). 你可以在代码目录找到不同的例子(在packaging-examples目录中)。本节中展示出的代码都来自其中的hello-vertx目录。

创建项目

创建一个叫my-first-vertx-app的目录并且打开它:

mkdir my-first-vertx-app
cd my-first-vertx-app

然后,执行下面的命令:

mvn io.fabric8:vertx-maven-plugin:1.0.5:setup
-DprojectGroupId=io.vertx.sample
-DprojectArtifactId=my-first-vertx-app
-Dverticle=io.vertx.sample.MyFirstVerticle

这个命令会创建Maven项目结构,设置vertx-maven-plugin,并创建一个verticle类(io.vertx.sample.MyFirstVeticle),不过这个类是空的。

写你的第一个Verticle

现在你可以开始写你第一个verticle的代码了。修改类 src/main/java/io/vertx/sample/MyFirstVerticle.java 为下面的内容:

例程15

执行命令运行这个应用:

mvn compile vertx:run

如果一切正常的话,你可以在浏览器中打开 http://localhost:8080 看到你的应用。vertx:run命令会运行Vert.x应用并且监控代码的修改。因此,如果你编辑了代码,应用会自动重新编译并且启动。

下面我们看一下这个应用的输出:

Hello from vert.x-eventloop-thread-0

这个请求被消息循环线程0所处理。你可以尝试发出更多的请求,它们都会被同样的线程处理,以确保Vert.x的并发模型。使用Ctrl+C可以停止应用。

使用RxJava

到这个时候,我们可以看一下Vert.x提供的RxJava支持,以便更好的了解它。在你pom.xml文件中,添加如下的依赖:

例程16

然后,我们将<vertx.verticle>属性改为io.vertx.sample.MyFirstRXVerticle。这个属性告诉Vert.x Maven plugin哪一个verticle是程序的入口。接着创建新的类 io.vertx.sample.MyFirstRXVerticle,并输入如下的代码:

例程17

Vert.x API中的RxJava变量都来自带有rxjava字样的包。RxJava方法都会有rx作为前缀,例如rxListen。这些API还添加了一些方法来提供Observable实例,通过它们你可以订阅接收数据。

将你的应用打包成Fat Jar

Vert.x Maven plug-in 将应用打包到一个fat jar中。打包之后,你可以简单得执行 java -jar <name>.jar 来运行应用:

mvn clean package
cd target
java -jar my-first-vertx-app-1.0-SNAPSHOT.jar

应用又被启动,监听HTTP请求。使用Ctrl+C可以关闭它。

作为一个工具箱,Vert.x并不偏向哪一个打包方式,你可以自由选择你所喜欢的。例如,你可以使用fat jar,也可以引入其他目录作为library,将应用放入一个war包之中,或者在其他程序中调用。

在本书中,我们将会使用fat jar——包含了应用的代码、资源,以及所有的依赖;包括Vert.x和其依赖。这种打包方式使用flat class加载机制,可以让我们更容易理解应用的启动、依赖顺序,以及日志。更重要的是,它可以帮助减少运行程序所需要提前安装的那些模块。你不需要将应用部署在已有的应用服务器上。一旦打包成fat jar,你只要简单的java -jar <name>.jar命令即可在任何地方运行应用。Vert.x Maven plug-in可以帮你构建fat jar,不过你也可以使用其他的Maven plug-in,例如maven-shader-plugin.

日志、监控,及其他生产元素

使用fat jar是一种很棒的打包方式,它简化了微服务和其他应用的部署和运行。但是,那些应用服务器提供的已有的生产环境工具呢?通常,我们期望能够产生并收集日志,监控应用,增加额外配置,健康检查等等。

不要担心——Vert.x提供了所有这些功能。而且由于Vert.x是中立的,因此它提供了多种方案供你选择使用。例如,Vert.x并不推行某一种日志框架,而是任你使用自己所喜欢的,例如Apache Log4j 1/2,SLF4j,甚至JUL(the JDK logging API)。如果你对Vert.x所log的消息感兴趣,你可以为Vert.x内部的日志工具选择配置成上述的任意一种。Vert.x应用的监控是通过JMX来完成的。Vert.x Dropwizard Metric模块将Vert.x的监控数据提供给JMX。你也可以自己选择一种监控服务来处理这些数据,例如Prometheus(https://prometheus.io/) ,或CloudForms(https://www.redhat.com/en/technologies/management/cloudforms) 。

总结

这一章我们学习了reactive微服务和Vert.x相关的知识。并且你还创建了你第一个Vert.x应用。这一章并不是一个全面的阅读指南,只是快速地给你介绍了核心的概念。如果你想深究这些问题,可以参考下面的资源:

Reactive programming vs. Reactive systems
The Reactive Manifesto
RxJava website
Reactive Programming with RxJava
The Vert.x website

推荐阅读更多精彩内容