配置 gRPC 请求的重试策略

在本教程中,我们将讨论在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 = "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概述了重试策略:

{
     "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("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("Message received: Warning - Heavy rains expected", notificationResponse.getResponse());
    } catch (Exception ex) {
        assertTrue(ex instanceof StatusRuntimeException);
    }
}
该方法调用GrpcBroadcastingClient类上的sendNotification()来调用服务器端远程过程来广播消息。