.NET 微服务实践(9)-gRPC

215 阅读9分钟

本文系油管上一个系列教程的学习记录第九章,原链接是.NET Microservices – Full Course。本文分成五部分:gRPC介绍、配置集群内gRPC通信服务、平台服务集成gRPC、指令服务集成gRPC和调用平台服务、本地测试和重新部署到集群内。

Google Remoe Procedure Call (gRPC) 介绍

gRPC 的主要优点是:

  • 现代高性能轻量级 RPC 框架
  • 协定优先 API 开发,默认使用协议缓冲区,允许与语言无关的实现。
  • 可用于多种语言的工具,以生成强类型服务器和客户端。
  • 支持客户端、服务器和双向流式处理调用。
  • 使用 Protobuf 二进制序列化减少对网络的使用。

配置集群内gRPC通信服务

Untitled.png 在整个架构图中提到,平台服务和指令服务会有两类、共三种通信方式。其中一类是同步信息通讯,包括HTTP协议通讯以及gRPC通讯。在生产环境中,为了区分二者,则需要平台服务开放额外的端口,用于指令服务通过gRPC调用。

在Kubernetes架构中,预先为platformservice再配置一个ClusterIP的Port以及TargetPort对(这里两个端口都采用了2333)。

apiVersion: v1
kind: Service
metadata:
  name: platforms-clusterip-svc
spec:
  selector:
    app: platformservice
  ports:
  - name: platformservice
    protocol: TCP 
    port: 80
    targetPort: 80
  - name: platformgrps
    protocol: TCP
    port: 2333
    targetPort: 2333

重新部署Service

>>kubectl apply -f deployement platforms-depl.yaml
deployment.apps/platforms-depl unchanged
service/platform-clusterip-svc configured
>>kubectl get services
NAME                      TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                          AGE
commands-clusterip-svc    ClusterIP      10.97.40.210     <none>        80/TCP                           30d
kubernetes                ClusterIP      10.96.0.1        <none>        443/TCP                          30d
mssql-clusterip-svc       ClusterIP      10.107.148.45    <none>        1433/TCP                         30d
mssql-loadbalancer        LoadBalancer   10.104.109.162   localhost     1433:31137/TCP                   30d
platforms-clusterip-svc   ClusterIP      10.96.108.221    <none>        80/TCP,2333/TCP                   30d
platforms-np-svc          NodePort       10.101.93.173    <none>        80:30001/TCP                     30d
rabbitmq-clusterip-svc    ClusterIP      10.98.53.151     <none>        15672/TCP,5672/TCP               14d
rabbitmq-loadbalancer     LoadBalancer   10.102.90.146    localhost     15672:32088/TCP,5672:30621/TCP   14d

看到对于platforms-clusterip-svc,除了原有的80端口、还有2333端口。

平台服务集成gRPC

这一部分将在平台服务中集成gRPC。暴露出基于gRPC的Endpoint,用于调用平台服务。细分内容包括,依赖包安装、配置文件、配置协议、以及实现gRPC服务。

依赖包安装

作为服务端,平台服务需要安装Nuget包:Grpc.AspNetCore

<PackageReference Include="Grpc.AspNetCore" Version="2.50.0" />

配置文件

上文提到生产环境中,需要在平台服务中开设额外的端口,用于指令服务调用。所以在appsettings.Production.json文件中添加如下的配置:

"Kestrel": {
    "Endpoints": {
      "Grpc": {
        "Protocols": "Http2",
        "Url": "http://platforms-clusterip-svc:2333"
      },
      "WebApi": {
        "Protocols": "Http1",
        "Url": "http://platforms-clusterip-svc:80"
      }
    }
  }

其中Kestrel是AspNet.Core的默认Web服务器,其默认端口配置在Properties/launchSetting.json中(本地),而在容器化的过程中,默认采用了80端口作为HTTP/1协议的API端口。现在还多出了gRPC框架,另外开设了2333端口,协议采用HTTP/2协议。

配置协议

创建协议文件

在平台服务主项目目录下创建Protos文件夹

