使用vertx構建響應式微服務-第四章 系統

上一章的重點是構建 microservices, 但本章是關於構建系統的。一個微服務不能叫做系統,系統由許多微服務組成。管理越多的微服務系統越複雜。
首先, 我們將學習如何使用服務發現來解決位置透明性和移動性問題。
然後, 我們將討論彈性和穩定性模式, 如超時, 斷路器, 和故障轉移。

服務發現

當你有一套微服務,第一個問題就是如果讓他們發現彼此?爲了和另一個微服務通信,需要知道它的地址。

正如上章所見,我們可以硬編程地址(事件總線地址,RUL,路徑,等)在代碼中或者放在配置文件中。然而這個解決方案沒有移動性。你的應用將會相當僵硬,每一部分都不能移動,這與我們想用微服務做的背道而馳。

客戶端和服務器端服務發現


Microservices 需要移動, 但可尋址。消費者需要能夠與 microservice 通信, 而不知道它的確切位置, 特別是因爲這個位置可能會隨着時間的推移而改變。位置透明性提供了彈性和活力: 使用循環策略的用戶可以調用 microservice 的不同實例, 在兩個調用之間 microservice 可能已經移動或更新。

位置透明度可以通過稱爲服務發現的模式來解決。每個 microservice 應該宣佈如何調用它及其特性, 包括它的位置, 以及其他元數據 (如安全策略或版本)。這些公告存儲在服務發現基礎結構中, 通常是執行環境提供的服務註冊表。microservice 還可以決定從註冊表中撤回其服務。尋找其他服務的 microservice 還可以搜索此服務註冊表以查找匹配的服務, 選擇最好的一個 (使用任何類型的標準), 然後開始使用它。


可以使用兩種類型的模式來消耗服務。使用客戶端服務發現時, 使用者服務會根據服務註冊表中的名稱和元數據查找服務, 選擇匹配的服務並使用它。從服務註冊表檢索到的引用包含指向 microservice 的直接鏈接。由於 microservices 是動態實體, 因此服務發現基礎結構必須不僅允許提供商發佈其服務和消費者來查找服務, 還提供有關服務的到達和離職的信息。使用客戶端服務發現時, 服務註冊表可以採取各種形式, 如分佈式數據結構、專用基礎結構 (如領事) 或存儲在諸如 Apache 動物園或 Redis 等庫存服務中。


或者, 您可以使用服務器端服務發現, 並讓負載平衡器、路由器、代理或 API 網關管理您的發現 (圖 4-2)。使用者仍然根據其名稱和元數據查找服務, 但檢索虛擬地址。當使用者調用服務時, 請求被路由到實際實現。您可以在 Kubernetes 或使用 AWS 彈性負載平衡器時使用此機制。

Vert.x服務發現


vert.x 提供了一個可擴展的服務發現機制。您可以使用相同的 API 在客戶端或服務器端服務發現。vert.x 服務發現可以從許多類型的服務發現基礎結構 (如Consul 或 Kubernetes) 中導入或導出服務 (圖 4-3)。它也可以在沒有任何專用服務發現基礎結構的情況下使用。在這種情況下, 它使用在vert.x 羣集上共享的分佈式數據結構。


您可以按類型檢索服務, 以便能夠使用已配置的服務客戶端。服務類型可以是 HTTP 端點、事件總線地址、數據源等。例如, 如果要檢索我們在上一章中實現的名爲 hello 的 HTTP 端點, 請編寫以下代碼:

// We create an instance of service discovery
ServiceDiscovery discovery = ServiceDiscovery.create(vertx);
// As we know we want to use an HTTP microservice, we can
// retrieve a WebClient already configured for the service
HttpEndpoint
  .rxGetWebClient(discovery,
    // This method is a filter to select the service     
rec -> rec.getName().endsWith("hello")
  )
  .flatMap(client ->
    // We have retrieved the WebClient, use it to call
    // the service
    client.get("/").as(BodyCodec.string()).rxSend())   
.subscribe(response -> System.out.println(response.body()));

檢索到的 WebClient 配置爲服務位置, 這意味着您可以立即使用它來調用該服務。如果您的環境正在使用客戶端發現, 則配置的 URL 將針對該服務的特定實例。如果使用服務器端發現, 客戶端將使用虛擬 URL。

根據您的運行時基礎結構, 您可能必須註冊您的服務。但是, 當使用服務器端服務發現時, 您 一般不必這樣做, 因爲您在部署服務時聲明瞭它。否則, 您需要顯式發佈服務。若要發佈服務, 需要創建包含服務名稱、位置和元數據的記錄:

