具有截止时间和取消功能的可靠的 gRPC 服务
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
截止时间和取消功能是 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 调用时,应传播截止时间。 例如:
- 客户端应用调用带有截止时间的
FrontendService.GetUser
。 FrontendService
调用UserService.GetUser
。 客户端指定的截止时间应随新的 gRPC 调用进行指定。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()
自动传播取消令牌。