使用vertx构建响应式微服务-第三章 创建响应式微服务(Building Reactive Microservices)

在这一章我们将用Vert.x创建我们的第一个微服务。由于大多数微服务使用HTTP交互,我们也从学习制作HTTP微服务开始。
但是, 由于系统包含多个通信 microservices, 我们将构建另一个 microservice, 它将消耗第一个。

然后, 我们将展示为什么这样的设计不完全 被响应式 microservices接受。

最后, 我们将实现基于消息的 microservices, 以了解消息传递如何改进 reactiveness。


第一个微服务

我们创建一个hello service,再创建一个消费者来消费hello service.这个小的系统可以帮助我们理解服务是怎么提供和怎么被消费的。

左边使用HTTP,右边使用Message

在前一章,我们看到了两种使用Vert.x的方式,一种是回调,一种是RxJava.

为了让你找你喜欢的风格,我们的生产者使用回调(callbacks),消费者使用RxJava

实现HTTP微服务

Microservices 经常通过 http 公开其 API, 并使用 http 请求来消费。让我们来看看如何使用Vert.x来实现这些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

我把项目导入了eclipse,导入maven项目到eclipse可以查看我的另一篇教程。

mvn compile vertx:run

HTTP微服务

@Override 
public void start() {     
vertx.createHttpServer()
        .requestHandler(req -> req.response()
          .end("hello"))         .listen(8080);
}

修改以后就可以在http://localhost:8080/看到hello

以后修改都会自动发布,只用刷新一下浏览器就行了。

使用路由和参数

许多服务通过 web url 调用, 因此检查路由对于了解请求要求是至关重要的。

但是, 在 requestHandler 中进行路由检查以实现不同的操作可能会变得复杂。幸运的是, Vert.x Web提供了一个路由器, 我们可以在上面注册路由。路由是Vertx.x 站点检查路径并调用关联操作的机制。让我们重写开始方法, 有两个路由:

@Override
public void start() {
    Router router = 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);
}

当我们输入http://localhost:8080 会返回 hello

当我们输入http://localhost:8080/name 会返回 hello name

产生JSON

我们再次修改

@Override
public void start() {
    Router router = 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) {
    String message = "hello";     if (rc.pathParam("name") != null) {         
message += " " + rc.pathParam("name");
    }
    JsonObject json = new JsonObject().put("message", message);
    rc.response()
        .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")         
.end(json.encode());
}

当我们输入http://localhost:8080/name 会返回 {"message":"hello name"},返回的是json数据

打包和运行

关闭运行的vertx

并打包 mvn clean package

cd target

浏览器打开http://localhost:8080/world显示{"message":"hello world"}

消费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

最后一行,web-client这个依赖可以帮助我们调用刚才的微服务,rx依赖可以让我们用RxJava编程。

修改src/main/java/io/vertx/book/http/HelloConsumer Microservice.java

public class HelloConsumerMicroservice extends AbstractVerticle {     private WebClient client;
    @Override     public void start() {         client = WebClient.create(vertx);
        Router router = Router.router(vertx);         router.get("/").handler(this::invokeMyFirstMicroservice);
        vertx.createHttpServer()
            .requestHandler(router::accept)             .listen(8081);
    }
    private void invokeMyFirstMicroservice(RoutingContext rc) {
        HttpRequest<JsonObject> request = 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());
            }
        });
    }
}
我们输入http://localhost:8081/程序会自动调用http://localhost:8080/vert.x然后把结果显示出来
{"message":"hello vert.x"}
HttpRequest<JsonObject> request1 = client
    .get(8080, "localhost", "/Luke")
    .as(BodyCodec.jsonObject());
HttpRequest<JsonObject> request2 = client
    .get(8080, "localhost", "/Leia")
    .as(BodyCodec.jsonObject());