// We create the service discovery object
ServiceDiscovery discovery = ServiceDiscovery.create(vertx);
vertx.createHttpServer()
  .requestHandler(req -> req.response().end("hello"))
  .rxListen(8083)
  .flatMap(
    // Once the HTTP server is started (we are ready to serve)
    // we publish the service.     
server -> {
      // We create a record describing the service and its
      // location (for HTTP endpoint)
      Record record = HttpEndpoint.createRecord(
        "hello",              // the name of the service
        "localhost",          // the host         
server.actualPort(),  // the port
        "/"                   // the root of the endpoint
      );
      // We publish the service       
      return discovery.rxPublish(record);
    }
  )
  .subscribe(rec -> System.out.println("Service published")); 

服務發現是 microservice 基礎結構中的一個關鍵組件。它實現了動態性、位置透明性和機動性。在處理一小組服務時, 服務發現可能看起來很麻煩, 但當您的系統增長時, 它是必需的。無論使用何種基礎結構和服務發現類型, vert.x 服務發現都爲您提供了唯一的 API。但是, 當您的系統增長時, 還有另一個變量會以指數形式出現故障。

穩定性和韌性模式

在處理分佈式系統時, 失敗是一流的公民, 您必須與他們一起生活。您的 microservices 必須知道, 他們調用的服務可能由於許多原因而失敗。microservices 之間的每一個互動最終都會以某種方式失敗, 你需要爲失敗做好準備。失敗可以採取不同的形式, 從各種網絡錯誤到語義錯誤不等。

響應式微服務的錯誤管理

反應 microservices 負責在本地管理故障。他們必須避免把失敗傳播到另一個 microservice。換言之, 你不應該把燙手山芋委派給另一個 microservice。因此, 反應性 microservice 的代碼認爲失敗是頭等公民。


Vert.x 開發模型使故障成爲一箇中心實體。使用回調開發模型時, 處理程序通常以參數的方式接收 AsyncResult。此結構封裝異步操作的結果。在成功的情況下, 您可以檢索結果。失敗時, 它包含描述失敗的 Throwable:

client.get("/").as(BodyCodec.jsonObject())
    .send(ar -> {         
if (ar.failed()) {             
Throwable cause = ar.cause();             // You need to manage the failure.
        } else {
            // It's a success
            JsonObject json = ar.result().body();
       

      }

    });


使用 RxJava api 時, 可以在訂閱方法中進行故障管理:

client.get("/").as(BodyCodec.jsonObject())
    .rxSend()
    .map(HttpResponse::body)
    .subscribe(
        json -> { /* success */ },         
err -> { /* failure */ }     );

如果發生錯誤,錯誤處理會被調用。我們也可以更早的處理錯誤。

client.get("/").as(BodyCodec.jsonObject())
    .rxSend()
    .map(HttpResponse::body)
    .onErrorReturn(t -> {
        // Called if rxSend produces a failure
        // We can return a default value         
return new JsonObject();
    })
    .subscribe(         json -> {             // Always called, either with the actual result             // or with the default value.
        }
    );

管理錯誤並不有趣但是必須這麼做。

使用超時

在處理分佈式交互時我們經常使用超時。超時是一種簡單的機制,讓你在認爲不會有響應時停止請求。超時可以有效的把錯誤隔離在單個微服務中。

client.get(path)
  .rxSend() // Invoke the service
  // We need to be sure to use the Vert.x event loop
  .subscribeOn(RxHelper.scheduler(vertx))   // Configure the timeout, if no response, it publishes
  // a failure in the Observable
  .timeout(5, TimeUnit.SECONDS)
  // In case of success, extract the body
  .map(HttpResponse::bodyAsJsonObject)
  // Otherwise use a fallback result
  .onErrorReturn(t -> {
    // timeout or another exception
    return new JsonObject().put("message", "D'oh! Timeout");
  })
  .subscribe(     json -> {
      System.out.println(json.encode());
    }
  );

超時經常和重試放在一起。有時候在超時後發起重試可以解決問題,比如網絡丟包引起的超時。但有時候立馬重試並不能解決問題。你可以自己決定是否重試。


client.get(path)
  .rxSend()
  .subscribeOn(RxHelper.scheduler(vertx))
  .timeout(5, TimeUnit.SECONDS)
  // Configure the number of retries   // here we retry only once.
  .retry(1)
  .map(HttpResponse::bodyAsJsonObject)
  .onErrorReturn(t -> {
    return new JsonObject().put("message", "D'oh! Timeout");
  })
  .subscribe(
    json -> System.out.println(json.encode())   );

你需要記住非常重要的一點是,超時並不一定是程序錯誤。在一個分佈式系統中,有很多原因導致錯誤。我們來看一個示例。你有兩個微服務,A和B。A發一個請求給B.但是B沒搭理A,A的請求超時。在這個例子中,可能有三種原因。

1.A給B的消息丟失了,操作沒有執行。

2.B中的操作失敗了--B的操作沒有完成

3.B返回給A的消息丟失了--B的操作執行了,但是A沒有收到響應。

最後一種情況經常被忽略而且很有害。這個時候發起重試會破壞系統的完整性(B被執行了兩遍)。重試只能用於冪等運算(在編程中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同)。在發起重試前,請確保該操作是冪等的。重試還會導致響應時間變長。因此返回回退經常比重試更好。此外不斷的向發生錯誤的服務器發送請求也不利於他們恢復正常。下面有請斷路器出場。

