Api網關Kong集成Consul做服務發現及在Asp.Net Core中的使用

1622219047536

寫在前面

  Api網關我們之前是用 .netcore寫的 Ocelot的,使用後並沒有完全達到我們的預期,花了些時間了解後覺得kong可能是個更合適的選擇。

簡單說下kong對比ocelot打動我的:

1、kong可以直接代替Nginx/OpenRestry做前端服務器。

2、kong的功能強大,性能不俗,生態不錯,操作面板,插件豐富,社區活躍;

本文目的

1、對kong和consul做個基本介紹;

2、kong集成consul 做服務發現;

3、Asp.net core WebApi 服務自動註冊到Consul;

4、Asp.net core WebApi 自動註冊路由規則到kong,實現程序啟動即部署;

運行環境

172.16.1.30 CentOS Linux release 7.6.1810 (Core) (虛擬機單核2g)

Docker version 18.09.3, build 774a1f4

kong apigateway(enterprise) 2.3.x (docker安裝)

kong

kong的簡介

KONG — The Microservice API Gateway | by faren | Medium

我們熟悉Nginx;

有個一個加強版的Nginx叫做OpenRestry,OpenRestry ≈ lua腳本+Nginx;

那麼Kong 網關就是滿血版的 OpenRestry,它有許許多多的的插件和各種豐富的功能,且提供對應的Rest Api,讓你輕鬆打造你所能想像到的 網關+ web前端服務器的功能;

特點(翻譯)

  • 雲原生:平台無關,kong支持任意平台,裸機容器或雲平台;

  • k8s原生:原生支持k8s,有kong-ingress,支持l4+l7協議;

  • 動態負載均衡:負載均衡到多個upstream;

  • Hash-based的負載均衡:根據cookie、session,ip等hash負載均衡;

  • 斷路器:自動剔除不健康的服務;

  • 心跳檢測:主動和被動心跳檢測;

  • 服務發現:通過第三方dns解析做服務發現,如consul;

  • Serverless:調用和保護 AWS Lambda or OpenWhisk functions directly ;

  • WebSockets:支持ws、wss協議;

  • gRPC:支持gRPC協議,並通過日誌和插件監控流量;

  • OAuth2.0:輕鬆添加OAuth2.0支持;

  • 日誌:輕鬆記錄請求和響應,通過HTTP, TCP, UDP, 或 直接到硬盤;

  • 安全性:訪問控制,爬蟲檢測、ip黑白名單等等;

  • Syslog:記錄到系統日誌;

  • SSL: 安裝不同的SSL證書到服務;

  • 監控:實時監控,提供關機負責負載均衡和性能指標;

  • 整箱代理:kong可以作為正向代理服務器;

  • 身份認證:HMAC, JWT, Basic, 各種奇奇怪怪的規則都支持.

  • 限制器:流量限制功能;

  • 傳輸轉換:新增、刪掉、或者修改你的請求或者響應;

  • 緩存:請求緩存;

  • CLI:命令行控制支持;

  • Rest Api:Rest Api控制支持;

  • Geo-Replicated:誇時區請求支持;

  • 故障檢測與恢復:數據庫(Cassandra /postgres)節點掛掉不影響kong的服務;

  • 集群:所有kong節點都自動加入集群保持配置同步;

  • 拓展性:分佈式拓展原生支持,水平伸縮加減節點就行;

  • 高性能:使用Nginx作為核心負載均衡組件,高性能可伸縮;

  • 插件:高拓展性,插件式添加功能;

詳細請看

github: //github.com/Kong/kong

官方文檔: //docs.konghq.com

kong的安裝

拉取鏡像

docker pull kong/kong-gateway:2.3.3.2-alpine

給鏡像改個名

docker tag <IMAGE_ID> kong-ee

創建一個網絡

docker network create kong-ee-net

運行一個postgresSql 9.6,用來存取kong的配置

docker run -d --name kong-ee-database \
  --network=kong-ee-net \
  -p 5432:5432 \
  -e "POSTGRES_USER=kong" \
  -e "POSTGRES_DB=kong" \
  -e "POSTGRES_PASSWORD=kong" \
  postgres:9.6