这两个请求是独立的, 可以并发执行。但在这里, 我们要写一个反应, 组装两个结果。调用该服务两次并组合两个结果所需的代码会变得错综复杂。我们需要检查是否其他请求已完成时, 我们收到的答复之一。虽然此代码仍可用于两个请求, 但在需要处理更多操作时, 它变得过于复杂。幸运的是, 正如上一章所指出的, 我们可以使用被动编程和 RxJava 来简化代码。

把引用改为

import io.vertx.rxjava.core.AbstractVerticle;
import io.vertx.core.json.JsonObject;
import io.vertx.rxjava.ext.web.*;
import io.vertx.rxjava.ext.web.client.*;
import io.vertx.rxjava.ext.web.codec.BodyCodec;
import rx.Single;

修改方法

private void invokeMyFirstMicroservice(RoutingContext rc) {
	    HttpRequest<JsonObject> request1 = client
	        .get(8080, "localhost", "/Luke")
	        .as(BodyCodec.jsonObject());
	    HttpRequest<JsonObject> request2 = client
	        .get(8080, "localhost", "/Leia")
	        .as(BodyCodec.jsonObject());
	    Single<JsonObject> s1 = request1.rxSend()
	      .map(HttpResponse::body);    
 Single<JsonObject> s2 = request2.rxSend()       .map(HttpResponse::body);
	    Single
	        .zip(s1, s2, (luke, leia) -> {
	            // We have the results of both requests in Luke and Leia
	            return new 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());
	            }
	        );
	}

当两个请求都收到结果时会调用single.zip方法把两个结果合并。

{
  "Luke" : "hello Luke",
  "Leia" : "hello Leia"
}

这些微服务是响应式微服务吗?
在这一点上, 我们有两个 microservices。它们是独立的, 可以按自己的速度部署和更新。它们还使用轻量级协议 (HTTP) 进行交互。

但是他们是响应式微服务吗?不,他们绝对不是。记住,响应式微服务有以下特点:


•   Autonomous

•   Asynchronous

•   Resilient

•   Elastic

当前设计的主要问题是两个 microservices 之间的紧密耦合。web 客户端被配置为明确地针对第一个 microservice。

如果第一个 microservice 失败, 我们将无法通过调用另一个来恢复。如果我们在负载, 创建一个新的hello microservice 实例也不会对我们有帮助。非常感谢Vert.x web client交互是异步的。

但是, 由于我们不使用虚拟地址 (目的) 来调用 microservice,而使用它的直接 URL,  我们将不能获得需要的弹性。

我们可以使用 HTTP 进行反应 microservices 吗?是的。但这需要一些基础结构将虚拟 url 路由到一组服务。我们还需要实施一个负载平衡战略, 以提供弹性和健康检查支持, 以提高弹性。

不要失望,下一节我们将像reactive microservices前进一大步。

Vert.x Event Bus -一个信息中枢

Vert.x提供了一个事件总线来让不同部分之间交互。信息发送到地址包含一套headers和一个body。地址是表示目标的不透明字符串。消息使用者将自己注册到地址以接收消息。事件总线也是一个集群,它可以在分布式的生产者和消费者之间调用消息。

通过在群集模式下启动一个Vert.x 应用程序, 节点被连接起来, 以实现共享数据结构、硬停止故障检测和负载平衡组通信。事件总线可以在集群所有节点调度消息。要创造这样的集群,你可以选择Apache Ignite, Apache Zookeeper, Infinispan, orHazelcast.

我们将使用Infinispan

虽然 Infinispan (或您选择的技术) 管理节点发现和清单, 但事件总线通信使用直接端对端TCP 连接。
事件总线提供三种类型的传递语义。首先, 发送方法允许组件向地址发送消息。单个使用者将收到该消息。如果在此地址上注册了多个消费者, 则vert.x 将应用循环策略来选择消费者:

// Consumer
vertx.eventBus().consumer("address", message -> {
    System.out.println("Received: '" + message.body() + "'");
});
// Sender
vertx.eventBus().send("address", "hello");


