1. 程式人生 > >基於 Consul 實現 MagicOnion(GRpc) 服務註冊與發現

基於 Consul 實現 MagicOnion(GRpc) 服務註冊與發現

override 打包 git second oms 鏡像 包括 red iap

0.簡介

0.1 什麽是 Consul

Consul是HashiCorp公司推出的開源工具,用於實現分布式系統的服務發現與配置。

這裏所謂的服務,不僅僅包括常用的 Api 這些服務,也包括軟件開發過程當中所需要的諸如 Rpc、Redis、Mysql 等需要調用的資源。

簡而言之 Consul 就是根據 Key/Value 存儲了一套所有服務的 IP/Port 集合,當你 Grpc 客戶端需要請求某種服務的時候,具體的 IP 與端口不需要你自己來進行指定,而是通過與 Consul Agent 通信獲得某個服務下面可用的 IP/Port 集合。

而 Consul 還提供了健康檢查等附加的功能,你可以通過對可用服務節點的遍歷來自己進行負載均衡或者服務選擇。

0.2 為什麽要用 Consul

沒用 Consul 之前的情況是,我 new 一個 Channel 的話,需要指定 Grpc Server 的地址與端口,一單服務掛掉或者 Grpc Server 的 IP 地址或者端口有變更,那麽我還得重新更改 setting 才能夠使用我的服務。

使用了 Consul 之後我只需要去 Consul Agent 裏面查詢我指定的服務有哪些節點可用,返回給我對應的 IP 地址和端口,我就可以進行連接了。

1.準備工作

1.1 Consul 集群安裝與配置

Consul 我是直接使用 Docker 的官方 Consul 鏡像來進行安裝的,直接執行以下命令 pull 到最新的鏡像:

docker pull consul

拿到之後我們先運行一個服務:

docker run -d --name=dev-consul-server1 -e CONSUL_BIND_INTERFACE=eth0 consul agent -server -bootstrap

之後我們再運行兩個 Consul Server:

docker run -d --name=dev-consul-server2 -e CONSUL_BIND_INTERFACE=eth0 consul agent -server -retry-join 172.17.0.20

這裏 172.17.0.20 是之前 dev-consul-server1 的 IP 地址。

docker run -d --name=dev-consul-server3 -e CONSUL_BIND_INTERFACE=eth0 consul agent -server -retry-join 172.17.0.20

我們可以運行 consul members 命令來查看 Consul 集群信息:

docker exec -t dev-consul-server1 consul members 
Node          Address          Status  Type    Build  Protocol  DC   Segment
5019b941791a  172.17.0.20:8301  alive   server  1.1.0  2         dc1  <all>
ac53858f8c34  172.17.0.21:8301  alive   server  1.1.0  2         dc1  <all>
fc3aba2ddc25  172.17.0.22:8301  alive   server  1.1.0  2         dc1  <all>

可以看到已經有 3 個 Consul Server 啟動了。

下面我們再來運行一個 Consul Client 作為服務註冊與發現的端口:

docker run -d -p 8500:8500 --name=dev-consul-client -e CONSUL_BIND_INTERFACE=eth0 -e CONSUL_UI_BETA=true consul agent -retry-join 172.17.0.20 -bind 0.0.0.0 -ui -client 0.0.0.0

這裏註意 -bind-client 命令是你綁定的 IP 地址,這裏我直接將其與 0.0.0.0 綁定,而 -e CONSUL_UI_BETA=true 則是用於啟動新版本的 WebUI 界面,-ui 是啟用 WebUI 界面。

啟動完成之後我們可以訪問已經啟動的 Client Agent 了:

技術分享圖片

2.客戶端與服務端編寫

在這裏我以 Abp 框架作為演示,如何編寫一個支持 Consul 的 Grpc 服務端與 Grpc 客戶端,在演示當中所使用到的 Abp.Grpc.Server 包與 Abp.Grpc.Client 包可以從 NuGet 站點當中搜索安裝,其源代碼我托管到 GitHub 上面的,地址為:https://github.com/GameBelial/Abp.Grpc,歡迎 Star。