│        
├─Controllers
├─Data
├─PlatformDomain
├─Properties
├─Protos
└─Utils

在其中创建.proto文件(可以右键文件夹选择新建项,搜索“协议缓冲区文件”),创建后进行如下配置:

syntax = "proto3";

option csharp_namespace = "PlatformService.Protos";

service PlatformGrpc {
	rpc GetAllPlatforms (GetAllRequest) returns (GetAllResponse);
}

message GetAllRequest {
}

message PlatformGrpcDto {
	int32 platformId = 1;
	string name = 2;
	string publisher = 3;
}

message GetAllResponse {
	repeated PlatformGrpcDto platforms = 1;
}

协议说明:

  • 采用的是proto3的语法结构

  • 命名空间是PlatformService.Protos

  • 整个协议用C#语法等价表示如下:

    public class PlatformGrpc 
    {
    		public GetAllResponse GetAllPlatforms(GetAllRequest request)
        {
    
        }
    
        public class GetAllResponse 
        {
            public IEnumerable<PlatformGrpcDto> platforms { get; set; }
        }
    
        public class PlatformGrpcDto
        {
            public int PlatformId { get; set; }
            public string Name { get; set; }
            public string Publisher { get; set; }
        }
    }
    

    其中service相当于class,rpc相当于method,message即数据结构。

  • PlatformGrpcDto中属性后复制的数字,代表属性在传输的二进制文件中的顺序。

  • 由于属性最终以二进制文件的形式进行值传递,所以属性的类型定义将遵循二进制中的基本类型。对于值类型,这里采用的是int32

设定协议类型

在PlatformService的项目文件.csproj中配置.proto文件

<ItemGroup>
    <Protobuf Include="Protos\Platforms.proto" GrpcServices="Server" />
</ItemGroup>

编译整个项目工程!!!

实现gRPC服务

配置映射关系

在Util文件夹下的MappingProfile里,

└─Utils
    │  MappingProfile.cs
    ├─CommandService
    └─MessageBusService

添加在.proto文件中创建的PlatformGrpcDtoPlatform的映射关系:

CreateMap<Platform, PlatformGrpcDto>();

实现PlatformGrpcServie

在PlatformDomain文件夹下,创建PlatformGrpcService。该类继承PlatformGrpc.PlatformGrpcBase

using PlatformService.Protos;

namespace PlatformService.PlatformDomain
{
    public class PlatformGrpcService: PlatformGrpc.PlatformGrpcBase
    {
    }
}

其中PlatformGrpc.PlatformGrpcBase的命名空间就是早先定义的

option csharp_namespace = "PlatformService.Protos";

而这个类是C#编译器根据.proto文件创建的。

接下来为PlatformGrpcService创建构造器、并且注入必要的服务:

private readonly IPlatformRepository _repo;
private readonly IMapper _mapper;

public PlatformGrpcService(IPlatformRepository repo, IMapper mapper)
{
    _repo = repo;
    _mapper = mapper;
}

再重写它的GetAllPlatforms

public override async Task<GetAllResponse> GetAllPlatforms(GetAllRequest request, ServerCallContext context)
{
    var response = new GetAllResponse();
    var platforms = await _repo.GetAllPlatformsAsync();

    foreach (var plat in platforms)
    {
        var dto = _mapper.Map<PlatformGrpcDto>(plat);
        response.Platforms.Add(dto);
    }

    return response;
}

默认情况下,未重写的GetAllPlatforms方法为(在PlatformGrpc.PlatformGrpcBase中):

/// <summary>Base class for server-side implementations of PlatformGrpc</summary>
[grpc::BindServiceMethod(typeof(PlatformGrpc), "BindService")]
public abstract partial class PlatformGrpcBase
{
  [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
  public virtual global::System.Threading.Tasks.Task<global::PlatformService.Protos.GetAllResponse> GetAllPlatforms(global::PlatformService.Protos.GetAllRequest request, grpc::ServerCallContext context)
  {
    throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
  }

}

注意到

