在这一章节里,我们将用Vert.X构建我们的第一个微服务。像大多数采用http交互的微服务系统,我们打算从http微服务开始。因为系统由多个互相通信的微服务构成,我们将构建另一个微服务,它作为第一个微服务的消费者。然后,我们将展示为何这样的设计并不完全符合响应式微服务。最后,我们将实现基于消息的微服务,看看消息是怎样提升了响应性。
第一个微服务
在这一章节,我们打算实现同类的微服务两个。第一个微服务暴露一个hello服务,我们称它为hello微服务。另一个消费这个服务两次(并发地)。消费者将被称为hello消费者微服务。这个小系统不仅展示了一个服务是怎样提供服务的,而且展示它是怎样被消费的。在图3-1的左边,微服务用http交互,hello消费者微服务作为http客户端向hello微服务发请求;在图的右边,hello消费者微服务用消息与hello微服务交互。这个不同影响了系统的响应性。
在前一章节,我们看到两种不同的方式使用Vert.X API: 回调和RxJava。展示它们的不同有助于你发现更佳途径,hello微服务是使用基于回调开发模式实现,而hello消费者微服务是用RxJava实现。
实现http微服务
微服务通常通过http暴露他们的API,通过http请求来消费。让我们看看用Vert.X怎样实现这些http交互。这个部分开发的代码在代码仓库的microservices/hello-microservice-http目录下可获得。
开始
创建hello-microservice-http目录,然后生成工程结构:
mkdir hello-microservice-http
cd hello-microservice-http
mvn io.fabric8:vertx-maven-plugin:1.0.5:setup \
-DprojectGroupId=io.vertx.microservice \
-DprojectArtifactId=hello-microservice-http \
-Dverticle=io.vertx.book.http.HelloMicroservice \
-Ddependencies=web
这个命令生成maven工程,配置Vert.X Maven插件。另外,它加上vertx-web依赖。Vert.X Web是一个模块,它提供你基于Vert.X构建流行的web应用的一切。
Verticle
打开src/main/java/io/vertx/book/http/HelloMicroservice.java,这个被生成的verticle代码没做什么很有趣的事,但它是一个起点:
package io.vertx.book.http;
import io.vertx.core.AbstractVerticle;
public class HelloMicroservice extendsAbstractVerticle {
@Override
publicvoid start() {
}
}
现在,执行下面的maven命令:
mvn compile vertx:run
你现在可以编辑verticle,每次你保存文件后,应用将被重新编译并自动重启。
http微服务
是时候让MyVerticle做点什么了。让我们启动一个http server。正如你前面章节看到的,用Vert.X创建一个http server仅仅:
@Override
public void start() {
vertx.createHttpServer()
.requestHandler(req-> req.response().end("hello"))
.listen(8080);
}
一旦加上这些代码并保存,在浏览上访问http://localhost:8080你应该能看到hello。这段代码创建一个http server监听端口8080,注册了一个请求处理器,每一个http请求进来时它会被调用。现在,我们仅仅输出hello到http响应。
使用路由和参数
许多服务是通过web url调用的,因此,检查路径是重要的,以知道请求在要求什么。然而,在请求处理器里面做路径检查以实现不同的动作可能会变得复杂。幸运地,Vert.X Web提供了一个路由器,通过它你可以注册路由。路由是Vert.X Web检查路径、调用相关动作的机制。让我们重写start方法,用两个路由:
@Override
public void start() {
Routerrouter = Router.router(vertx);
router.get("/").handler(rc-> rc.response().end("hello"));
router.get("/:name").handler(rc-> rc.response().end("hello " + rc.pathParam("name")));
vertx.createHttpServer()
.requestHandler(router::accept)
.listen(8080);
}
我们创建了路由器对象后,我们注册了两个路由:第一个处理根路径的请求仅仅输出hello,第二个路由有一个路径参数(:name),处理器追加参数值到欢迎中。最后,我们更改请求处理器(requestHandler),使用路由器的accept方法。
如果你没有停止vertx:run,你打开浏览器:
访问http://localhost:8080,你应该会看到hello
访问http://localhost:8080/vert.x,你应该会看到hello vert.x
生成JSON
在微服务里,JSON是常用的。让我们修改前一个类,生成JSON:
@Override
public void start() {
Routerrouter = Router.router(vertx);
router.get("/").handler(this::hello);
router.get("/:name").handler(this::hello);
vertx.createHttpServer()
.requestHandler(router::accept)
.listen(8080);
}
private void hello(RoutingContext rc) {
Stringmessage = "hello";
if(rc.pathParam("name") != null) {
message+= " " + rc.pathParam("name");
}
JsonObjectjson = new JsonObject().put("message", message);
rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE,"application/json")
.end(json.encode());
}
Vert.X提供一个JsonObject类来创建和操作JSON。放上这段代码,你打开浏览器:
访问http://localhost:8080,你应该会看到{“message”:“hello”}
访问http://localhost:8080/vert.x,你应该会看到{“message”: “hello vert.x”}
打包和运行
按CTRL+C,停止vertx:run的执行,在同一目录下执行下面的命令:
mvn package
这生成一个fat jar在target目录下:hellomicroservice-http-1.0-SNAPSHOT.jar。fat jar之所以胖,因为jar包有一个合理的大小(约6.3MB),包含了运行应用所需的一切:
java -jar target/hello-microservice-http-1.0-SNAPSHOT.jar
你可以通过访问http://localhost:8080来检查确定它是运行起来的。保持住运行,因为下一个微服务将调用它。
消费http微服务
一个微服务不构成一个应用,你需要一个微服务系统。现在我们有了一个运行中的微服务,让我们写第二个微服务来消费它。第二个微服务也提供了一个http请求接口,每一个请求会调用我们刚刚实现的微服务。这个章节展示的代码可以从代码仓库的microservices/helloconsumer-microservice-http目录获得。
创建工程
一样地,让我们创建一个新工程:
mkdir hello-consumer-microservice-http
cd hello-consumer-microservice-http
mvn io.fabric8:vertx-maven-plugin:1.0.5:setup
\
-DprojectGroupId=io.vertx.microservice \
-DprojectArtifactId=hello-consumer-microservice-http \
-Dverticle=io.vertx.book.http.HelloConsumerMicroservice \
-Ddependencies=web,web-client,rx
最后的命令增加了其它的依赖:Vert.X web客户端,异步的http客户端。我们将使用这个客户端来向第一个微服务发请求。这个命令也增加的Vert.X RxJava绑定,我们打算在后面使用它。
现在编辑src/main/java/io/vertx/book/http/HelloConsumerMicroservice.java文件,更改它的内容:
package io.vertx.book.http;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.*;
import io.vertx.ext.web.client.*;
import io.vertx.ext.web.codec.BodyCodec;
public class HelloConsumerMicroservice extends AbstractVerticle {
private WebClientclient;
@Override
public void start(){
client = WebClient.create(vertx);
Routerrouter = Router.router(vertx);
router.get("/").handler(this::invokeMyFirstMicroservice);
vertx.createHttpServer()
.requestHandler(router::accept)
.listen(8081);
}
private voidinvokeMyFirstMicroservice(RoutingContext rc) {
HttpRequestrequest = client
.get(8080,"localhost","/vert.x")
.as(BodyCodec.jsonObject());
request.send(ar-> {
if(ar.failed()) {
rc.fail(ar.cause());
} else {
rc.response().end(ar.result().body().encode());
}
});
}
}
在start方法,我们创建一个WebClient,一个Router,我们注册了一个根路径的route,启动http server,传递router的accept方法给requestHandler。这个方法用web客户端来调用第一个微服务的指定路径(/vert.x),输出结果到http响应。
一旦http请求被创建,我们调用send方法来发出请求,无论是响应返回或者是有错误发生,我们设定的处理器会被调用。If-else块检查请求成功与否。不要忘了这是一个远程交互,有很多原因导致失败。例如,第一个微服务可能没有运行。当它成功时,我们输入收到的数据到响应,否则,我们返回一个500的http响应。
多次调用服务
现在让我们改变当前的行动,用两个不同的路径参数请求hello微服务两次:
HttpRequest request1 = client
.get(8080, "localhost", "/Luke")
.as(BodyCodec.jsonObject());
HttpRequest request2 = client
.get(8080, "localhost", "/Leia")
.as(BodyCodec.jsonObject());
这两个请求是独立的,能够并发地执行。可是这里我们想输出一个把这两个请求的结果装配起来的响应。需要调用两次服务、把两个结果装配起来的代码可以变得复杂。当我们接收到其中一个响应时,我们需要检查另一个请求完成与否。当然,对于两个请求,这个代码仍然是可管理的。当我们需要处理更多的时候,它变得极其复杂。幸运地,正如前一章节所讲,我们能够使用响应式编程,RxJava使代码变得简单。
我们介绍了vertx-mavan-plugin插件引入Vert.X RxJava API。在HelloConsumerMicroservice里,我们替换重要的import语句:
import io.vertx.core.json.JsonObject;
import io.vertx.rxjava.core.AbstractVerticle;
import io.vertx.rxjava.ext.web.*;
import io.vertx.rxjava.ext.web.client.*;
import io.vertx.rxjava.ext.web.codec.BodyCodec;
import rx.Single;
用RX, 我们要写的调用两个请求、构造他们的结果成一个响应的复杂代码变得比较简单:
private voidinvokeMyFirstMicroservice(RoutingContext rc) {
HttpRequestrequest1 = client
.get(8080,"localhost", "/Luke")
.as(BodyCodec.jsonObject());
HttpRequestrequest2 = client
.get(8080,"localhost", "/Leia")
.as(BodyCodec.jsonObject());
Singles1 = request1.rxSend().map(HttpResponse::body);
Singles2 = request2.rxSend().map(HttpResponse::body);
Single.zip(s1,s2, (luke, leia) -> {
//We have the results of both requests in Luke and Leia
returnnew JsonObject()
.put("Luke",luke.getString("message"))
.put("Leia",leia.getString("message"));
})
.subscribe(
result-> rc.response().end(result.encodePrettily()),
error-> {
error.printStackTrace();
rc.response()
.setStatusCode(500).end(error.getMessage());
}
);
}
注意rxSend方法调用。Vert.X里,RxJava方法加上了rx前缀,以更容易识别。rxSend方法的结果是一个Single,可订阅的单个元素,代表一个操作的延期结果,single.zip方法用一组Single作为参数,一旦所有的Single收到它们的值,就用这些值来调用一个函数。最后,订阅(subscribe),这个方法用两个函数作为参数:
第1个函数是用zip函数的结果(一个json对象)作为参数被调用,我们输出接收到的json内容到http响应;
第2个函数是某种失败(超时,异常等等)发生时被调用,在这里,我们用空的json对象做出响应。
这段代码生效后,hello微服务仍在运行,如果我们打开http://localhost:8081我们应该会看到:
{
"Luke" : "hello Luke",
"Leia" : "hello Leia"
}
这是响应式微服务吗?
现在我们有两个微服务。他们是可独立部署和修改的。他们也使用轻量级的http协议交互。但是他们是响应式微服务吗?不,他们不是。记住,响应式微服务必须是:
. 自治的
. 异步的
. 可恢复的
. 弹性的
当前设计的主要问题是两个微服务是紧耦合的。Web客户端是显示地配置了第一个微服务的地址。如果第一个微服务失败了,我们不能够通过请求另一个来恢复服务。我们想降低负载,创建一个新的hello微服务实例将帮助不了我们。感谢Vert.X web客户端,交互是异步的。然而,我们没有用一个虚拟的目标地址来调用微服务、而是直接用它的URL,这不能提供我们需要的可恢复性和弹性。
不要失望,在下一章节我们将朝响应式微服务迈出一大步。
Vert.X事件总线---一个消息后端
Vert.X提供了事件总线,允许一个应用的不同组件用消息来交互。消息被送到地址,有消息头和消息体。一个地址是一个字符串,代表一个目标地址。消息消费者注册它们自己到这个地址、以接收消息。事件总线也是集群的,意味着它能够跨越网络、在分布的发送者和消费者之间传递消息。以集群模式启动Vert.X应用,被连接的节点可以共享数据结构、做停止失败检查、负载均衡。事件总线能够在集群的所有节点间传递消息。为了创建这样一个集群,你可以用Apache Ignite、Apache Zookeeper、Infinispan或者是Hazelcast。在这本书里,我们打算用Infinispan,但是,我们不做高级的配置。如果需要,可参考Infinispan文档(http://infnispan.org/)。Infinispan(或者你选的其他技术)管理节点的发现和存储。事件总线用直接的p2p
tcp连接通讯。
事件总线提供了三种传递语法:第一种,send方法允许一个组件送一个消息到一个地址,
单个消费者将接收它。假如不止一个消费者注册到这个地址,Vert.X将采用轮询策略来选择一个消费者:
// Consumer
vertx.eventBus().consumer("address",message -> {
System.out.println("Received:'" + message.body() + "'");
});
// Sender
vertx.eventBus().send("address","hello");
与send不同,你可以用publish方法传递消息给所有注册在这个地址的消费者。
最后,send方法能够带一个应答处理器,这个请求/响应机制允许两个组件间实现基于消息的异步交互:
// Consumer
vertx.eventBus().consumer("address",message -> {
message.reply("pong");
});
// Sender
vertx.eventBus().send("address","ping", reply -> {
if(reply.succeeded()) {
System.out.println("Received:" + reply.result().body());
}else {
//No reply or failure
reply.cause().printStackTrace();
}
});
如果你用RX API,你能够用rxSend方法,它返回一个Single,当应用被收到时这个Single接收一个值。我们将很快看到这个方法。
基于消息的微服务
让我们重新实现hello微服务,这次用事件总线代替http server来接收请求。微服务应答消息、提供响应。
创建工程
让我们创建一个新工程。这次我们将加上Infnispan依赖,一个内存数据网格,被用来管理集群:
mkdir hello-microservice-message
cd hello-microservice-message
mvn io.fabric8:vertx-maven-plugin:1.0.5:setup
\
-DprojectGroupId=io.vertx.microservice \
-DprojectArtifactId=hello-microservice-message \
-Dverticle=io.vertx.book.message.HelloMicroservice \
-Ddependencies=infinispan
一旦生成后,为了构建集群,我们需要配置Infinispan。缺省的配置是用组播的方式来发现节点。
如果你的网络支持组播,就可以。否则,检查代码仓库的resource/cluster目录。
写消息驱动的Verticle
编辑src/main/java/io/vertx/book/message/HelloMicroservice.java文件,修改start方法:
@Override
public void start() {
// Receive messagefrom the address 'hello'
vertx.eventBus().consumer("hello",message -> {
JsonObject json =new JsonObject()
.put("served-by",this.toString());
// Check whether wehave received a payload in the
// incoming message
if (message.body().isEmpty()){
message.reply(json.put("message","hello"));
} else {
message.reply(json.put("message","hello" + message.body()));
}
});
}
这段代码从vertx对象获取事件总线(eventBus),注册一个消费者到地址hello。当接收到一个消息时,它应答它。取决于进来的消息是否有一个空的消息体,我们给以不同的响应。像前面章节的例子一样,我们返回一个json对象。你可能想知道为什么我们在json里面加了served-by。你很快就会明白为什么。现在verticle写好了,是时候启动它:
mvn compile vertx:run \
-Dvertx.runArgs="-cluster -Djava.net.preferIPv4Stack=true"
-cluster选项告诉Vert.X以集群模式启动。
现在让我们写一个微服务来消费这个服务。
初始化基于消息的交互
在这一节里,我们将创建另一个微服务来调用hello微服务,通过送一个消息到hello地址并获得应答。微服务将重新实现与前一章节同样的逻辑,调用服务两次。
同样地,让我们创建一个新的工程:
mkdir hello-consumer-microservice-message
cd hello-consumer-microservice-message
mvn io.fabric8:vertx-maven-plugin:1.0.5:setup
\
-DprojectGroupId=io.vertx.microservice \
-DprojectArtifactId=hello-consumer-microservice-message \
-Dverticle=io.vertx.book.message.HelloConsumerMicroservice \
-Ddependencies=infinispan,rx
这里我们也加了Vert.X RxJava以便于使用Vert.X提供的RX API。如果在前一节中你更改了Infinispan的配置,你需要拷贝它到这个新工程。
现在编辑io.vertx.book.message.HelloConsumerMicroservice。因为我们打算用RxJava,改变相应的引入语句为io.vertx.rxjava.core.AbstractVerticle,然后实现start方法:
@Override
public void start() {
EventBus bus =vertx.eventBus();
Singleobs1 = bus
.rxSend("hello","Luke")
.map(Message::body);
Singleobs2 = bus
.rxSend("hello","Leia")
.map(Message::body);
Single.zip(obs1,obs2, (luke, leia) ->
newJsonObject()
.put("Luke",luke.getString("message"))
.put("Leia",leia.getString("message"))
)
subscribe(x ->System.out.println(x.encode()), Throwable::printStackTrace);
}
这段代码与前一章节是很类似的,替代用WebClient请求http,我们用事件总线发送消息到hello地址、获取应答内容。我们用zip操作获得两个响应并且构建最终的结果。在subscribe方法,我们打印最终结果到控制台或者是输出异常堆栈。
让我们把这段代码和http server合并在一起,当接收到http请求时,我们调用hello服务两次、把构建结果作为响应返回:
@Override
public void start() {
vertx.createHttpServer()
.requestHandler(req-> {
EventBus bus =vertx.eventBus();
Singleobs1 = bus
.rxSend("hello","Luke")
.map(Message::body);
Singleobs2 = bus
.rxSend("hello","Leia")
.map(Message::body);
Single.zip(obs1,obs2, (luke, leia) ->
newJsonObject()
.put("Luke",luke.getString("message") + " from " +luke.getString("served-by"))
.put("Leia",leia.getString("message") + " from " +leia.getString("served-by"))
)
.subscribe(
x ->req.response().end(x.encodePrettily()),
t -> {
t.printStackTrace();
req.response().setStatusCode(500).end(t.getMessage());
}
);
})
.listen(8082);
这段代码仅仅是打包事件总线的交互到请求处理器(requestHandler)并且处理http响应。在失败的情况下,我们返回一个包含错误信息的json对象。
如果你运行这段代码用
mvn compile vertx:run -Dvertx.runArgs="-cluster-Djava.net.preferIPv4Stack=true"
打开你的浏览器访问http://localhost:8082,你应该会看到像这样:
{
"Luke" : "hello Luke from ...HelloMicroservice@39721ab",
"Leia" : "hello Leia from ...HelloMicroservice@39721ab"
}
现在是响应式吗?
这段代码与我们前面写的基于http的微服务很类似,唯一的不同是我们用事件总线代替http。这改变了响应性?的确是,让我们看看为什么。
弹性
弹性是http版本的微服务没有的一个特性。因为微服务是被定位到一个指定的微服务实例(用硬编码的URL),它没有提供我们需要的弹性。但是我们现在采用送到一个地址的消息,这改变了游戏。让我们看看这个微服务系统的表现。
记得前面执行的输出。返回的json对象显示verticle有处理hello消息。输出总是显示是同一个verticle。这个信息表明是同一个实例。我们预计这是因为我们只有一个实例在运行。现在让我们看看用两个实例将发生什么。
停止hello微服务的vertx:run,运行:
mvn clean package
然后,打开两个不同的终端,在hello-microservice-message目录里执行下面的命令:
java -jar target/hello-microservice-message-1.0-SNAPSHOT.jar \
--cluster -Djava.net.preferIPv4Stack=true
这将启动两个Hello微服务实例,返回到你的浏览器刷新页面你应该看到类似这样:
{
"Luke" : "hello Luke from...HelloMicroservice@16d0d069",
"Leia" : "hello Leia from...HelloMicroservice@411fc4f"
}
两个Hello实例被调用。Vert.X集群连接不同的节点,事件总线也是被集群的。感谢事件总线轮循,Vert.X事件总线分发消息到可用的实例、在监听同一地址的不同节点间均衡负载。
因此,通过用事件总线,我们有了我们所需要的弹性特征。
可恢复性
可恢复性又如何呢?在当前的代码中,如果hello微服务失败了,我们将得到一个失败、执行这个代码:
t -> {
t.printStackTrace();
req.response().setStatusCode(500).end(t.getMessage());
}
尽管用户得到了错误信息,我们没有崩溃,我们没有限制伸缩性,仍然能够处理请求。然而,为了提升用户体验,我们应该总是在适当的时间内响应,即使我们没有从服务中接收到响应。实现这个逻辑,我们可以用timeout增加代码。
为了展示,让我们修改hello微服务、注入失败。这段代码放在代码仓库的microservices/hello-microservice-faulty目录下。
这个新的start方法随机地选择3个策略中的一个:1. 用一个显示的失败响应,2.忘了响应,3.发送正确的结果:
@Override
public void start() {
vertx.eventBus().consumer("hello", message-> {
double chaos = Math.random();
JsonObject json = new JsonObject().put("served-by",this.toString());
if (chaos < 0.6) {
// Normal behavior
if (message.body().isEmpty()) {
message.reply(json.put("message", "hello"));
} else {
message.reply(json.put("message", "hello "+message.body()));
}
} else if (chaos < 0.9) {
System.out.println("Returning a failure");
// Reply with a failure
message.fail(500, "message processing failure");
} else {
System.out.println("Not replying");
// Just do not reply, leading to a timeout on the
// consumer side.
}
});
}
重新打包并重启两个hello微服务的实例。
使用这个故障注入的服务,我们需要改进消费方的容错性。事实上,消费方可能得到超时或是接收到一个显示的失败。在hello消费者微服务里,改变请求hello服务的代码为:
EventBus bus = vertx.eventBus();
Single obs1 = bus
.rxSend("hello", "Luke")
.subscribeOn(RxHelper.scheduler(vertx))
.timeout(3, TimeUnit.SECONDS)
.retry()
.map(Message::body);
Single obs2 = bus.
rxSend("hello", "Leia")
.subscribeOn(RxHelper.scheduler(vertx))
.timeout(3, TimeUnit.SECONDS)
.retry()
.map(Message::body);
这段代码放在代码仓库的microservices/hello-consumer-microservice-timeout目录下。如果有给定的时间内没有接收到响应,timeout方法发出一个失败。如果得到一个超时失败或是一个显示的失败,retry方法将试图重试去获取值。subScribeOn方法指明请求需要在哪一个线程上执行。我们用Vert.X事件轮循器来调用callback。没有指定的话,方法将被从缺省的RxJava线程池中取一个线程来执行,破坏了Vert.X的线程模式。RxHelper类是Vert.X提供的。盲目地重试服务调用不是明智的容错策略,它甚至可能是有害的。下一章节阐述不同的方法。
现在你可以重新加载页面。你总能获得一个结果,即使是失败或者超时。记住当调用服务时线程是不阻塞的,因此,你总是能够接收新的请求、在一个合适的时间内响应。然而,超时重试经常是有害而不是有益,正如我们在下一章节将看到的那样。
小结
在这一章节,我们学习了怎样用Vert.X开发一个http微服务,怎样消费它。正如我们所学的,在代码里硬编码被消费服务的URL不是一个明智的主意,因为它破坏了响应式特征之一。在第二部分,我们用消息替换http交互,这展示了消息和Vert.X事件总线怎样构建响应式微服务。
那么,现在是yes还是no。是的,我们知道怎样构建响应式微服务,但是,这里仍然有一些我们需要关注的缺点:首先,如果你仅仅有http服务,你怎样避免硬编码位置?可恢复性呢?在这一章节我们已经看到了超时和重试,但是熔断器(circuit breaker)、故障转移(failover)、隔仓(bulkhead)呢?让我们继续我们的旅程。
如果你想更深入这些topic:
. Vert.X Web文档(http://vertx.io/docs/vertx-web/java)
. Vert.X Web客户端文档(http://vertx.io/docs/vertx-web-client/java)
. Vert.X响应式微服务