2.1 Grpc 服務端編寫

2.1.1 Abp 集成

首先建立一個標準的 ASP.NET Core Web Application 程序,引入 AbpAbp.AspNetCoreAbp.Grpc.Server 包,項目取名為 Abp.Grpc.Server.Demo,類型選擇空項目,在我們的 Startup 類當中編寫如下代碼:

using Abp.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace Abp.Grpc.Server.Demo
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            // 添加 MVC
            services.AddMvc();
            // 添加 ABP 框架,註意更改 ConfigureServices 返回值為 IServiceProvider
            return services.AddAbp<AbpGrpcServerDemoModule>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            // 啟用 ABP 框架中間件
            app.UseAbp();
            // 啟用 MVC 中間件
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "defaultWithArea",
                    template: "{area}/{controller=Home}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

2.1.2 建立項目啟動模塊

新建一個 AbpGrpcServerDemoModule 類,並編寫以下代碼:

using Abp.AspNetCore;
using Abp.Grpc.Server.Extensions;
using Abp.Modules;

namespace Abp.Grpc.Server.Demo
{
    // 此處依賴 ABP 的 AspNetCore 模塊與我們的 GRPC 服務模塊
    [DependsOn(typeof(AbpAspNetCoreModule),
        typeof(AbpGrpcServerModule))]
    public class AbpGrpcServerDemoModule : AbpModule
    {
        public override void PreInitialize()
        {
            Configuration.Modules.UseGrpcService(option =>
            {
                // GRPC 服務綁定的 IP 地址
                option.GrpcBindAddress = "0.0.0.0";
                // GRPC 服務綁定的 端口號
                option.GrpcBindPort = 5001;
                // 啟用 Consul 服務註冊
                option.UseConsul(consulOption =>
                {
                    // Consul 服務註冊地址
                    consulOption.ConsulAddress = "10.0.75.1";
                    // Consul 服務註冊端口號
                    consulOption.ConsulPort = 8500;
                    // 註冊到 Consul 的服務名稱
                    consulOption.RegistrationServiceName = "TestGrpcService";
                    // 健康檢查接口的端口號
                    consulOption.ConsulHealthCheckPort = 5000;
                });
            })
            .AddRpcServiceAssembly(typeof(AbpGrpcServerDemoModule).Assembly); // 掃描當前程序集的所有 GRPC 服務
        }

        public override void Initialize()
        {
            IocManager.RegisterAssemblyByConvention(typeof(AbpGrpcServerDemoModule).Assembly);
        }
    }
}

2.1.3 編寫健康檢查控制器

技術分享圖片

新建一個文件夾叫做 Controllers ,並且新建一個 HealthController 類,其內容如下:

using Abp.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc;

namespace Abp.Grpc.Server.Demo.Controllers
{
    public class HealthController : AbpController
    {
        /// <summary>
        /// 健康檢查接口
        /// </summary>
        public IActionResult Check()
        {
            return Ok("OJBK");
        }
    }
}

註意:此處應該繼承自 AbpController 基類

2.1.4 編寫 RPC 服務

技術分享圖片

新建一個 RpcServices 文件夾,並且新建一個 TestGrpcService 文件,其內容如下:

using MagicOnion;
using MagicOnion.Server;

namespace Abp.Grpc.Server.Demo.RpcServices
{
    public interface ITestGrpcService : IService<ITestGrpcService>
    {
        UnaryResult<int> Sum(int x, int y);
    }

    public class TestGrpcService : ServiceBase<ITestGrpcService>, ITestGrpcService
    {
        public UnaryResult<int> Sum(int x, int y)
        {
            return UnaryResult(x + y);
        }
    }
}

可以看到我們編寫了一個簡單的 Sum 方法,該方法接收兩個 int 類型的參數,計算其和並返回。

2.1.5 編寫 Dockerfile 文件

因為我們的 Consul 是放在 Docker 容器當中的,所以我們將我們的站點發布出去,並且編寫一個 Dockerfile 文件,內容如下:

FROM microsoft/dotnet
ENV ASPNETCORE_URLS http://+:5000
## 開放 5000 網站端口
EXPOSE 5000
## 開放 5001 RPC 端口
EXPOSE 5001

WORKDIR /app
COPY ./ .

ENTRYPOINT [ "dotnet","Abp.Grpc.Server.Demo.dll" ]

將其拷貝到發布好的站點,並且執行 docker build 命令:

PS D:\Project\DEMO\Abp.Grpc.Server.Demo\Abp.Grpc.Server.Demo\bin\Release\netcoreapp2.1\publish> docker build -t grpc-server-demo .
Sending build context to Docker daemon   29.9MB
Step 1/7 : FROM microsoft/dotnet
 ---> d8381e1175a1
Step 2/7 : ENV ASPNETCORE_URLS http://+:5000
 ---> Using cache
 ---> da7659cff6d2
Step 3/7 : EXPOSE 5000
 ---> Using cache
 ---> 7ecfc480ad43
Step 4/7 : EXPOSE 5001
 ---> Using cache
 ---> 75f10934ad1e
Step 5/7 : WORKDIR /app
 ---> Using cache
 ---> dee9739da4cd
Step 6/7 : COPY ./ .
 ---> 1a5acc1f0298
Step 7/7 : ENTRYPOINT [ "dotnet","Abp.Grpc.Server.Demo.dll" ]
 ---> Running in a46efbabc7fc
Removing intermediate container a46efbabc7fc
 ---> 321201373ecf
Successfully built 321201373ecf
Successfully tagged grpc-server-demo:latest
SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories

構建完鏡像之後,我們運行該鏡像:

docker run -d -p 5000:5000 -p 5001:5001 --name=grpc-server-demo grpc-server-demo

2.1.6 查看 Consul

來到 Consul 的 UI 界面查看效果:

技術分享圖片

可以看到已經成功註冊,說明已經成功了。

2.2 Grpc 客戶端編寫

2.2.1 Abp 集成

首先建立一個標準的 .Net Console 程序,引入 Abp.Grpc.Client 包,在我們的 Program 類當中編寫如下代碼:

using Abp.Grpc.Client.Demo.RpcServices;
using Abp.Grpc.Client.Utility;
using System;

namespace Abp.Grpc.Client.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var bootstrapper = AbpBootstrapper.Create<AbpGrpcClientDemoModule>())
            {
                bootstrapper.Initialize();

                Console.WriteLine("Press enter to stop application...");
                Console.ReadLine();
            }

            Console.WriteLine("Hello World!");
        }
    }
}

2.2.2 建立項目啟動模塊

然後我們新建一個 AbpGrpcClientDemoModule 類,該類一樣是一個啟動模塊,用於配置連接信息:

using Abp.Grpc.Client.Configuration;
using Abp.Grpc.Client.Extensions;
using Abp.Modules;

namespace Abp.Grpc.Client.Demo
{
    [DependsOn(typeof(AbpGrpcClientModule))]
    public class AbpGrpcClientDemoModule : AbpModule
    {
        public override void PreInitialize()
        {
            Configuration.Modules.UseGrpcClient(new ConsulRegistryConfiguration("10.0.75.1", 8500, null));
        }
    }
}

很簡單,直接配置 Consul 註冊的 IP 與端口號即可。

2.2.3 建立 RPC 接口定義

要調用我們 Server 提供的 RPC 端口的話,得編寫一個接口定義,就是我們在 Server 項目裏面寫的那個,新建一個 ITestGrpcService 接口,內容如下:

using MagicOnion;

namespace Abp.Grpc.Client.Demo.RpcServices
{
    public interface ITestGrpcService : IService<ITestGrpcService>
    {
        UnaryResult<int> Sum(int x, int y);
    }
}

2.2.4 調用 RPC 接口

using Abp.Grpc.Client.Demo.RpcServices;
using Abp.Grpc.Client.Utility;
using System;