  • 尽管在.proto文件中GetAllResponse里的platforms是以驼峰法命名的;但是经过编译之后,定义的response(var response = new GetAllResponse();)访问Platform却是以Pascal法命名的。

注册并使用PlatformGrpcServie

Startup.cs文件中注册和使用gRPC

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();
    
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<PlatformGrpcService>();
    });
}

指令服务集成gRPC和调用平台服务

这一部分将在指令服务中集成gRPC,调用平台服务暴露的Endpoint。细分内容包括,依赖包安装、配置文件、配置协议、调用平台服务以及。

依赖包安装

作为客户端,指令服务需要安装Nuget包如下:

  • Google.Protobuf
  • Grpc.Net.Client
  • Grpc.Tools
<PackageReference Include="Google.Protobuf" Version="3.17.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.50.0" />
<PackageReference Include="Grpc.Tools" Version="2.51.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

配置文件

由于本地和生产环境,平台服务gRPC服务的Endpoint不同,因此需要分配配置。在本地(开发)环境下,需要在appsettings.Development.json文件中进行配置:

"PlatformGrpc": "http://localhost:5000"

这个端口号是平台服务在Kestrel服务器中的默认HTTP协议端口号。

在生产环境中,需要在appsettings.Production.json文件中添加如下的配置:

"PlatformGrpc": "http://platforms-clusterip-svc:2333"

IP地址和端口号是早先配置ClusterIP时设置的。

配置协议

复制协议文件

将平台服务中创建的.proto文件复制到如下目录

│        
├─Controllers
├─Data
├─Domains
├─Properties
├─Protos
└─Util

在其中创建.proto文件(可以右键文件夹选择新建项,搜索“协议缓冲区文件”),创建后进行如下配置

设定协议类型

在CommandService的项目文件.csproj中配置.proto文件

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

编译整个项目工程!!!

调用平台服务

配置映射关系

在Util文件夹下的MappingProfile里,

└─Util
    │  IEntityRepository.cs
    │  MappingProfile.cs
    ├─MessageBusService
    └─SubscriberService

添加在.proto文件中创建的PlatformGrpcDtoPlatform的映射关系:

CreateMap<PlatformGrpcDto, Platform>()
                .ForMember(dest => dest.ExternalId, src => src.MapFrom(x => x.PlatformId));

接口定义

在Util目录下创建PlatformGrpcService文件夹,继续在其中创建IPlatoformClient以及实现PlatformClient

using CommandService.Domains.Platforms;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace CommandService.Util.PlatformGrpcService
{
    public interface IPlatformClient
    {
        Task<IEnumerable<Platform>> GetAllPlatformsAsync();
    }
}

接口实现

using CommandService.Domains.Platforms;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace CommandService.Util.PlatformGrpcService
{
    public class PlatformClient : IPlatformClient
    {
        public async Task<IEnumerable<Platform>> GetAllPlatformsAsync()
        {
            throw new System.NotImplementedException();
        }
    }
}

首先创建构造器、注入必要的服务

private readonly IConfiguration _config;
private readonly IMapper _mapper;

public PlatformClient(IConfiguration config, IMapper mapper)
{
    _config = config;
    _mapper = mapper;
}

添加包引用:

using AutoMapper;
using Microsoft.Extensions.Configuration;

然后继承的GetAllPlatformsAsync方法中,创建gRPC客户端:

var channel = GrpcChannel.ForAddress(_config["PlatformGrpc"]);
var client = new PlatformGrpc.PlatformGrpcClient(channel);

其中PlatformGrpc.PlatformGrpcClient是C#编译器根据.proto文件创建的。

获取client之后,就可以调用GetAllPlatforms方法获取全部的平台数据:

public async Task<IEnumerable<Platform>> GetAllPlatformsAsync()
{
    Console.WriteLine($">>> Calling GRPC Server on the path: {_config["PlatformGrpc"]}");
    var channel = GrpcChannel.ForAddress(_config["PlatformGrpc"]);
    var client = new PlatformGrpc.PlatformGrpcClient(channel);
    var request = new GetAllRequest();

    try
    {
        var response = client.GetAllPlatforms(request);
        return response.Platforms.Select(x => _mapper.Map<Platform>(x));
    }
    catch (Exception ex)
    {
        Console.WriteLine($">>> Fail to call GRPC Server {ex.Message}");
        return null;
    }
}

