eShopOnContainers 知多少[11]:服务间通信之gRPC
1. 引言
最近翻看最新3.0 eShopOncontainers源碼,發(fā)現(xiàn)其在架構(gòu)選型中補充了 gRPC 進(jìn)行服務(wù)間通信。那就索性也寫一篇,作為系列的補充。
2. gRPC
老規(guī)矩,先來理一下gRPC的基本概念。gRPC是Google開源的RPC框架,比肩dubbo、thrift、brpc。其優(yōu)勢在于:
基于proto buffer:二進(jìn)制協(xié)議,具有高性能的序列化機制。相較于JSON(文本協(xié)議)而言,首先從數(shù)據(jù)包上就有60%-80%的減小,其次其解包速度僅需要簡單的數(shù)學(xué)運算完成,無需復(fù)雜的詞法語法分析,具有8倍以上的性能提升。
基于proto 文件:可以更方便的在客戶端和服務(wù)端之間進(jìn)行交互。
gRPC語言無關(guān)性:?所有服務(wù)都是使用原型文件定義的。這些文件基于protobuffer語言,并定義服務(wù)的接口。基于原型文件,可以為每種語言生成用于創(chuàng)建服務(wù)端和客戶端的代碼。其中protoc編譯工具就支持將其生成C #代碼。從.NET Core 3 中,gRPC在工具和框架中深度集成,開發(fā)者會有更好的開發(fā)體驗。
支持?jǐn)?shù)據(jù)流。
3. gRPC?在 eShopOncontainers?的應(yīng)用
首先來理一下eShopOncontainers 中服務(wù)間同步通信的技術(shù)選型,主要還是是基于HTTP/REST,gRPC作為補充。
在eShopOncontainers中Ordering API、Catalog API、Basket API微服務(wù)通過gRPC端點暴露服務(wù)。其中Mobile Shopping、Web Shopping BFFs使用gRPC客戶端訪問服務(wù)。以下以O(shè)rdering API gRPC 服務(wù)舉例說明。
訂單微服務(wù)中定義了一個gRPC服務(wù),用于從購物車創(chuàng)建訂單。
3.1 服務(wù)端實現(xiàn)
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; }服務(wù)實現(xiàn),主要是借助Mediator充當(dāng)CommandBus進(jìn)行命令分發(fā),具體實現(xiàn)如下:
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,});}} }同時,服務(wù)端還要注冊gRPC的請求處理管道:
app.UseEndpoints(endpoints => {endpoints.MapDefaultControllerRoute();endpoints.MapControllers();endpoints.MapGrpcService<OrderingService>(); });3.2 客戶端調(diào)用
接下來看下客戶端[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
我們已經(jīng)知道gRpc 是基于HTTP2.0 協(xié)議。然而,連接的建立,默認(rèn)并不是一步到位直接基于HTTP2.0建立連接的。客戶端是先基于HTTP1.1進(jìn)行協(xié)議協(xié)商,協(xié)商成功后,確認(rèn)服務(wù)端支持HTTP2.0后,才會建立HTT2.0連接,協(xié)議協(xié)商需要TLS的ALPN協(xié)議來實現(xiàn)。流程如下:
這意味著,默認(rèn)情況下,您需要啟用TLS協(xié)議才能完成HTTP2.0協(xié)議協(xié)商,進(jìn)而才能使用gRPC。
然而,在微服務(wù)架構(gòu)中,并不是所有服務(wù)都需要啟用安全傳輸層協(xié)議,尤其是微服務(wù)間的內(nèi)部調(diào)用。那么在微服務(wù)內(nèi)部如何使用gRPC進(jìn)行通信呢?
客戶端繞過協(xié)議協(xié)商,直連HTTP2.0(前提是:服務(wù)端必須支持HTTP2.0)。
服務(wù)端配置如下:
WebHost.CreateDefaultBuilder(args).ConfigureKestrel(options?=>{options.Listen(IPAddress.Any,?ports.httpPort,?listenOptions?=>{listenOptions.Protocols?=?HttpProtocols.Http1AndHttp2; //同時監(jiān)聽協(xié)議HTTP1,HTTP2});options.Listen(IPAddress.Any,?ports.gRPCPort,?listenOptions?=>{listenOptions.Protocols?=?HttpProtocols.Http2; // gRPC端口僅監(jiān)聽HTTP2.0});})客戶端需要添加以下設(shè)置,這些設(shè)置只能在客戶端開始時設(shè)置一次:
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",?true); AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support",?true);知道了這些,再回過來看 GrpcCallerService的實現(xiàn),就一目了然了。
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 來完善服務(wù)間同步通信機制,希望對你在對微服務(wù)進(jìn)行RPC相關(guān)技術(shù)選型時有一定的啟示和幫助。
參考資料:
[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]
總結(jié)
以上是生活随笔為你收集整理的eShopOnContainers 知多少[11]:服务间通信之gRPC的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 闲谈设计模式
- 下一篇: 使用sqlserver搭建高可用双机热备