啟動kong

  docker run -d --name kong-ee2
  --network=kong-ee-net \
  -e "KONG_DATABASE=postgres" \
  -e "KONG_PG_HOST=172.16.1.30" \
  -e "KONG_PG_PASSWORD=kong" \
  -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
  -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
  -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
  -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
  -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
  -e "KONG_ADMIN_GUI_URL=//172.16.1.30:8002" \ 
  -e "KONG_DNS_RESOLVER=172.16.1.30:8600" \  #注意按需使用,consul的才配
  -p 8000:8000 \
  -p 8443:8443 \
  -p 8001:8001 \
  -p 8444:8444 \
  -p 8002:8002 \
  -p 8445:8445 \
  -p 8003:8003 \
  -p 8004:8004 \
  kong-ee
  
  
  //-e "KONG_DNS_RESOLVER=172.16.1.30:8600" 注意這個配置,這是我需要用的consul的dns配置,如果不想用consul做服務發現,刪掉這行

這裡說明一下,kong的配置是用postgres(或者Cassandra )來存配置,但每一次請求都不需要去讀取數據庫的。修改的配置會直接 reload 到內存中,不影響性能;

另外說說kong的集群;

因為kong 網關其實最終 表現為一個超級前端服務器+網關,所以每個連接到同個數據庫的kong實例配置一樣,連接同個數據庫的kong作為一個集群;

一般在kong的前面是直接做dns解析就行,如果dns不支持多ip的話做keepalive + vip就行;

驗證

#admin api 獲取所有服務
curl -i -X GET --url //127.0.0.1:8001/services

#admin 管理後台 
curl -i -X GET --url //127.0.0.1:8002

1622292530454

1622292862508

管理後台

1622297057459

consul

consul簡介

Consul Service Mesh with Paul Banks - Software Engineering Daily

  Consul是HashiCorp公司推出的開源工具,用於實現分佈式系統的服務發現與配置。與其他分佈式服務註冊與發現的方案,比如 Airbnb的SmartStack等相比,Consul的方案更「一站式」,內置了服務註冊與發現框 架、分佈一致性協議實現、健康檢查、Key/Value存儲、多數據中心方案,不再需要依賴其他工具(比如ZooKeeper等),使用起來也較 為簡單。

其實就是做服務治理的。

github: //github.com/hashicorp/consul

官方文檔: //www.consul.io/

consul的安裝

直接docker安裝

*這是作為開發節點安裝

docker run  -d --name=dev-consul1 --network=host -e CONSUL_BIND_INTERFACE=eth0 consul:1.8

安裝成功

1622295177798

運行一個WebApi服務

先在服務運行一個Asp.net Core WebApi (就是是新建的一個包含),我的版本是3.1的,我給服務命名:DemoApi31,監聽端口5002

1622294659671

將服務註冊到Consul

curl --location --request PUT '//172.16.1.30:8500/v1/agent/service/register' \
--header 'Content-Type: application/json' \
--data-raw '{
  "ID": "DemoApi31_172.16.1.30:5002",
  "Name": "DemoApi31",
  "Address": "172.18.1.30",
  "Port": 5002,
  "EnableTagOverride": false,
  "Weights": {
    "Passing": 10,
    "Warning": 1
  }
}'

註冊成功:

1622295901281

Dns解析驗證

# 如果沒安裝dig 安裝:yum install bind-utils
dig @172.16.1.30 -p 8600 Demoapi31.service.consul SRV

1622294451840

ok,我們這裡已經把服務註冊到consul,且能通過dns常解析到了,我們做跟kong的集成吧。

consul提供內置Dns解析和Rest Api 兩種方式集成做服務發現,我們這裡跟kong的集成選用的Dns方式。

kong集成consul做服務發現

因為consul的角色是dns服務器,所以非常簡單,我們已註冊好的 DemoApi31為例:

1、創建一個名為consul的服務

DemoApi31.service.consul 是consu要求的格式

1622297254563

2、創建一個名為consul的路由

1622297366317

驗證

訪問我們配置的kong路由://172.16.1.30:8000/consul/api/values

1622297455784

ok

到目前為止我們只完成了本文目的1、2

3,和4三請往下看;

在Asp.net Core中的使用

  以之前的DemoApi31為例,換成5003端口,我需要達到的效果是,程序啟動的時候就把服務註冊到Consul 做好心跳檢測,並同時部署到網關Kong,直接對外服務。

Asp.net Core 服務自動註冊到Consul

安裝nuget包