注册并使用IPlatformClient

Startup.cs文件中注册IPlatformClient

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IPlatformClient, PlatformClient>();
}

业务整合

指令服务获取全部的平台数据,是为了在指令服务初始化时,就提供全部的平台信息。因此,需要App启动(重启)时调用数据灌入的方法、其从平台服务中获取全部平台数据,存到指令服务的内存中。

数据灌入定义与调用

在Data目录下创建PrepInMemoryDatabase.cs

├─Data
│      ApplicationDbContext.cs
│      CommandRepository.cs
│      EntityRepository.cs
│      PlatformRepository.cs
└─     PrepInMemoryDatabase.cs

再在其中创建PrepPopulation方法:

using CommandService.Domains.Platforms;
using Microsoft.AspNetCore.Builder;
using System.Collections.Generic;

namespace CommandService.Data
{
    public class PrepInMemoryDatabase
    {
        public static void PrepPopulation(IApplicationBuilder app)
        {
        }
    }
}

在Startup.cs中调用该方法:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    PrepInMemoryDatabase.PrepPopulation(app);
}

数据灌入实现

public static void PrepPopulation(IApplicationBuilder app)
{
    using (var serviceScope = app.ApplicationServices.CreateScope())
    {
        var grpcClient = serviceScope.ServiceProvider.GetService<IPlatformClient>();
        var platforms = grpcClient.GetAllPlatformsAsync().Result;

        SeedData(serviceScope.ServiceProvider.GetService<IPlatformRepository>(), platforms);
    }
}

private static void SeedData(IPlatformRepository repo, IEnumerable<Platform> platforms)
{
    Console.WriteLine(">>>Seeding new platforms...");

    if(platforms is not null)
    {
        foreach (var platform in platforms)
        {
            if (!repo.JudgePlatformExistencyAsync(platform.ExternalId).Result)
                repo.CreatePlatform(platform);
        }
        Task.FromResult(repo.SaveChangesAsync());
    }
}

由于IPlatformClient的生命周期定义为Scope,所以需要从Scope类的构造注入容器中取服务:

var serviceScope = app.ApplicationServices.CreateScope();
var grpcClient = serviceScope.ServiceProvider.GetService<IPlatformClient>();

取出服务后,即可调用远程调用平台服务获取全部平台数据:

var platforms = grpcClient.GetAllPlatformsAsync().Result;

然后将数据映射转化、判断去重后,存入指令服务的内存中。

本地测试

分别启动平台服务与指令服务,注意其中平台服务需要以Kestrel服务器形式(指令服务调用的是Kestrel服务器的默认端口,如果用的是IIS服务器端口、则应该以IIS服务器形式)启动。

当指令服务启动后,可以看到控制台的消息

>>> Listening on the MessageBus...
>>> Calling GRPC Server on the path: http://localhost:5000
>>>Seeding new platforms...
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\03Codes\HaitaoCodes\self-practise\dotnet-microservice-practice\Service\CommandService\CommandService

通过Postman调用获取指令服务的全部平台数据: Untitled 1.png 以上正是平台服务本地其中时,存放在内存中的平台数据。

重新部署到集群内

参考之前的内容

看到平台服务的容器内控制台消息:

>>> Using SQL Server
>>> Attempting to apply migration...
>>> Seed data exist...
>>> CommandService Endpoint http://commands-clusterip-svc:80
warn: Microsoft.AspNetCore.Server.Kestrel[0]
      Overriding address(es) 'http://+:80'. Binding to endpoints defined in UseKestrel() instead.
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:2333
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.
>>> Connect to MessageBus

看到指令服务的容器内控制台消息:

>>> Listening on the MessageBus...
>>> Calling GRPC Server on the path: http://platforms-clusterip-svc:2333
>>>Seeding new platforms...
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.

通过Postman调用获取指令服务的全部平台数据: Untitled 2.png