具有截止时间和取消功能的可靠的 gRPC 服务

作者:James Newton-King

截止时间和取消功能是 gRPC 客户端用来中止进行中调用的功能。 本文介绍截止时间和取消功能非常重要的原因,以及如何在 .NET gRPC 应用中使用它们。

截止时间

截止时间功能让 gRPC 客户端可以指定等待调用完成的时间。 超过截止时间时,将取消调用。 设定一个截止时间非常重要,因为它将提供调用可运行的最长时间。 它能阻止异常运行的服务持续运行并耗尽服务器资源。 截止时间对于构建可靠应用非常有效,应该进行配置。

截止时间配置:

  • 在进行调用时,使用 CallOptions.Deadline 配置截止时间。
  • 没有截止时间默认值。 gRPC 调用没有时间限制,除非指定了截止时间。
  • 截止时间指的是超过截止时间的 UTC 时间。 例如,DateTime.UtcNow.AddSeconds(5) 是从现在起 5 秒的截止时间。
  • 如果使用的是过去或当前的时间,则调用将立即超过截止时间。
  • 截止时间随 gRPC 调用发送到服务,并由客户端和服务独立跟踪。 gRPC 调用可能在一台计算机上完成,但当响应返回给客户端时,已超过了截止时间。

如果超过了截止时间,客户端和服务将有不同的行为:

  • 客户端将立即中止基础的 HTTP 请求并引发 DeadlineExceeded 错误。 客户端应用可以选择捕获错误并向用户显示超时消息。
  • 在服务器上,将中止正在执行的 HTTP 请求,并引发 ServerCallContext.CancellationToken。 尽管中止了 HTTP 请求,gRPC 调用仍将继续在服务器上运行,直到方法完成。 将取消令牌传递给异步方法,使其随调用一同被取消,这非常重要。 例如,向异步数据库查询和 HTTP 请求传递取消令牌。 传递取消令牌让取消的调用可以在服务器上快速完成,并为其他调用释放资源。

配置 CallOptions.Deadline 以设置 gRPC 调用的截止时间:

var client = new Greet.GreeterClient(channel);

try
{
    var response = await client.SayHelloAsync(
        new HelloRequest { Name = "World" },
        deadline: DateTime.UtcNow.AddSeconds(5));
    
    // Greeting: Hello World
    Console.WriteLine("Greeting: " + response.Message);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
    Console.WriteLine("Greeting timeout.");
}

在 gRPC 服务中使用 ServerCallContext.CancellationToken

public override async Task<HelloReply> SayHello(HelloRequest request,
    ServerCallContext context)
{
    var user = await _databaseContext.GetUserAsync(request.Name,
        context.CancellationToken);

    return new HelloReply { Message = "Hello " + user.DisplayName };
}

截止时间和重试

当 gRPC 调用配置了重试故障处理和截止日期时,截止日期会跟踪 gRPC 调用的所有重试时间。 如果超过了截止时间,gRPC 调用会立即中止底层 HTTP 请求,跳过任何剩余的重试,并引发 DeadlineExceeded 错误。

传播截止时间

从正在执行的 gRPC 服务进行 gRPC 调用时,应传播截止时间。 例如:

  1. 客户端应用调用带有截止时间的 FrontendService.GetUser
  2. FrontendService 调用 UserService.GetUser。 客户端指定的截止时间应随新的 gRPC 调用进行指定。
  3. UserService.GetUser 接收截止时间。 如果超过了客户端应用的截止时间,将正确超时。

调用上下文将使用 ServerCallContext.Deadline 提供截止时间:

public override async Task<UserResponse> GetUser(UserRequest request,
    ServerCallContext context)
{
    var client = new User.UserServiceClient(_channel);
    var response = await client.GetUserAsync(
        new UserRequest { Id = request.Id },
        deadline: context.Deadline);

    return response;
}

手动传播截止时间可能会很繁琐。 截止时间需要传递给每个调用,很容易不小心错过。 gRPC 客户端工厂提供自动解决方案。 指定 EnableCallContextPropagation

  • 自动将截止时间和取消令牌传播到子调用。
  • 如果子调用指定较早的截止时间,则不传播截止时间。 例如,如果子调用使用 CallOptions.Deadline 指定新的 5 秒截止时间,则不使用传播的 10 秒截止时间。 当多个截止时间可用时,使用最早的截止时间。
  • 这是确保复杂的嵌套 gRPC 场景始终传播截止时间和取消的一种极佳方式。
services
    .AddGrpcClient<User.UserServiceClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .EnableCallContextPropagation();

有关详细信息,请参阅 .NET 中的 gRPC 客户端工厂集成

取消

取消功能让 gRPC 客户端可以取消不再需要的长期运行的调用。 例如,当用户访问网站上的页面时,将启动流实时更新的 gRPC 调用。 当用户离开页面时,应取消流。

通过传递带有 CallOptions.CancellationToken 的取消令牌,或通过调用 Dispose,可以在客户端中取消 gRPC 调用。

private AsyncServerStreamingCall<HelloReply> _call;

public void StartStream()
{
    _call = client.SayHellos(new HelloRequest { Name = "World" });

    // Read response in background task.
    _ = Task.Run(async () =>
    {
        await foreach (var response in _call.ResponseStream.ReadAllAsync())
        {
            Console.WriteLine("Greeting: " + response.Message);
        }
    });
}

public void StopStream()
{
    _call.Dispose();
}

可以取消的 gRPC 服务应:

  • ServerCallContext.CancellationToken 传递给异步方法。 取消异步方法可以使服务器上的调用快速完成。
  • 将取消令牌传播给子调用。 传播取消令牌可确保子调用与其父级一起取消。 gRPC 客户端工厂EnableCallContextPropagation() 自动传播取消令牌。

其他资源