Java 和微服務,第 3 部分: 微服務通信
此內容是該系列的一部分: Java 和微服務,第 3 部分
微服務的設計目的是方便擴展。這種擴展通過橫向擴展各個服務來完成。面對眾多微服務實例,您需要一種查找服務的方法,并在您調用的服務的不同實例之間進行負載平衡。本章將介紹可用于查找系統中的微服務并向其發出請求的選項,并將介紹在確定所需服務的位置后,如何實現微服務架構中的不同服務之間的通信。
服務注冊表
服務注冊表是一個持久存儲區,包含隨時可用的所有微服務的列表,以及訪問它們的路徑。微服務可能因為 4 種原因而需要與服務注冊表進行通信:
-
注冊
成功部署某項服務后,必須向服務注冊表注冊該微服務。
-
檢測信號
微服務應定期向注冊表發送檢測信號,以表明它已準備好接收請求。
-
服務發現
要與另一個服務進行通信,微服務必須調用服務注冊表來獲得可用實例列表。(詳見小節"服務調用"了解更多的細節)。
-
注銷
當服務停止運行時,必須從服務注冊表中的可用服務列表中刪除它。
第三方注冊與自注冊
向服務注冊表注冊微服務的過程,可由微服務或第三方來完成。如果借助第三方,則要求第三方檢查微服務,以確定當前狀態,并將該信息轉發給服務注冊表。第三方還負責服務的注銷。如果微服務本身負責執行注冊和發送檢測信號,那么在未收到檢測信號時,服務注冊表就可以注銷該服務。
使用第三方的優勢在于,注冊和檢測信號發送邏輯能夠與業務邏輯保持分離。缺點在于,需要部署一個額外的軟件,而且微服務必須向一個健康端點公開,以供第三方輪詢。
使用自注冊會將注冊和檢測信號發送邏輯整合到微服務自身中。使用此方法需要小心測試各種考慮因素,需要了解代碼分離的更多信息。但是,許多服務注冊表解決方案提供了一種便捷的庫形式注冊功能,降低了所需代碼的復雜性。
通常會使用一個服務注冊表,提供基于 Java 的庫來處理注冊和檢測信號。這種配置使您的服務能更快地編碼,并在系統中(即 Java 中)的所有微服務之間實現一致性。可以使用以下服務注冊表解決方案:
所有這些注冊表都是開源的,支持針對 Java 的集成。作為 Netflix 產品棧的一部分,Eureka 與其他 Netflix 解決方案緊密集成來實現負載平衡和容錯。使用 Eureka 作為服務注冊表,您可以輕松地添加其他解決方案。Amalgam8 開源項目同時提供了服務注冊和負載平衡功能。它還提供了可在測試期間使用的路由配置。
可用性與一致性
大部分服務注冊表提供了分區容錯性以及一致性或可用性。由于 CAP 定理,它們無法同時提供所有 3 種功能。例如,Eureka 提供了可用性,Consul 和 Apache Zookeeper都提供了一致性。一些文章認為,您應該始終選擇一種或另一種功能(Knewton 博客上的這篇文章值得一讀),但最終決定完全取決于您的應用程序的確切要求。如果您要求所有微服務每次都擁有相同的外觀,那么您應該選擇一致性。如果要求快速響應請求,而不是等待一致的答案,那么您應該選擇可用性。一些解決方案可將這兩種功能與 Java 集成。
服務調用
當一個服務需要與另一個服務通信時,它會使用服務注冊表中存儲的信息。然后在服務器端或客戶端對實際的微服務執行調用。
服務器端
服務器端與微服務的通信是通過服務代理執行的。服務代理在服務注冊表中提供或作為一個單獨服務提供。當微服務需要執行調用時,它調用服務代理的已知端點。服務代理負責從服務注冊表獲取目標微服務的位置,并轉發該請求。代理獲取響應并將其路由回發出請求的原始微服務。在示例場景中,負載平衡完全在服務器端執行。圖 1 給出了使用服務代理時的請求流。
圖 1.對其他微服務的調用是使用服務代理來實現的
使用服務器端調用具有以下重要優點:
- 請求簡單
微服務發出的請求可以很簡單,因為它調用的是一個已知端點。
- 更容易測試
使用服務器代理會從微服務消除任何負載平衡或路由邏輯。這種配置使您能夠使用模擬代 理來測試服務的完整功能。
在云平臺中,比如 Cloud Foundry 和 Bluemix,代理由平臺提供。在 Bluemix 中,每個部署的應用程序都有一個已知的端點,稱為 路由 。當另一個服務或應用程序調用此路由時,Bluemix 會在后臺執行負載平衡。
這種方法有以下主要缺點:
- 跳數更多
添加一次對服務代理的調用,就會增加每個請求的網絡跳數。結果,每個請求的跳數通常比客戶端解決方案還要多。值得注意的是,服務代理可能保存了有關其他微服務的緩存信息,所以在許多情況下,請求會經歷 4 跳:1 →4 →5 →6。圖 2 顯示了所需的網絡跳數。
圖 2 .在使用服務代理時,完成一個請求服務需要網絡跳躍
客戶端
一個微服務對另一個微服務的請求可直接從客戶端發出。首先,向服務注冊表請求服務的一個或多個實例的位置。然后,在客戶端發出對該微服務的請求。該微服務的位置通常已緩存,所以在未來,發出請求時無需返回到服務注冊表。如果在未來,請求失敗,客戶端可重新調用服務注冊表。一種最佳實踐是對緩存的微服務位置設置一個超時值。這種配置意味著,如果部署了一個服務的新版本,那么其他微服務不必等到其緩存的實例發生錯誤才知道存在新實例。
與服務代理方法相比,此方法需要的網絡跳數更少。圖 3 顯示了所需的網絡跳數。同樣地,這是在客戶端沒有服務的緩存信息時的跳數。在許多情況下,不需要向服務注冊表發出請求,所以只需要兩跳:3 →4。
圖 3.從客戶端向服務注冊表發出請求時的網絡跳數
請求和所需的任何負載平衡可通過以下兩種機制之一來處理:
- 客戶端庫
- Sidecar
兩種機制都在客戶端發出請求。但是,客戶端可在微服務內運行,而 sidecar 雖然與微服務一起部署,但要在一個單獨的進程上運行。圖 4 顯示了一個示例架構圖。
圖 4.請求由客戶端庫或 sidecar 進程發出
客戶端庫
客戶端庫在許多方面很有用,一個方面是隔離與遠程資源通信的細節。對于服務注冊表,客戶端庫(比如 Consul 或 Netflix Eureka 所提供的庫)處理服務注冊和檢測信號。其他庫(比如 Netflix Ribbon)提供了客戶端負載平衡功能。
您通常可以采用現有的庫作為起點,而不是編寫自己的庫。如果您系統中的所有微服務都是使用 Java 編寫的,則應標準化您的庫。如果您有一個多語言的微服務系統,則需要為每種語言標準化一個庫。通過在每個地方使用同一個庫,開發人員可以靈活地在微服務之間移動,避免了讓您的基礎架構和構建流程變得過于復雜。
使用客戶端庫的缺點是,您現在將復雜的服務調用移回到了應用程序中。這種配置使測試您的服務變得更加復雜。確保使d用客戶端庫的代碼與微服務中的業務邏輯保持了良好的分離。
Sidecar
Sidecar 是服務代理與客戶端庫之間的一種不錯的折衷方案。Sidecar 是一個與您的微服務一起部署的單獨進程,它既可作為容器內的單獨進程,也可作為一個單獨但緊密相關的容器。
Sidecar 維護了向服務注冊表的服務注冊,通常還為對其他微服務的出站調用執行客戶端負載平衡。
因為 Sidecar 是在自己的進程中運行的,所以它們不依賴于語言。您可以對一個多語言應用程序中的所有微服務使用同一種 Sidecar 實現,無論這些服務是使用什么語言編寫的。
Netflix Prana 是一個開源 Sidecar,它向非 Java 應用程序提供了基于 Java 的 Ribbon 和 Eureka 客戶端庫的功能。
Kubernetes 是一個容器編排引擎,提供了類似 Sidecar 的服務發現和負載平衡功能。
Amalgam8 解決方案可與容器結合使用,也可以單獨使用。它管理服務注冊,提供了客戶端服務發現、路由和負載平衡功能。
選擇一個能處理多個方面的解決方案,可以降低您的服務和基礎架構的復雜性。
API 網關
API 網關可用于為內部和外部客戶端執行服務調用。API 網關也可對服務代理執行類似功能(參見小節"服務器端")。服務向 API 網關發出請求,API 網關在注冊表中查找目標服務,發出請求,然后返回響應。但是二者之間存在區別。對服務代理的請求使用了最終服務所提供的 API。對 API 網關的請求使用了網關提供的 API。這種配置意味著,API 網關可提供與微服務不同的 API。
內部微服務可使用這些相同的 API,而 API 網關對外部客戶端最有利。通常,微服務提供了更細粒度的 API。相較而言,使用應用程序的外部客戶端可能不需要細粒度的 API。它可能還需要一個 API 來使用來自多個微服務的信息。API 網關向外部客戶端公開對它們有幫助的 API,將微服務的實際實現隱藏在網關背后。所有外部客戶端都通過這個 API 網關訪問應用程序。
微服務通信
在分布式系統中,服務間的通信至關重要。組成應用程序的微服務必須無縫協同工作,才能向客戶提供有價值的東西。在上面我們討論了如何找到特定的微服務,還介紹了跨不同實例執行負載平衡的選項。接下來,將介紹在確定所需服務的位置后,如何實現微服務架構中的不同服務之間的通信。本章還將介紹同步和異步通信,以及如何實現災難恢復。
同步和異步
同步通信是一種需要響應的請求,無論是立即還是在一定時間量后獲得響應。異步通信是一種不需要響應的消息。
在使用獨立部件構建的高度分布式系統中使用異步事件或消息,具有令人信服的理由。在某些情況下,使用同步調用可能更適合,小節"同步消息 (REST)"將介紹這些情況。
對于每種調用風格,服務會使用 Swagger 等工具記錄發布的所有 API。還會記錄事件或消息有效負載,以確保系統容易被理解并為未來的使用者提供支持。事件訂閱者和 API 使用者應該容忍無法識別的字段,因為它們可能是新字段。服務還應預料到和處理不良數據。假設某個時刻所有功能都將失效。
同步消息 (REST)
前面已經提到,在分布式系統中,異步消息傳遞形式極為有用。如果適合明確的請求/響應語義,或者一個服務需要觸發另一個服務中的特定行為,則應使用同步 API。
這些通常是傳遞 JSON 格式數據的 RESTful 操作,但也可以使用其他協議和數據格式。在基于 Java 的微服務中,讓應用程序傳遞 JSON 數據是最佳選擇。一些庫可以將 JSON 解析為 Java 對象,而且 JSON 已得到廣泛采用,這些使得 JSON 成為了讓您的微服務容易使用的不錯選擇。
JAX-RS 的異步支持功能
盡管 JAX-RS 請求始終是同步的(您總是要求響應),但您可以利用異步編程模型。JAX-RS 2.0 中的異步支持使得線程在等待 HTTP 響應期間可執行其他請求。在無狀態 EJB bean 中,注入一個 AsyncResponse 對象實例,并使用 @Suspended 注釋與活動請求的處理相綁定。@Asynchronous 注釋允許卸載活動線程上的工作。示例 1 給出了用于返回響應的代碼。
示例 1. JAX-RS 2.0 包含異步支持
@Stateless
@Path("/")
public class AccountEJBResource {
@GET
@Asynchronous
@Produces(MediaType.APPLICATION_JSON)
public void getAccounts(@Suspended final AsyncResponse ar) {
Collection<Account> accounts = accountService.getAccounts();
Response response = Response.ok(accounts).build();
ar.resume(response);
}
另一個選項是使用反應式的庫,比如 RxJava。RxJava 是 ReactiveX 的一種 Java 實現,是一個旨在對來自多個并行出站請求的響應進行聚合和過濾的庫。它擴展了觀察者模式,還緊密集成了旨在處理負載平衡和向端點添加恢復能力的庫,比如 FailSafe和 Netflix Hystrix。
異步消息(事件)
異步消息用于解耦協調。僅在事件的創建者不需要響應時,才能使用異步事件。
來自外部客戶端的請求,通常必須經歷多個微服務才能獲得響應。如果每個調用都是同步執行,那么調用所需的總時間會阻礙其他請求。微服務系統越復雜,每個外部請求所需的微服務間的交互就越多,在系統中產生的延遲也就越大。如果在進程中發出的請求可替換為異步事件,那么您應實現該事件。
對事件采用一種反應式應對方式。服務僅發布與它自己的狀態或活動相關的事件。其他服務然后可訂閱這些事件并做出相應的反應。
事件是創作新交互模式而不引入依賴性的好方法。它們支持采用最初創建架構時未考慮到的方式來擴展架構。
微服務模式要求每個微服務都擁有自己的數據。這個要求意味著,當請求傳入時,可能有多個服務需要更新它們的數據庫。服務應使用其他關注方可以訂閱的事件來通告數據更改。要進一步了解如何使用事件處理數據事務和實現一致性。
要協調應用程序的消息,可以使用任何一個可用的消息代理和匹配的客戶端庫。一些與 Java 集成的示例包括帶 RabibitMQ Java 客戶端的 AMQP 代理、Apache Kafka 和 MQTT(針對物聯網領域而設計)。
內部消息
事件可在一個微服務內使用。例如,為了處理訂單請求,可以創建一組可由服務連接器類訂閱的事件,這些事件可觸發這些類來發布外部事件。使用內部事件可提高對同步請求的響應速度,因為不需要在返回響應之前等待其他同步請求的完成。
利用 Java EE 規范對事件的支持。內部異步事件可使用上下文和依賴注入 (CDI) 來完成。在此模型中,一個事件包含一個 Java 事件對象和一組修飾符類型。任何想對觸發的事件做出反應的方法都需要使用 @Observes 注釋。事件是使用 fire() 方法觸發的。示例 2 給出了我們使用的方法和注釋。
示例 2. 使用上下文和依賴注入的事件
public class PaymentEvent {
public String paymentType;
public long value;
public String dateTime;
public PaymentEvent() {}
}
public class PaymentEventHandler {
public void credit(@Observes @Credit PaymentEvent event) {
…
}
}
public class Payment {
@Inject
@Credit
Event<PaymentEvent> creditEvent;
Public String pay() {
PaymentEvent paymentPayload = new PaymentEvent();
// populate payload…
crediteEvent.fire(paymentPayload);
…
}
}
容錯
遷移到微服務架構的一個強烈動機是獲得具有更強的容錯和恢復能力的應用程序。現代應用程序要求宕機時間接近于零,并在數秒內而不是數分鐘內響應請求。微服務中的每項服務都必須能持續運行,即使其他服務已宕機。本節將重點介紹在與其他微服務通信時應使用的技術和工具,以便為您的微服務實現容錯和恢復能力。
適應更改
當一個微服務向另一個微服務發出同步請求時,它使用了一個特定的 API。該 API 擁有明確的輸入屬性和輸出屬性,輸入屬性必須包含在請求中,輸出屬性包含在響應中。在微服務環境中,這通常采用在服務間傳遞的 JSON 數據形式。假設這些輸入和輸出屬性絕不會發生更改是不切實際的。甚至在設計最好的應用程序中,各種需求也在不斷變化,導致需要添加、刪除和更改屬性。要適應這些形式的更改,微服務的制作者必須認真考慮他們制作的 API 和使用的 API。
- 使用 API
作為 API 的使用者,必須對您收到的響應執行驗證,確認它包含執行該功能所需的信息。如果收到了 JSON 數據,還需要在執行任何 Java 轉換之前解析 JSON 數據。執行驗證或 JSON 解析時,必須做兩件事情:
-
僅驗證請求中需要的變量或屬性。
不要僅僅因為提供了變量,就對其執行驗證。如果請求中未使用它們,則不需要驗證它們的存在。
-
接受未知屬性
如果您收到意料之外的變量,不要拋出異常。如果響應包含您需要的信息,一起提供的其他信息則無關緊要。
選擇一個允許您配置傳入數據的解析方式的 JSON 解析工具。Jackson Project 提供了注釋來配置解析器,如示例 3 所示。Jackson 庫提供的 @JsonInclude 和 @JsonIgnoreProperties 注釋用于指示在序列化期間應使用哪些值,以及是否忽略未知屬性。
示例 3 . 用于 JSON 解析的 Jackson 注釋
@JsonInclude(Include.NON_EMPTY)
@JsonIgnoreProperties(ignoreUnknown = true)
Public class Account {
// Account ID
@JsonProperty("id")
protected String id;
}
通過遵循這些規則,您的微服務可適應任何對它沒有直接影響的更改。即使微服務的制作者刪除或添加了屬性,如果您不使用這些屬性,您的微服務就可以繼續正常運行。
- 制作 API
當向外部客戶端提供 API 時,在接受請求和返回響應時應執行以下兩種操作:
-
接受請求中包含的未知屬性
如果一個服務使用了不必要的屬性來調用您的 API,則丟棄這些值。在這種情況下,返回錯誤消息會導致不必要的故障。
-
僅返回與被調用的 API 相關的屬性
為以后對服務的更改留出盡可能多的空間。避免因為過度分享而泄漏實現細節。
前面已經提到,這些規則組成了健壯性原則 (Robustness Principle)。通過遵循這兩條規則,您能夠在未來以最容易的方式更改 API,適應不斷變化的客戶需求。
超時
使用超時可預防請求無限期地等待響應。但是,如果某個特定請求總是超時,您會浪費大量時間來等待超時。
斷路器專為避免反復超時而設計。它的工作方式類似于電子領域中的斷路器。特定請求每次失敗或生成超時時,斷路器都會進行記錄。如果次數達到某個限制,斷路器就會阻止進一步調用,并立即返回一個錯誤。它還包含一種重試調用機制,無論是在一定時間量后進行重試,還是為應對一個事件而重試。
在調用外部服務時,斷路器至關重要。通常,會使用 Java 中的一個現有的庫作為斷路器。
斷路器
使用超時可預防請求無限期地等待響應。但是,如果某個特定請求總是超時,您會浪費大量時間來等待超時。
斷路器專為避免反復超時而設計。它的工作方式類似于電子領域中的斷路器。特定請求每次失敗或生成超時時,斷路器都會進行記錄。如果次數達到某個限制,斷路器就會阻止進一步調用,并立即返回一個錯誤。它還包含一種重試調用機制,無論是在一定時間量后進行重試,還是為應對一個事件而重試。
在調用外部服務時,斷路器至關重要。通常,會使用 Java 中的一個現有的庫作為斷路器。
隔板
在船運領域,隔板是一種隔離物,可以防止某個隔間中的滲漏導致整條船沉沒。微服務中的隔板是一個類似的概念。您需要確保應用程序的某個部分中的故障不會導致整個應用程序崩潰。隔板模式關乎微服務的創建方式,而不是任何特定工具或庫的使用。創建微服務時,應該總是詢問自己如何才能夠隔離不同部分,防止出現連鎖故障。
隔板模式的最簡單實現是提供應變計劃 (fallback)。添加應變計劃后,應用程序就能在非關鍵服務中斷時繼續正常運行。例如,在一家在線零售店中,可能有一個項用戶提供推薦的服務。如果推薦服務中斷,用戶仍應能搜索商品和下單。一個有趣的示例是連鎖應變計劃,相對于拋出錯誤,針對個性化內容的失敗請求會優先回退到針對更通用內容的請求,進而回退到返回緩存(且可能過時)的內容。
防止緩慢或受限制的遠程資源導致整個應用程序崩潰的另一種策略是,限制可供這些出站請求使用的資源。實現此目標的最常見方式是使用隊列和旗語 (Semaphore)。
隊列的深度表示待處理工作的最大量。任何在隊列裝滿后發出的請求都會快速失敗。隊列還被分配了有限數量的工作者,該數量定義了可被遠程資源阻塞的服務器線程的最大數量。
旗語機制采用了一組許可。發出遠程請求需要許可。請求成功完成后,就會釋放該許可。
二者之間的重要區別在于分配的資源被用盡時出現的結果。對于 Semaphore 方法,如果無法獲得許可,則會完全跳過出站請求,但隊列方法會提供一些填充信息(至少在隊列裝滿之前)。
這些方法也可用于處理設置了速率限制的遠程資源,速率限制是另一種要處理的意外故障形式。
總結
本文重點介紹了在眾多微服務實例中查找服務的方法,以及不同微服務之間的通信。下一部分將介紹如何使用基于 Java 的微服務實現微服務在數據處理方面保持可管理。好了,學習愉快,下次再見!
參考資源 (resources)
來自:http://www.ibm.com/developerworks/cn/java/j-cn-java-and-microservice-3/index.html?ca=drs-