配置 gRPC 請求的重試策略

banq發表於2024-05-13

在本教程中,我們將討論在gRPC(Google 開發的遠端過程呼叫框架)中實現重試策略的各種方法。 gRPC 在許多程式語言中都是可互操作的,但我們將重點關注 Java 實現。

重試的重要性
應用程式越來越依賴分散式架構。這種方法有助於透過水平擴充套件處理繁重的工作負載。它還促進了高可用性。然而,它也引入了更多潛在的故障點。因此,在開發具有多個微服務的應用程式時,容錯至關重要。

由於多種因素,RPC 可能會暫時或暫時失敗:

  • 網路延遲或連線中斷
  • 由於內部錯誤,伺服器未響應
  • 系統資源繁忙
  • 下游服務繁忙或不可用
  • 其他相關問題

重試是一種錯誤處理機制。重試策略可以幫助根據某些條件自動重新嘗試失敗的請求。它還可以定義客戶端可以重試的時間或頻率。這種簡單的模式可以幫助處理瞬態故障並提高可靠性。

RPC 失敗階段
讓我們首先了解遠端過程呼叫 (RPC) 可能在哪裡失敗:
客戶端應用程式發起請求,gRPC 客戶端庫將請求傳送到伺服器。收到請求後,gRPC 伺服器庫會將請求轉發到伺服器應用程式的邏輯。

RPC 可能會在各個階段失敗:

  • 離開客戶之前
  • 在伺服器中但在到達伺服器應用程式邏輯之前
  • 在伺服器應用程式邏輯中

gRPC 中的重試支援
由於重試是一種重要的恢復機制,gRPC 在特殊情況下會自動重試失敗的請求,並允許開發人員定義重試策略以實現更好的控制。

透明重試
我們必須明白,只有在請求尚未到達應用程式伺服器邏輯的情況下,gRPC 才能安全地重新嘗試失敗的請求。除此之外,gRPC 無法保證交易的冪等性。

內部重試是在離開客戶端或伺服器之前但在到達伺服器應用程式邏輯之前安全地進行。這種重試策略稱為透明重試。一旦伺服器應用程式成功處理請求,它就會返回響應並且不再嘗試重試。

當 RPC 到達 gRPC 伺服器庫時,gRPC 可以執行單次重試,因為多次重試可能會增加網路負載。但是,當 RPC 無法離開客戶端時,它可能會無限次重試。

重試策略
為了給開發人員更多的控制權,gRPC 支援在單個服務或方法級別為其應用程式配置適當的重試策略。一旦請求跨越第 2 階段,它就屬於可配置重試策略的範圍。服務所有者或釋出者可以藉助service config(一個 JSON 檔案)來配置其 RPC 的重試策略。

服務所有者通常使用 DNS 等名稱解析服務將服務配置分發到 gRPC 客戶端。但是,在名稱解析不提供服務配置的情況下,服務使用者或開發人員可以透過程式設計方式對其進行配置。

gRPC 支援多個重試引數:

  • 最大嘗試次數    :最大 RPC 嘗試次數,包括原始請求 預設最大值為 5
  • 初始退避    :重試嘗試之間的初始退避延遲
  • 最大退避    :它對指數退避增長設定了上限它是強制性的並且必須大於零
  • 退避乘數    :每次重試後,退避將乘以該值,並且當乘數大於 1 時,退避將呈指數增加它是強制性的並且必須大於零
  • 可重試狀態程式碼    :失敗且狀態匹配的 gRPC 呼叫將自動重試服務所有者在設計可重試的方法時應該小心。這些方法應該是冪等的,或者僅允許在伺服器中未進行任何更改的 RPC 的錯誤狀態程式碼上重試

值得注意的是,gRPC 客戶端使用initialBackoff、maxBackoff和backoffMultiplier引數在重試請求之前隨機化延遲。

有時,伺服器可能會在響應後設資料中傳送一條指令,不要重試或在延遲一段時間後嘗試請求。這稱為伺服器推回。

現在我們已經討論了 gRPC 的透明重試功能和基於策略的重試功能,讓我們總結一下 gRPC 如何整體管理重試:

以程式設計方式應用重試策略
假設我們有一項服務,可以透過呼叫向手機傳送 SMS 的底層通知服務來向公民廣播訊息。政府使用這項服務來發布緊急情況公告。使用此服務的客戶端應用程式必須具有重試策略,以減少由於暫時性故障而導致的錯誤。

讓我們進一步探討這一點。

1.高層設計
首先我們看broadcast.proto檔案中的介面定義:

syntax = <font>"proto3";
option java_multiple_files = true;
option java_package =
"com.baeldung.grpc.retry";
package retryexample;
message NotificationRequest {
  string message = 1;
  string type = 2;
  int32 messageID = 3;
}
message NotificationResponse {
  string response = 1;
}
service NotificationService {
  rpc notify(NotificationRequest) returns (NotificationResponse){}
}
Broadcast.proto檔案定義了NotificationService ,其中包含一個遠端方法notify()和兩個DTONotificationRequest和NotificationResponse。


稍後,我們可以使用broadcast.proto檔案生成支援Java原始碼來實現NotificationService。 Maven 外掛生成類NotificationRequest、NotificationResponse和NotificationServiceGrpc。

伺服器端的GrpcBroadcastingServer類使用ServerBuilder類註冊NotificationServiceImpl來廣播訊息。客戶端類GrpcBroadcastingClient使用gRPC 庫的ManagedChannel類來管理執行 RPC 的通道。

服務配置檔案retry-service-config.json概述了重試策略:

{
     <font>"methodConfig": [
         {
             
"name": [
                 {
                     
"service": "retryexample.NotificationService",
                     
"method": "notify"
                 }
              ],
             
"retryPolicy": {
                 
"maxAttempts": 5,
                 
"initialBackoff": "0.5s",
                 
"maxBackoff": "30s",
                 
"backoffMultiplier": 2,
                 
"retryableStatusCodes": [
                     
"UNAVAILABLE"
                 ]
             }
         }
     ]
}

之前,我們瞭解了重試策略,例如maxAttempts、指數退避引數和retryableStatusCodes。當客戶端呼叫先前在broadcast.proto檔案中定義的NotificationService中的遠端過程notify()時,gRPC 框架會強制執行重試設定。

2.實施重試策略
讓我們看一下GrpcBroadcastingClient類:

public class GrpcBroadcastingClient {
    protected static Map<String, ?> getServiceConfig() {
        return new Gson().fromJson(new JsonReader(new InputStreamReader(GrpcBroadcastingClient.class.getClassLoader()
            .getResourceAsStream(<font>"retry-service-config.json"), StandardCharsets.UTF_8)), Map.class);
    }
    public static NotificationResponse broadcastMessage() {
        ManagedChannel channel = ManagedChannelBuilder.forAddress(
"localhost", 8080)
          .usePlaintext()
          .disableServiceConfigLookUp()
          .defaultServiceConfig(getServiceConfig())
          .enableRetry()
          .build();
        return sendNotification(channel);
    }
    
    public static NotificationResponse sendNotification(ManagedChannel channel) {
        NotificationServiceGrpc.NotificationServiceBlockingStub notificationServiceStub = NotificationServiceGrpc
          .newBlockingStub(channel);
        NotificationResponse response = notificationServiceStub.notify(NotificationRequest.newBuilder()
          .setType(
"Warning")
          .setMessage(
"Heavy rains expected")
          .setMessageID(generateMessageID())
          .build());
        channel.shutdown();
        return response;
    }
}

Broadcast ()方法使用必要的配置構建ManagedChannel物件。然後,我們將其傳遞給sendNotification(),後者進一步呼叫存根上的notify()方法。

ManagedChannelBuilder類中在設定由重試策略組成的服務配置中發揮關鍵作用的方法是:

  • disableServiceConfigLookup():透過名稱解析顯式禁用服務配置查詢
  • enableRetry():啟用每個方法的重試配置
  • defaultServiceConfig():顯式設定服務配置
  • getServiceConfig()方法從retry-service-config.json檔案中讀取服務配置並返回其內容的Map表示形式。隨後,該Map被傳遞到ManagedChannelBuilder類中的defaultServiceConfig()方法。

最後,建立完ManagedChannel物件後,我們呼叫NotificationServiceGrpc.NotificationServiceBlockingStub型別的 notificationServiceStub物件的notify()方法來廣播訊息。該策略也適用於非阻塞存根。

建議使用專用類來建立ManagedChannel物件。這允許集中管理,包括重試策略的配置。

為了演示重試功能,伺服器中的NotificationServiceImpl類被設計為隨機停止服務。讓我們看一下GrpcBroadcastingClient 的實際應用:

@Test
void whenMessageBroadCasting_thenSuccessOrThrowsStatusRuntimeException() {
    try {
        NotificationResponse notificationResponse = GrpcBroadcastingClient.sendNotification(managedChannel);
        assertEquals(<font>"Message received: Warning - Heavy rains expected", notificationResponse.getResponse());
    } catch (Exception ex) {
        assertTrue(ex instanceof StatusRuntimeException);
    }
}

該方法呼叫GrpcBroadcastingClient類上的sendNotification()來呼叫伺服器端遠端過程來廣播訊息。

相關文章