Install-Package Passport.Infrastructure -Version 0.1.4.7-preview-1

**加入配置appsettings.json**

大家主要各服務器要改成自己的

  "ServiceDiscovery": {
    "ServiceName": "DemoApi31",
    "Consul": {
      "HttpEndpoint": "//172.16.1.30:8500", 
      "HttpHeathCheck": {
        "Path": "/healthcheck",
        "TimeOunt": 10,
        "Interval": 10
      },
      "Tags": [
        "NetCore",
        "DemoApi",
        "v1.0"
      ]
    }
  }

StartUp.cs ConfigureServices方法

public void ConfigureServices(IServiceCollection services)
{
    //第一行
    PassportConfig.InitPassportConfig(Configuration, Environment);
        
    ......
       
    services.AddHealthChecks();
    services.AddConsul();
}

StartUp.cs Configure方法

app.UseHealthChecks("/healthcheck");

啟動程序

dotnet DemoApi.Core3.1.dll --healthhost 172.16.1.30 --urls //*:5003 

1622299506567

1622299584100

源碼解析

/// <summary>
/// 加入consul做服務管理
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddConsul(this IServiceCollection services)
{
    var options = PassportConfig.GetSection("ServiceDiscovery").Get<ServiceDiscoveryOptions>();

    if (options?.Disable != true)
    {
	var healthHost = PassportConfig.GetHealthHost();

	if (string.IsNullOrWhiteSpace(options?.ServiceName) || string.IsNullOrWhiteSpace(options?.Consul?.HttpEndPoint))
	{
	    throw new ArgumentNullException("ServiceDiscovery.ServiceName/Consul.HttpEndpoint cannot be null or empty!");
	}

	//實例化kongclient
	var consulClient = new ConsulClient(x => x.Address = new Uri(options.Consul.HttpEndPoint));
	services.AddSingleton(consulClient);

	services.Configure(new Action<ConsulOptions>(op =>
	{
	    op.HttpEndPoint = options.Consul.HttpEndPoint;
	    op.Token = options.Consul.Token;
	    op.TcpEndPoint = options.Consul.TcpEndPoint;
	}));

	var checkOptions = options.Consul.HttpHeathCheck;
	var checkUrl = $"//{healthHost}:{PassportConfig.GetCurrentPort()}{checkOptions.Path}";

	new ConsulBuilder(consulClient)
	    .AddHttpHealthCheck(checkUrl, checkOptions.TimeOunt, checkOptions.Interval)
	    .RegisterService(options.ServiceName, healthHost, PassportConfig.GetCurrentPort(), options.Consul.Tags)
	    .Wait();
    }

    return services;
}

ConsulBuilder.cs 參考曉晨大佬

 public class ConsulBuilder
    {
        private readonly ConsulClient _client;
        private readonly List<AgentServiceCheck> _checks = new List<AgentServiceCheck>();

        public ConsulBuilder(ConsulClient client)
        {
            _client = client;
        }

        public ConsulBuilder AddHealthCheck(AgentServiceCheck check)
        {
            _checks.Add(check);
            return this;
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="url"></param>
        /// <param name="timeout">unit: second</param>
        /// <param name="interval">check interval. unit: second</param>
        /// <returns></returns>
        public ConsulBuilder AddHttpHealthCheck(string url, int timeout = 10, int interval = 10)
        {
            _checks.Add(new AgentServiceCheck()
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(timeout * 3),
                Interval = TimeSpan.FromSeconds(interval),
                HTTP = url,
                Timeout = TimeSpan.FromSeconds(timeout)
            });

            PassportConsole.Success($"[Consul]Add Http Healthcheck Success! CheckUrl:{url}");

            return this;
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="endpoint">GPRC service address.</param>
        /// <param name="grpcUseTls"></param>
        /// <param name="timeout">unit: second</param>
        /// <param name="interval">check interval. unit: second</param>
        /// <returns></returns>
        public ConsulBuilder AddGRPCHealthCheck(string endpoint, bool grpcUseTls = false, int timeout = 10, int interval = 10)
        {
            _checks.Add(new AgentServiceCheck()
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(20),
                Interval = TimeSpan.FromSeconds(interval),
                GRPC = endpoint,
                GRPCUseTLS = grpcUseTls,
                Timeout = TimeSpan.FromSeconds(timeout)
            });

            PassportConsole.Success($"[Consul]Add GRPC HealthCheck Success! Endpoint:{endpoint}");

            return this;
        }

        public async Task RegisterService(string name, string host, int port, string[] tags)
        {
            var registration = new AgentServiceRegistration()
            {
                Checks = _checks.ToArray(),
                ID = $"{name}_{host}:{port}",
                Name = name,
                Address = host,
                Port = port,
                Tags = tags
            };

            await _client.Agent.ServiceRegister(registration);
            PassportConsole.Success($"[Consul]Register Service Success! Name:{name} ID:{registration.ID}");

            AppDomain.CurrentDomain.ProcessExit += async (sender, e) =>
            {
                PassportConsole.Information($"[Consul] Service Deregisting ....  ID:{registration.ID}");

                await _client.Agent.ServiceDeregister(registration.ID);
            };
        }

        /// <summary>
        /// 移除服務
        /// </summary>
        /// <param name="serviceId"></param>
        public async Task Deregister(string serviceId)
        {
            await _client?.Agent?.ServiceDeregister(serviceId);
        }
    }

