使用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 服務怎麼辦?如何避免硬編碼位置?彈性呢?本章中我們看到了超時和重試, 但是斷路器、故障轉移和艙壁怎麼辦?讓我們繼續旅程吧。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章