gRPC-微服務間通訊實踐

ddockerman發表於2020-09-30

微服務間通訊常見的兩種方式

由於微服務架構慢慢被更多人使用後,迎面而來的問題是如何做好微服務間通訊的方案。我們先分析下目前最常用的兩種服務間通訊方案。

gRPC(rpc遠端呼叫)

場景:A服務主動發起請求到B服務,同步方式
範圍:只在微服務間通訊應用

EventBus(基於訊息佇列的整合事件)

技術:NotNetCore.Cap + Rabbitmq + Database
場景:A服務要在B服務做某件事情後響應,非同步方式
實現:B服務在完成某件事情後釋出訊息,A服務訂閱此訊息
範圍:只在微服務間通訊應用

通過對比,兩種方式完全不一樣。rpc是類似於http請求的及時響應機制,但是比http更輕量、快捷,它更像以前的微軟的WCF,可以自動生成客戶端程式碼,充分體現了面向實體物件的遠端呼叫的思想;Eventbus是非同步的訊息機制,基於cap的思想,不關心下游訂閱方服務是否消費成功,保障了主服務業務的流暢性,同時也是一款分散式事務的實現方案,可以保障分散式架構中的資料的最終一致性。

我們今天主要介紹gRPC在微服務中的實踐案例。

gRPC-Server(服務端)

框架介紹

  • .Net Core sdk 3.1
  • Grpc.AspNetCore 2.30.0
  • Grpc.Core 2.30.0

搭建步驟

以.net core webapi 專案為例,詳細說明如何整合gRPC。

建立專案

建立web api專案,此步驟說明省略

引入nuget包

引入gRPC 服務端需要的 nuget包,Grpc.AspNetCore 2.30.0和Grpc.Core 2.30.0

外部訪問

考慮到專案釋出後,有webapi本身的http的介面和gRPC的介面都要給外部訪問,就需要暴露http1和http2兩個埠。

方式1:本地除錯時,可以直接暴露http和https,如果你的伺服器支援https,也可以在生產環境使用https來訪問gRPC服務。

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseNLog()
                .UseUrls("http://*:5000;https://*:5001");

方式2:如果在容器化部署場景下,一般會在dockerfile中指定ASPNETCORE_PORT環境變數,然後程式監聽http1和http2兩個埠。

 public static IHostBuilder CreateHostBuilder(string[] args) =>
          Host.CreateDefaultBuilder(args)
              .ConfigureWebHostDefaults(webBuilder =>
              {
                  var aspnetcorePort = Environment.GetEnvironmentVariable("ASPNETCORE_PORT") ?? 5000;
                  int.TryParse(aspnetcorePort, out int  port);
                  webBuilder.ConfigureKestrel(options =>
                  {
                      options.ListenAnyIP(port, options => options.Protocols = HttpProtocols.Http1);
                      options.ListenAnyIP(port + 1, options => options.Protocols = HttpProtocols.Http2);
                  })
                  .UseStartup<Startup>();
                  webBuilder.UseNLog();
              });

異常處理

由於gRPC服務端只能throw 基於 Grpc.Core.RpcException 的異常型別,所以我們可以自定義中介軟體來統一處理下異常

using Grpc.Core;
using Grpc.Core.Interceptors;
using System;
using System.Threading.Tasks;

public class ExceptionInterceptor : Interceptor
    {
        public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
           TRequest request,
           ServerCallContext context,
           UnaryServerMethod<TRequest, TResponse> continuation
           )
        {
            try
            {
                return await continuation(request, context);
            }
            catch (RpcException ex)
            {
                throw ex;
            }
            catch (Exception ex)
            {
                throw new RpcException(new Status(StatusCode.Internal, ex.Message + "\r\n" + ex.StackTrace));
            }
        }
    }

程式碼中被繼承的 Interceptor 是 Grpc.Core.Interceptors.Interceptor。主要處理的目的是把在gRPC介面中丟擲的非 RpcException 的異常,轉換為 RpcException。此中介軟體也是根據具體的業務需求來做的,主要是告訴大家可以重寫 Grpc.Core.Interceptors.Interceptor 的攔截器來統一處理一些事情。

定義協議緩衝區(protocol3)

新建項搜尋rpc可以出現協議緩衝區檔案