斷路器

斷路器可以防止我們重複調用一個錯誤的操作。當調用一個操作錯誤時,斷路器會記錄,當這個記錄超出一定次數時,斷路器打開,以後的請求將會被斷路器切斷,不會去執行那個錯誤的操作,而是由斷路器返回一個結果。但是斷路器也不是永遠的打開了,經過我們配置的時間後,它覺得那個操作可以成功了,於是下次請求會去調用那個操作,如果那個操作成功了,它會自動關閉,如果失敗了,他會繼續打開,直到我們配置的時間到了會去再試一次,這樣周而復始。

CircuitBreaker circuit = CircuitBreaker.create("my-circuit",
    vertx, new CircuitBreakerOptions()
        .setFallbackOnFailure(true) // Call the fallback
                                    // on failures
        .setTimeout(2000)           // Set the operation timeout
        .setMaxFailures(5)          // Number of failures before
                                    // switching to
                                    // the 'open' state
        .setResetTimeout(5000)      // Time before attempting
                                    // to reset
                                    // the circuit breaker
);
// ... circuit.rxExecuteCommandWithFallback(
    future ->         client.get(path)             .rxSend()
            .map(HttpResponse::bodyAsJsonObject)
            .subscribe(future::complete, future::fail),     t -> new JsonObject().put("message", "D'oh! Fallback")
).subscribe(         json -> {
            // Get the actual json or the fallback value
            System.out.println(json.encode());
        }
);

在這個代碼中HTTP交互受到斷路器保護,當失敗到一定次數時,斷路器阻止繼續調用,在一個週期時間裏,斷路器會允許一次調用通過,來試探操作是否正常了。

健康檢查和故障轉移

雖然超時和斷路器可以讓消費者處理錯誤,但是崩潰呢?當崩潰時,故障轉移策略將重啓崩潰的部分。

但是在做到這些之前,我們需要知道什麼時候微服務崩了。微服務提供了api來檢查它自己的狀態。它會告訴調用者他是否正常。調用通常使用HTTP交互,但不是必須的。經過調用,一系列的檢查被執行,並返回狀態。當一個服務被發現不正常時它將不會再被調用,因爲就算調用也很可能會失敗。就算調用正常的服務也不能保證成功。建康檢查僅僅表明那個服務在運行,而不能確定它能給你想要的結果。

根據你的環境,你可能需要不同級別的健康檢查。比如啓動時執行準備檢查,確保服務已經初始化,並準備好接受請求,運行時接受服務檢查,確保服務可以正常返回結果,如果沒有返回結果,說明它崩了。

在Vert.x中有多種方法來實現健康檢查。你可以僅僅用返回狀態的路由,或者發一個真實請求。你還可以使用Vert.x健康檢查模塊來實現多種健康檢查,並返回不同結果。下邊的代碼演示瞭如果在一個應用中使用兩種級別的健康檢查。

Router router = Router.router(vertx);
HealthCheckHandler hch = HealthCheckHandler.create(vertx);
// A procedure to check if we can get a database connection
hch.register("db-connection", future -> {
  client.rxGetConnection()
    .subscribe(c -> {         future.complete();
        c.close();
      },       future::fail     );
});
// A second (business) procedure
 hch.register("business-check", future -> {
  // ...
});
// Map /health to the health check handler
 router.get("/health").handler(hch); // ...

完成健康檢查後,就可以實施故障轉移策略。一般的策略僅僅是重啓崩掉的部分,樂觀的期望。Vert.x提供了內置的故障轉移,當一個節點崩掉時觸發。它不需要你週期性的檢查節點狀態。當Vert.x丟失一個節點的連接,Vert.x會在集羣中選擇另一個健康的節點,並重新部署崩掉的那個。

故障轉移可以保證你的系統運行,但不能解決根本問題,發生故障需要你來做分析並解決。

總結

本章討論了你的微服務系統增長時你將面臨的幾個問題。

正如我們所學的,服務發現是必須做的在微服務系統中,來確保位置的公開。接着,由於不可避免的錯誤,我們討論了幾個模式,來提高你的系統的容錯性和穩定性。

Vert.x可以用相同的API來處理客戶端服務發現和服務的服務發現。Vert.x服務發現可以導入導出到其他服務發現架構。Vert.x包含了一套容錯模式例如超時,斷路器,故障轉移。我們也看到了這些示例。處理錯誤雖然很枯燥,但我們不得不做。

下一章,我們將學習如何發佈Vert.x 響應式微服務到OpenShift和演示如果使用服務發現,斷路器,故障轉移來讓你的系統堅不可摧。雖然這些主題非常重要,但不要低估其他問題的重要性。例如安全,部署,彙總日誌,測試等等。

本章完撒花奏樂。


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