邏輯簡單,確定自己需要用的是註冊服務功能,調Consul Api 註冊,然後程序退出的時候註銷consul的服務就行;

Asp.net core WebApi 自動註冊路由規則到kong

通過Consul

安裝nuget包

#已安裝跳過
Install-Package Passport.Infrastructure -Version 0.1.4.7-preview-1

**加入配置appsettings.json**

guid順便去//www.guidgen.com/ 生成一個

"Kong": {
    //"Disable": false, //true=禁用
    "Host": "//172.16.1.30:8001",
    "Services": [
      {
        "Id": "72e21af8-283f-44c4-a766-53de8bb35c21", //guid
        "Name": "service-autoapi",
        "Retries": 5,
        "Protocol": "http",
        "Host": "DemoApi31.service.consul", 
        "Port": 0,
        "Path": null,
        "Connect_timeout": 60000, //毫秒
        "Write_timeout": 60000,
        "Read_timeout": 60000,
        "Tags": null
      }
    ],
    "Routes": [
      {
        "Id": "5370e1b7-6c43-442d-9a44-23c249f958f7",
        "Name": "route-autoapi",
        "Protocols": [ "http" ],
        "Methods": null,
        "Hosts": null,
        "Paths": [ "/autoapi" ],
        "Https_redirect_status_code": 307,
        "Regex_priority": 0,
        "Strip_path": true,
        "Preserve_host": false,
        "Tags": null,
        "Service": {
          "Id": "72e21af8-283f-44c4-a766-53de8bb35c21" //這個id跟關聯的Services的id一致
        }
      }
    ]
 }

StartUp.cs ConfigureServices方法

public void ConfigureServices(IServiceCollection services)
{
   ......
   
   
    services.AddConsul();
    services.RouteRegistToKong();
}

啟動程序

dotnet DemoApi.Core3.1.dll --healthhost 172.16.1.30 --urls //*:5003 

驗證

查看kong管理後台:

1622301181490

訪問 //172.16.1.30:8000/auto/api/values

1622301195836

大功告成。

不通過Consul,直接配置路由到kong

StartUp.cs ConfigureServices方法

public void ConfigureServices(IServiceCollection services)
{
   ......
   
   
    //刪掉這行services.AddConsul();
    services.RouteRegistToKong();
}

配置變為