定義示例介面,建立訂單方法,以及建立訂單入參和出參。關於proto3協議具體說明,請參考往期文章。

syntax = "proto3";

option csharp_namespace = "GrpcTest.Protos";

service Order {
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderReply);
}

  message CreateOrderRequest {
    string ItemCode  = 1;
    string ItemName = 2;
    string Spec = 3;
    double Price = 4;
    double Quantity = 5;
    string Unit = 6;
    double Cost = 7;
}

message CreateOrderReply {
  bool success = 1;
}

在專案的csproj檔案中,需要有proto包含進去,GrpcServices="Server"表示當前是服務端。改好後重新生成下專案。

<ItemGroup>
	<Protobuf Include="Protos/GrpcTest.Protos" GrpcServices="Server" />
 </ItemGroup>

建立OrderService

手動建立OrderService,繼承自Order.OrderBase(proto自動生成的程式碼)

 public class OrderService : Order.OrderBase
    {
        public async override Task<CreateOrderReply> CreateOrder(CreateOrderRequest request, ServerCallContext context)
        {
            //todo something

            //throw RpcException異常
            throw new RpcException(new Status(StatusCode.NotFound, "資源不存在"));

            //返回
            return new CreateOrderReply
            {
                Success = true
            };
        }
    }

重寫CreateOrder方法,此處就可以寫你的實際的業務程式碼,相當於Controller介面入口。如果業務中需要主動丟擲異常,可以使用RpcException,有定義好的一套狀態碼和異常封裝。

修改Startup

在ConfigureServices方法中加入AddGrpc,以及上面提到的異常處理中介軟體,程式碼如下

services.AddGrpc(option => option.Interceptors.Add<ExceptionInterceptor>());

在Configure方法中將OrderService啟用,程式碼如下

app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<OrderService>();
               
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("this is a gRPC server");
                });
            });

至此 gRPC服務端搭建完成。

gRPC-Client(客戶端)

框架介紹

  • .Net Core sdk 3.1
  • Google.Protobuf 3.12.4
  • Grpc.Tools 2.30.0
  • Grpc.Net.ClientFactory 2.30.0

搭建步驟

以.net core webapi 專案為例,詳細說明如何整合gRPC客戶端

建立專案

建立web api專案,此步驟說明省略

引入nuget包

引入gRPC 客戶端需要的 nuget包,Google.Protobuf 3.12.4、Grpc.Tools 2.30.0和Grpc.Net.ClientFactory 2.30.0

引入proto檔案

將服務端的 order.proto 拷貝到客戶端的web api專案中,並在csproj檔案中新增ItemGroup節點。GrpcServices="Client"表示當前是客戶端。改好後重新生成下專案。

<ItemGroup>
	<Protobuf Include="Protos/OutpAggregation.proto" GrpcServices="Client" />
  </ItemGroup>

修改Startup

在ConfigureServices方法中加入AddGrpcClient,程式碼如下

 services.AddHttpContextAccessor(); 
 AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
 
 var baseUrl = "http://localhost:5001/";
 services.AddGrpcClient<Order.OrderClient>(
            options =>
                {
                    options.Address = new Uri(baseUrl);
                });

注意:要使用.NET Core客戶端呼叫不安全的gRPC服務,需要進行其他配置。 gRPC客戶端必須將System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport開關設定為true,並在伺服器地址中使用http。可以在以下連結檢視具體說明。

[Troubleshoot gRPC on .NET Core]

另外說明下services.AddGrpcClient方法,來自於nuget包Grpc.Net.ClientFactory 2.30.0,將gRPC客戶端的注入封裝,具體程式碼實現可以檢視以下連結。

Grpc.Net.ClientFactory

客戶端呼叫

以在Controller中呼叫為例,示例程式碼如下

    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private readonly Order.OrderClient _orderClient;

        public WeatherForecastController(Order.OrderClient orderClient)
        {
            _orderClient = orderClient;
        }

        [HttpGet]
        public async Task<IEnumerable<WeatherForecast>> Get()
        {
           var result = await _orderClient.CreateOrderAsync(new CreateOrderRequest 
           {
               ItemCode = "123",
               ItemName = "名稱1"
           });
        }
    }

通過建構函式注入gRPC客戶端,然後就可以使用裡面的同步或者非同步方法啦!

相關文章