namespace Abp.Grpc.Client.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var bootstrapper = AbpBootstrapper.Create<AbpGrpcClientDemoModule>())
            {
                bootstrapper.Initialize();
                
                // 調用接口
                var connectionUtility = bootstrapper.IocManager.Resolve<IGRpcConnectionUtility>();
                var result = connectionUtility.GetRemoteService<ITestGrpcService>("TestGrpcService").Sum(10, 5).ResponseAsync.Result;
                // 展示結果
                Console.WriteLine("Result:" + result);

                Console.WriteLine("Press enter to stop application...");
                Console.ReadLine();
            }

            Console.WriteLine("Hello World!");
        }
    }
}

調用接口的話,需要註入 IGRpcConnectionUtility 工具類,使用其 GetRemoteService 方法就可以調用你的遠程方法,記住一定要傳入有效的服務名稱。

2.2.5 編寫 Dockerfile 文件

一樣的,我們新建一個 Dockerfile 文件,將我們的 client 也打包成鏡像:

FROM microsoft/dotnet

WORKDIR /app
COPY ./ .

ENTRYPOINT [ "dotnet","Abp.Grpc.Client.Demo.dll" ]

內容很簡單,一樣的復制到發布成功的文件夾,構建鏡像:

docker build -t grpc-client-demo .

構建之後運行:

docker run grpc-client-demo

不出意外的話會看到如下輸出:

PS D:\Project\DEMO\Abp.Grpc.Client.Demo\Abp.Grpc.Client.Demo\bin\Release\netcoreapp2.1\publish> docker run grpc-client-demo
Result:15
Press enter to stop application...
Hello World!

技術分享圖片

3.代碼分析

拋開 ABP 框架部分的代碼,其實要實現服務註冊很簡單,核心就是 ConsulClient 這個類,下面就來分析一下 Abp.Grpc 庫裏面的代碼。

3.1 註冊服務

註冊服務其核心就在於 ConsulClient.Agent.ServiceRegister() 方法,通過傳入一個構造好的 AgentServiceRegistration 對象就可以成功註冊一個服務到 Consul。

例如:

_agentServiceRegistration = new AgentServiceRegistration
{
    ID = Guid.NewGuid().ToString(),// 唯一ID
    Name = config.RegistrationServiceName,// 註冊的服務名稱
    Address = currentIpAddress, // 服務提供者地址
    Port = config.GrpcBindPort, // 服務提供者端口
    Tags = new[] { "Grpc", $"urlprefix-/{config.RegistrationServiceName}" }, // 註冊的服務標簽
    Check = new AgentServiceCheck // 健康檢查
    {
        DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5), // 取消註冊時間
        Interval = TimeSpan.FromSeconds(10), // 檢查間隔
        Status = HealthStatus.Passing, // 檢查通過的狀態
        Timeout = TimeSpan.FromSeconds(5), // 超時時間
        HTTP = $"http://{currentIpAddress}:{config.ConsulHealthCheckPort}/health/check" // 健康檢查接口地址
    }
};

構建成功後通過 ConsulClient.Agent.ServiceRegister() 方法即可註冊到 Consul。

取消註冊則是通過 ConsulClient.Agent.ServiceDeregister 方法。

3.2 發現服務

服務發現相較於服務註冊簡單得多,只需要通過 ConsulClient.Catalog.Services 遍歷其結果即可獲得所有節點,並且通過 LINQ 來篩選出指定 tag 的服務。

4.其他相關參考資料

田園裏的蟋蟀:Docker & Consul & Fabio & ASP.NET Core 2.0 微服務跨平臺實踐)

Edison Chou:.NET Core微服務之基於Consul實現服務治理

Cecilphillip:Using Consul for Service Discovery with ASP.NET Core

5.所使用到的代碼

Abp.Grpc 庫代碼:https://github.com/GameBelial/Abp.Grpc

DEMO 代碼:

https://github.com/GameBelial/Abp.Grpc.Server.Demo

https://github.com/GameBelial/Abp.Grpc.Client.Demo

基於 Consul 實現 MagicOnion(GRpc) 服務註冊與發現