"Kong": {
    //"Disable": false, //true=禁用
    "Host": "//172.16.1.30:8001",
    "Services": [
      {
        "Id": "0f86015b-b170-4ada-b045-740ae7d77ed6", //guid
        "Name": "configupapi",
        "Retries": 5,
        "Protocol": "http",
        "Host": "configupapi",
        "Port": 0,
        "Path": null,
        "Connect_timeout": 60000, //毫秒
        "Write_timeout": 60000,
        "Read_timeout": 60000,
        "Tags": null
      }
    ],
    "Routes": [
      {
        "Id": "1be79a57-af87-43b0-a0a0-b7a6cc0c5ade",
        "Name": "configupapi",
        "Protocols": [ "http" ],
        "Methods": null,
        "Hosts": null,
        "Paths": [ "/configupapi" ],
        "Https_redirect_status_code": 307,
        "Regex_priority": 0,
        "Strip_path": true,
        "Preserve_host": false,
        "Tags": null,
        "Service": {
          "Id": "0f86015b-b170-4ada-b045-740ae7d77ed6" //這個id跟Services的id一致
        }
      }
    ],

    "Upstream": {
      "Id": "8efd15af-df78-422f-97a0-9072fa7e7431",
      "Tags": [ "exampleapi", "v1.0" ],
      "Name": "configupapi",
      "Hash_on": "none",
      "Healthchecks": {
        "Active": {
          "Unhealthy": {
            "Http_statuses": [ 429, 500, 501, 502, 503, 504, 505 ],
            "Tcp_failures": 1,
            "Timeouts": 2,
            "Http_failures": 1,
            "Interval": 5
          },
          "Type": "http",
          "Http_path": "/healthcheck",
          "Timeout": 1,
          "Healthy": {
            "Successes": 1,
            "Interval": 20,
            "Http_statuses": [ 200, 302 ]
          },
          "Https_verify_certificate": true,
          "Concurrency": 1
        },
        "Passive": {
          "Unhealthy": {
            "Http_statuses": [ 429, 500, 501, 502, 503, 504, 505 ]
          },
          "Healthy": {
            "Http_statuses": [ 200, 201, 302 ]
          },
          "Type": "http"
        }
      },
      "Hash_on_cookie_path": "/",
      "Hash_fallback": "none",
      "Slots": 10000
    },
    "Target": {
      "Tags": [ "exampleapi", "v1.0" ],
      "Weight": 100
    }
  }

源碼解析

/// <summary>
/// 路由註冊到kong;
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection RouteRegistToKong(this IServiceCollection services)
{
    if (!PassportConfig.GetBool("Kong:Disable"))
    {
	var konghost = PassportConfig.Get("Kong:Host") ?? throw new ArgumentNullException("Kong:Host cannot be null or empty!");

	var options = new KongClientOptions(HttpClientFactory.Create(), konghost);
	var client = new KongClient(options);
	services.AddSingleton<KongClient>(client);

	var upStream = PassportConfig.GetSection("Kong:Upstream").Get<UpStream>();
	var target = PassportConfig.GetSection("Kong:Target").Get<TargetInfo>();

	if (upStream != null && target != null)
	{
	    upStream.Created_at = DateTime.Now;
	    upStream = client.UpStream.UpdateOrCreate(upStream).Result;

	    target.Target = $"{PassportConfig.GetHealthHost()}:{PassportConfig.GetCurrentPort()}";
	    target.Id = PassportTools.GuidFromString($"{Dns.GetHostName()}{target.Target}");
	    target.Created_at = DateTime.Now;
	    target.UpStream = new TargetInfo.UpStreamId { Id = upStream.Id.Value };
	    client.Target.Add(target).Wait();

	    PassportConsole.Success($"[Kong]UpStream registered:{upStream.Name} Target:{target.Target}");

	    // app.UseKongHealthChecks(upStream, onExecuter);
	}

	var kongServices = PassportConfig.GetSection("Kong:Services").Get<ServiceInfo[]>();
	var kongRoutes = PassportConfig.GetSection("Kong:Routes").Get<RouteInfo[]>();

	if (kongServices?.Length > 0 == true)
	{
	    foreach (var item in kongServices)
	    {
		item.Updated_at = DateTime.Now;
		item.Path = string.IsNullOrWhiteSpace(item.Path) ? null : item.Path;
		client.Service.UpdateOrCreate(item).Wait();
		PassportConsole.Success($"[Kong]Service registered:{item.Name}");
	    }
	}

	if (kongRoutes?.Length > 0 == true)
	{
	    foreach (var item in kongRoutes)
	    {
		item.Updated_at = DateTime.Now;
		client.Route.UpdateOrCreate(item).Wait();
		PassportConsole.Success($"[Kong]Route registered:{item.Name}");
	    }
	}
    }

    return services;
}

  邏輯也簡單,也是調用kong配置把本該手工配置的路由,分別調用upstream、service、route Api修改配置。有區別的是程序退出時不會去刪對應的路由;

總結

  我在各技術博客都沒有看到總結的比較好的kong+consul+asp.net core的集成文章,特此總結。期待您的點贊留意;

[參考]

//docs.konghq.com/

//www.cnblogs.com/stulzq/p/11942691.html

//github.com/lianggx/Kong.Net

//www.consul.io/docs