eShopOnContainers 知多少[11]:服务间通信之gRPC
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]
總結
以上是生活随笔為你收集整理的eShopOnContainers 知多少[11]:服务间通信之gRPC的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 闲谈设计模式
- 下一篇: 龙芯开源社区上线.NET主页