与发送相反, 您可以使用发布方法将消息传递给在地址上注册的所有用户。最后, 发送方法可以与应答处理程序一起使用。此请求/响应机制允许在两个组件之间实现基于消息的异步交互:

// 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 ified api, 则可以使用 rxSend 方法返回单个。此单在收到答复时收到一个值。我们很快就会看到这个方法。

基于消息的的微服务


让我们重新实现hello microservice, 这一次使用事件总线而不是 HTTP 服务器来接收请求。microservice 答复消息, 以提供答复。

创建项目

让我们创建一个新项目。这一次, 我们要添加 Infinispan 依赖项, 即用于管理群集的内存中数据网格:

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目录

编辑src/main/java/io/vertx/book/message/HelloMicroservice.jar

@Override
public void start() {
    // Receive message from the address 'hello'
    vertx.eventBus().<String>consumer("hello", message -> {
        JsonObject json = new JsonObject()
            .put("served-by", this.toString());
        // Check whether we have 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的消费者地址。收到消息后他会进行答复。根据传入消息是否为空我们返回不同的结果。


mvn compile vertx:run \   
-Dvertx.runArgs="-cluster -Djava.net.preferIPv4Stack=true"

-cluster表示集群模式。

启动基于消息的交互


在本节中, 我们将创建另一个 microservice, 通过向 hello 地址发送消息并得到答复来调用 hello microservice。microservice 将重新实现与上一章相同的逻辑, 并调用两次服务 (一次与卢克, 一次与莱娅)。

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

编辑HelloConsumerMicroservice.java

@Override
public void start() {
  EventBus bus = vertx.eventBus();
  Single<JsonObject> obs1 = bus
    .<JsonObject>rxSend("hello", "Luke")
    .map(Message::body);
  Single<JsonObject> obs2 = bus
    .<JsonObject>rxSend("hello", "Leia")
    .map(Message::body);
  Single
    .zip(obs1, obs2, (luke, leia) ->
      new JsonObject()
        .put("Luke", luke.getString("message"))         
.put("Leia", leia.getString("message"))
    )
    .subscribe(
      x -> System.out.println(x.encode()),
      Throwable::printStackTrace);
}

此代码与上一章中的代码非常相似。我们将使用事件总线将消息发送到 hello 地址并提取答复正文, 而不是使用 WebClient 调用 HTTP 端点。

我们使用 zip 操作检索两个响应并生成最终结果。在订阅方法中, 我们将最终结果打印到控制台或打印堆栈跟踪。让我们将它与 HTTP 服务器结合起来。收到 HTTP 请求后, 我们将调用 hello 服务两次并将生成的结果作为响应返回.

继续修改

@Override public void start() {   vertx.createHttpServer()
    .requestHandler(       req -> {
        EventBus bus = vertx.eventBus();
        Single<JsonObject> obs1 = bus           .<JsonObject>rxSend("hello", "Luke")
          .map(Message::body);
        Single<JsonObject> obs2 = bus
          .<JsonObject>rxSend("hello", "Leia")           .map(Message::body);
        Single
          .zip(obs1, obs2, (luke, leia) ->
            new JsonObject()
              .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);
}
mvn compile vertx:run -Dvertx.runArgs="-cluster -Djava.net.preferIPv4Stack=true"

http://localhost:8082/ 我的运行出错了,正常情况会

{

  "Luke" : "helloLuke from ...HelloMicroservice@39721ab",

  "Leia" : "helloLeia from ...HelloMicroservice@39721ab"

}

现在我们是响应式了吗?

代码与我们以前编写的基于 HTTP 的 microservice 非常接近。唯一的区别是我们使用了事件总线而不是 HTTP。这会改变我们的 reactiveness 吗?真的!让我们看看为什么。

Elasticity(弹性)

弹性是 microservice 的 HTTP 版本不强制执行的特性之一。因为 microservice 是针对 microservice 的特定实例 (使用硬编码的 URL), 所以它没有提供我们需要的弹性。但现在, 我们使用发送到地址的消息, 这改变了游戏。让我们看看这个 microservice 系统的行为方式。

记住以前执行的输出。返回的 JSON 对象显示verticle计算了 hello 消息。输出总是显示相同的verticle。消息指示相同的实例。我们也许会认为这是因为我们运行了一个实例。现在让我们看看两个会发生什么。

停止vertx:run 并打包

mvn clean package

项目文件夹中打开两个终端,每个终端输入

java -jar target/hello-microservice-message-1.0-SNAPSHOT.jar \     
--cluster -Djava.net.preferIPv4Stack=true

这将启动两个 Hello microservice 的实例。返回到浏览器并刷新页面, 您应该看到如下内容:

{

  "Luke" : "helloLuke from ...HelloMicroservice@16d0d069",  

"Leia" : "helloLeia from ...HelloMicroservice@411fc4f"

}


使用了两个 Hello 实例。Vert.x 群集连接不同的节点, 事件总线是群集的。多亏了事件总线循环, Vert.x 事件总线将消息调度到可用的实例, 从而平衡了侦听同一地址的不同节点之间的负载。
因此, 通过使用事件总线, 我们有我们需要的弹性特性。

Resilience(容错)

容错呢?在当前代码中, 如果 hello microservice 失败, 我们将会失败并执行以下代码:

t -> {
  t.printStackTrace();
  req.response().setStatusCode(500).end(t.getMessage()); }

即使用户收到错误信息, 我们也不会崩溃, 我们不会限制我们的可伸缩性, 我们仍然可以处理请求。然而, 为了改善用户体验, 我们应该总是及时回复用户, 即使我们没有收到服务的响应。为了实现这个逻辑, 我们可以用超时来增强代码。

为了说明这一点, 让我们修改 Hello microservice, 以注入故障和歧路。

这个新的启动方法随机选择三策略之一: (1) 用显式失败回复 (2) 忘记回复 (导致用户端超时), 或 (3) 发送正确的结果。

@Override public void start() {
    vertx.eventBus().<String>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.
        }
    });
}

重新打包并重启 "您好 microservice" 的两个实例。随着这个错误的注入到位, 我们需要改善我们的消费者的 faulttolerance。实际上, 使用者可能会超时或收到显式故障。在您好消费者 microservice 中, 更改我们如何调用 hello 服务来:
EventBus bus = vertx.eventBus();
Single<JsonObject> obs1 = bus
  .<JsonObject>rxSend("hello", "Luke")
  .subscribeOn(RxHelper.scheduler(vertx))   .timeout(3, TimeUnit.SECONDS)
  .retry()
  .map(Message::body); 
Single<JsonObject> obs2 = bus.
  <JsonObject>rxSend("hello", "Leia")
  .subscribeOn(RxHelper.scheduler(vertx))
  .timeout(3, TimeUnit.SECONDS)
  .retry()
  .map(Message::body);

现在, 您可以重新加载该页。即使有失败或超时, 您也总能得到结果。请记住, 在调用服务时, 线程没有被阻止, 因此您可以随时接受新的请求并及时响应它们。但是, 此超时重试通常会导致更多的危害, 而不是好的, 我们将在下一章中看到。

总结

Microsoft® Translator(机翻)
在本节中, 我们学习了如何开发带有垂直 x 的 HTTP microservice 以及如何使用它。正如我们所了解到的, 硬编码在代码中消耗的服务的 URL 不是一个绝妙的主意, 因为它打破了一个反应性的特性。在第二部分中, 我们使用消息传递来替换 HTTP 交互, 这显示了消息传递和垂直 x 事件总线如何帮助生成被动 microservices。我们还在吗?是的, 没有。是的, 我们知道如何构建被动 microservices, 但是我们需要研究一些缺点。首先, 如果您只有 HTTP 服务怎么办?如何避免硬编码位置?弹性呢?本章中我们看到了超时和重试, 但是断路器、故障转移和舱壁怎么办?让我们继续旅程吧。


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值