1. 引言
最近翻看最新3.0 eShopOncontainers源码,发现其在架构选型中补充了 gRPC 进行服务间通信。那就索性也写一篇,作为系列的补充。
2. gRPC
老规矩,先来理一下gRPC的基本概念。gRPC是Google开源的RPC框架,比肩dubbo、thrift、brpc。其优势在于:
基于proto buffer:二进制协议,具有高性能的序列化机制。相较于JSON(文本协议)而言,首先从数据包上就有60%-80%的减小,其次其解包速度仅需要简单的数学运算完成,无需复杂的词法语法分析,具有8倍以上的性能提升。
基于proto 文件:可以更方便的在客户端和服务端之间进行交互。
gRPC语言无关性: 所有服务都是使用原型文件定义的。这些文件基于protobuffer语言,并定义服务的接口。基于原型文件,可以为每种语言生成用于创建服务端和客户端的代码。其中protoc编译工具就支持将其生成C #代码。从.NET Core 3 中,gRPC在工具和框架中深度集成,开发者会有更好的开发体验。
支持数据流。
3. gRPC 在 eShopOncontainers 的应用
首先来理一下eShopOncontainers 中服务间同步通信的技术选型,主要还是是基于HTTP/REST,gRPC作为补充。
在eShopOncontainers中Ordering API、Catalog API、Basket API微服务通过gRPC端点暴露服务。其中Mobile Shopping、Web Shopping BFFs使用gRPC客户端访问服务。以下以Ordering API gRPC 服务举例说明。
订单微服务中定义了一个gRPC服务,用于从购物车创建订单。
3.1 服务端实现
proto文件定义如下:
syntax = "proto3";
option csharp_namespace = "GrpcOrdering";
package OrderingApi;
service OrderingGrpc {rpc CreateOrderDraftFromBasketData(CreateOrderDraftCommand) returns (OrderDraftDTO) {}
}
message CreateOrderDraftCommand {string buyerId = 1;repeated BasketItem items = 2;
}
message BasketItem {string id = 1;int32 productId = 2;string productName = 3;double unitPrice = 4;double oldUnitPrice = 5;int32 quantity = 6;string pictureUrl = 7;
}
message OrderDraftDTO {double total = 1;repeated OrderItemDTO orderItems = 2;
}
message OrderItemDTO {int32 productId = 1;string productName = 2;double unitPrice = 3;double discount = 4;int32 units = 5;string pictureUrl = 6;
}
服务实现,主要是借助Mediator充当CommandBus进行命令分发,具体实现如下:
namespace GrpcOrdering
{public class OrderingService : OrderingGrpc.OrderingGrpcBase{private readonly IMediator _mediator;private readonly ILogger<OrderingService> _logger;public OrderingService(IMediator mediator, ILogger<OrderingService> logger){_mediator = mediator;_logger = logger;}public override async Task<OrderDraftDTO> CreateOrderDraftFromBasketData(CreateOrderDraftCommand createOrderDraftCommand, ServerCallContext context){_logger.LogInformation("Begin gRPC call from method {Method} for ordering get order draft {CreateOrderDraftCommand}", context.Method, createOrderDraftCommand);_logger.LogTrace("----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})",createOrderDraftCommand.GetGenericTypeName(),nameof(createOrderDraftCommand.BuyerId),createOrderDraftCommand.BuyerId,createOrderDraftCommand);var command = new AppCommand.CreateOrderDraftCommand(createOrderDraftCommand.BuyerId,this.MapBasketItems(createOrderDraftCommand.Items));var data = await _mediator.Send(command);if (data != null){context.Status = new Status(StatusCode.OK, $" ordering get order draft {createOrderDraftCommand} do exist");return this.MapResponse(data);}else{context.Status = new Status(StatusCode.NotFound, $" ordering get order draft {createOrderDraftCommand} do not exist");}return new OrderDraftDTO();}public OrderDraftDTO MapResponse(AppCommand.OrderDraftDTO order){var result = new OrderDraftDTO(){Total = (double)order.Total,};order.OrderItems.ToList().ForEach(i => result.OrderItems.Add(new OrderItemDTO(){Discount = (double)i.Discount,PictureUrl = i.PictureUrl,ProductId = i.ProductId,ProductName = i.ProductName,UnitPrice = (double)i.UnitPrice,Units = i.Units,}));return result;}public IEnumerable<ApiModels.BasketItem> MapBasketItems(RepeatedField<BasketItem> items){return items.Select(x => new ApiModels.BasketItem(){Id = x.Id,ProductId = x.ProductId,ProductName = x.ProductName,UnitPrice = (decimal)x.UnitPrice,OldUnitPrice = (decimal)x.OldUnitPrice,Quantity = x.Quantity,PictureUrl = x.PictureUrl,});}}
}
同时,服务端还要注册gRPC的请求处理管道:
app.UseEndpoints(endpoints =>
{endpoints.MapDefaultControllerRoute();endpoints.MapControllers();endpoints.MapGrpcService<OrderingService>();
});
3.2 客户端调用
接下来看下客户端[web.bff.shopping]怎么消费的:
public class OrderingService : IOrderingService{private readonly UrlsConfig _urls;private readonly ILogger<OrderingService> _logger;public readonly HttpClient _httpClient;public OrderingService(HttpClient httpClient, IOptions<UrlsConfig> config, ILogger<OrderingService> logger){_urls = config.Value;_httpClient = httpClient;_logger = logger;}public async Task<OrderData> GetOrderDraftAsync(BasketData basketData){return await GrpcCallerService.CallService(_urls.GrpcOrdering, async channel =>{var client = new OrderingGrpc.OrderingGrpcClient(channel);_logger.LogDebug(" gRPC client created, basketData={@basketData}", basketData);var command = MapToOrderDraftCommand(basketData);var response = await client.CreateOrderDraftFromBasketDataAsync(command);_logger.LogDebug(" gRPC response: {@response}", response);return MapToResponse(response, basketData);});}private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData){if (orderDraft == null){return null;}var data = new OrderData{Buyer = basketData.BuyerId,Total = (decimal)orderDraft.Total,};orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData{Discount = (decimal)o.Discount,PictureUrl = o.PictureUrl,ProductId = o.ProductId,ProductName = o.ProductName,UnitPrice = (decimal)o.UnitPrice,Units = o.Units,}));return data;}private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData){var command = new CreateOrderDraftCommand{BuyerId = basketData.BuyerId,};basketData.Items.ForEach(i => command.Items.Add(new BasketItem{Id = i.Id,OldUnitPrice = (double)i.OldUnitPrice,PictureUrl = i.PictureUrl,ProductId = i.ProductId,ProductName = i.ProductName,Quantity = i.Quantity,UnitPrice = (double)i.UnitPrice,}));return command;}}
其中, GrpcCallerService
是对gRPC Client的一层封装,主要是为了解决未启用TLS无法使用gRPC的问题。
4. 不启用TLS使用gRPC
我们已经知道gRpc 是基于HTTP2.0 协议。然而,连接的建立,默认并不是一步到位直接基于HTTP2.0建立连接的。客户端是先基于HTTP1.1进行协议协商,协商成功后,确认服务端支持HTTP2.0后,才会建立HTT2.0连接,协议协商需要TLS的ALPN协议来实现。流程如下:
这意味着,默认情况下,您需要启用TLS协议才能完成HTTP2.0协议协商,进而才能使用gRPC。
然而,在微服务架构中,并不是所有服务都需要启用安全传输层协议,尤其是微服务间的内部调用。那么在微服务内部如何使用gRPC进行通信呢?
客户端绕过协议协商,直连HTTP2.0(前提是:服务端必须支持HTTP2.0)。
服务端配置如下:
WebHost.CreateDefaultBuilder(args).ConfigureKestrel(options =>{options.Listen(IPAddress.Any, ports.httpPort, listenOptions =>{listenOptions.Protocols = HttpProtocols.Http1AndHttp2; //同时监听协议HTTP1,HTTP2});options.Listen(IPAddress.Any, ports.gRPCPort, listenOptions =>{listenOptions.Protocols = HttpProtocols.Http2; // gRPC端口仅监听HTTP2.0});})
客户端需要添加以下设置,这些设置只能在客户端开始时设置一次:
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);
知道了这些,再回过来看 GrpcCallerService
的实现,就一目了然了。
public static class GrpcCallerService
{public static async Task<TResponse> CallService<TResponse>(string urlGrpc, Func<GrpcChannel, Task<TResponse>> func)
{AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);var channel = GrpcChannel.ForAddress(urlGrpc);/*using var httpClientHandler = new HttpClientHandler{ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }};*/Log.Information(@"Creating gRPC client base address urlGrpc ={@urlGrpc}, BaseAddress={@BaseAddress} ", urlGrpc, channel.Target);try{return await func(channel);}catch (RpcException e){Log.Error("Error calling via gRPC: {Status} - {Message}", e.Status, e.Message);return default;}finally{AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false);AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", false);}}public static async Task CallService(string urlGrpc, Func<GrpcChannel, Task> func)
{AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);/*using var httpClientHandler = new HttpClientHandler{ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }};*/var channel = GrpcChannel.ForAddress(urlGrpc);Log.Debug("Creating gRPC client base address {@httpClient.BaseAddress} ", channel.Target);try{await func(channel);}catch (RpcException e){Log.Error("Error calling via gRPC: {Status} - {Message}", e.Status, e.Message);}finally{AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false);AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", false);}}
}
5. 最后
本文简要介绍了 eShopOnContainers 如何通过集成 gRPC 来完善服务间同步通信机制,希望对你在对微服务进行RPC相关技术选型时有一定的启示和帮助。
参考资料:
[HTTP2.0笔记之连接建立:http://www.blogjava.net/yongboy/archive/2015/03/18/423570.html]
[eShopOnContainers/wiki/gRPC:https://github.com/dotnet-architecture/eShopOnContainers/wiki/gRPC]
[Google Protocol Buffer 的使用和原理:https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/index.html]