.Net Core gRPC 实战(二)
概述
gRPC 客户端必须使用与服务相同的连接级别安全性。 如调用服务时通道和服务的连接级别安全性不一致,gRPC 客户端就会抛出错误。
gRPC 配置使用HTTP
gRPC 客户端传输层安全性 (TLS) 是在创建 gRPC 通道时服务器地址以https开头配置的。若要配置为http协议做如下修改
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
GrpcChannel.ForAddress("http://localhost:5000")
关于通道和客户端的说明
- 创建通道成本高昂。 重用 gRPC 调用的通道可提高性能。
- gRPC 客户端是使用通道创建的。 gRPC 客户端是轻型对象,无需缓存或重用。
配置截止时间
建议配置 gRPC 调用的截止时间,因为它限制调用时间的上限,阻止异常运行的服务持续运行并耗尽服务器资源。 截止时间对于构建可靠应用非常有效。
进行调用时,使用 CallOptions.Deadline
配置截止时间。
如果超过了截止时间,客户端和服务将有不同的行为:
- 客户端将立即中止基础的 HTTP 请求并引发
DeadlineExceeded
错误。 客户端可以选择捕获错误并向用户显示超时消息。 - 服务器将中止正在执行的 HTTP 请求,并引发 ServerCallContext.CancellationToken。 尽管中止了 HTTP 请求,gRPC 调用仍将继续运行直到方法完成。 将取消令牌传递给异步方法,使其随调用一同被取消。 例如,向异步数据库查询和 HTTP 请求传递取消令牌。 传递取消令牌让取消的调用可以在服务器上快速完成,并为其他调用释放资源。
客户端代码示例:
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."); }
服务器代码示例:
var response = await client.GetUserAsync( new UserRequest { Id = request.Id }, deadline: context.Deadline);
注册 gRPC 客户端
在 Startup类的ConfigureServices方法中,使用 AddGrpcClient 扩展方法指定 gRPC客户端类和服务地址。
services.AddGrpcClient<Greeter.GreeterClient>(o => { o.Address = new Uri("https://localhost:5001"); });
ASP.NET Core MVC 控制器和 gRPC 服务等通过构造函数等方式自动注入。
配置 HttpHandler
.ConfigurePrimaryHttpMessageHandler(() => new GrpcWebHandler(GrpcWebMode.GrpcWebText, new HttpClientHandler()));
配置通道和拦截器
通道
通道(Channel)是.Net Core 3.X引入的类型,Channel是线程安全的,Channel的预期用例是多线程场景,可以实现多线程之间通信。类似Golang的chan类型。
通道相关文章:https://webmote.blog.csdn.net/article/details/115361367
gRPC配置通道示例:
services .AddGrpcClient<Greeter.GreeterClient>(o => { o.Address = new Uri("https://localhost:5001"); }) .ConfigureChannel(o => { var credentials = CallCredentials.FromInterceptor((context, metadata) => { if (!string.IsNullOrEmpty(_token)) { metadata.Add("Authorization", $"Bearer {_token}"); } return Task.CompletedTask; }); o.Credentials = ChannelCredentials.Create(new SslCredentials(), credentials); });
拦截器
具有面向切面的思想,可以在调用服务的时候进行一些统一处理, 很适合在这里处理验证、日志等流程。
Interceptor类是gRPC服务拦截器的基类,是一个抽象类
各个方法作用如下:
方法名称 |
作用 |
---|---|
BlockingUnaryCall |
拦截阻塞调用 |
AsyncUnaryCall |
拦截异步调用 |
AsyncServerStreamingCall |
拦截异步服务端流调用 |
AsyncClientStreamingCall |
拦截异步客户端流调用 |
AsyncDuplexStreamingCall |
拦截异步双向流调用 |
UnaryServerHandler |
用于拦截和传入普通调用服务器端处理程序 |
ClientStreamingServerHandler |
用于拦截客户端流调用的服务器端处理程序 |
ServerStreamingServerHandler |
用于拦截服务端流调用的服务器端处理程序 |
DuplexStreamingServerHandler |
用于拦截双向流调用的服务器端处理程序 |
在实际使用中,可以根据自己的需要来使用对应的拦截方法。
本文示例为创建一个客户端拦截器ClientLoggerInterceptor,该类继承Interceptor。按需实现方法,这里我客户端调用的是SayHelloAsync方法则实现对应的AsyncUnaryCall方法。
注册拦截器:
运行效果图:
服务器端拦截器同理,继承Interceptor类实现对应方法。
服务器端注入方式:
services.AddGrpc(options => { options.Interceptors.Add<ServerLoggerInterceptor>(); });
调用取消
可以使用 EnableCallContextPropagation() 对 gRPC 服务中工厂所创建的 gRPC 客户端进行配置,以自动将截止时间和取消令牌传播到子调用。
手动传播截止时间可能会很繁琐。 截止时间需要传递给每个调用,很容易不小心错过。 gRPC 客户端工厂提供自动解决方案。 指定 EnableCallContextPropagation
:
- 自动将截止时间和取消令牌传播到子调用。
- 这是确保复杂的嵌套 gRPC 场景始终传播截止时间和取消的一种极佳方式。
services .AddGrpcClient<User.UserServiceClient>(o => { o.Address = new Uri("https://localhost:5001"); }) .EnableCallContextPropagation();
如果客户端在 gRPC 调用的上下文之外使用,EnableCallContextPropagation
将引发错误。 此错误旨在提醒你没有要传播的调用上下文。 如果要在调用上下文之外使用客户端,请使用 SuppressContextNotFoundErrors
在配置客户端时禁止显示该错误:
services .AddGrpcClient<Greeter.GreeterClient>(o => { o.Address = new Uri("https://localhost:5001"); }) .EnableCallContextPropagation(o => o.SuppressContextNotFoundErrors = true);
配置 gRPC 重试策略
重试策略在创建 gRPC 通道时配置
var defaultMethodConfig = new MethodConfig { Names = { MethodName.Default }, RetryPolicy = new RetryPolicy { MaxAttempts = 5, InitialBackoff = TimeSpan.FromSeconds(1), MaxBackoff = TimeSpan.FromSeconds(5), BackoffMultiplier = 1.5, RetryableStatusCodes = { StatusCode.Unavailable } } }; var channel = GrpcChannel.ForAddress("http://localhost:5000", new GrpcChannelOptions { LoggerFactory = loggerFactory, ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } } });
重试策略可以按方法配置,而方法可以使用
Names
属性进行匹配。 此方法配置有MethodName.Default
,因此它将应用于此通道调用的所有 gRPC 方法。
gRPC 重试选项
下表描述了用于配置 gRPC 重试策略的选项:
选项 | 描述 |
---|---|
MaxAttempts |
最大调用尝试次数,包括原始尝试。 此值受 GrpcChannelOptions.MaxRetryAttempts (默认值为 5)的限制。 必须为该选项提供值,且值必须大于 1。 |
InitialBackoff |
重试尝试之间的初始退避延迟。 介于 0 与当前退避之间的随机延迟确定何时进行下一次重试尝试。 每次尝试后,当前退避将乘以 BackoffMultiplier 。 必须为该选项提供值,且值必须大于 0。 |
MaxBackoff |
最大退避会限制指数退避增长的上限。 必须为该选项提供值,且值必须大于 0。 |
BackoffMultiplier |
每次重试尝试后,退避将乘以该值,并将在乘数大于 1 的情况下以指数方式增加。 必须为该选项提供值,且值必须大于 0。 |
RetryableStatusCodes |
状态代码的集合。 具有匹配状态的失败 gRPC 调用将自动重试。 有关状态代码的更多信息,请参阅状态代码及其在 gRPC 中的用法。 至少需要提供一个可重试的状态代码。 |
配置 gRPC hedging 策略
Hedging 是一种备选重试策略。 Hedged gRPC 调用可以在服务器上执行多次,并获取第一个成功的结果。
重要的是,务必仅针对可安全执行多次且不会造成负面影响的方法启用 hedging。且hedging 策略不能与重试策略结合使用。
Hedging 具有以下优缺点:
- Hedging 的优点是,它可能更快地返回成功的结果。 它允许同时进行多个 gRPC 调用,并在出现第一个成功的结果时完成。
- Hedging 的一个缺点是它可能会造成浪费。 进行了多个调用并且这些调用全部成功。 仅使用第一个结果放弃其余结果。
Hedging 策略的配置类似于重试策略:
var defaultMethodConfig = new MethodConfig { Names = { MethodName.Default }, HedgingPolicy = new HedgingPolicy { MaxAttempts = 5, NonFatalStatusCodes = { StatusCode.Unavailable } } }; var channel = GrpcChannel.ForAddress("http://localhost:5000", new GrpcChannelOptions { LoggerFactory = loggerFactory, ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } } });
gRPC hedging 选项
下表描述了用于配置 gRPC hedging 策略的选项:
选项 | 描述 |
---|---|
MaxAttempts |
Hedging 策略将发送的调用数量上限。 MaxAttempts 表示所有尝试的总数,包括原始尝试。 此值受 GrpcChannelOptions.MaxRetryAttempts (默认值为 5)的限制。 必须为该选项提供值,且值必须大于 2。 |
HedgingDelay |
第一次调用立即发送,但后续 hedging 调用将按该值延迟发送。 如果延迟设置为零或 null ,那么所有所有 hedged 调用都将立即发送。 默认值为 0。 |
NonFatalStatusCodes |
指示其他 hedge 调用仍可能会成功的状态代码集合。 如果服务器返回非致命状态代码,hedged 调用将继续。 否则,将取消未完成的请求,并将错误返回到应用。 有关状态代码的更多信息,请参阅状态代码及其在 gRPC 中的用法。 |
Github
本文示例代码:https://github.com/MayueCif/GrpcDemo