寫在前面
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的簡介
我們熟悉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: https://github.com/Kong/kong
官方文件: https://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=http://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 http://127.0.0.1:8001/services
#admin 管理後臺
curl -i -X GET --url http://127.0.0.1:8002
管理後臺
consul
consul簡介
Consul是HashiCorp公司推出的開源工具,用於實現分散式系統的服務發現與配置。與其他分散式服務註冊與發現的方案,比如 Airbnb的SmartStack等相比,Consul的方案更“一站式”,內建了服務註冊與發現框 架、分佈一致性協議實現、健康檢查、Key/Value儲存、多資料中心方案,不再需要依賴其他工具(比如ZooKeeper等),使用起來也較 為簡單。
其實就是做服務治理的。
github: https://github.com/hashicorp/consul
官方文件: https://www.consul.io/
consul的安裝
直接docker安裝
*這是作為開發節點安裝
docker run -d --name=dev-consul1 --network=host -e CONSUL_BIND_INTERFACE=eth0 consul:1.8
安裝成功
執行一個WebApi服務
先在服務執行一個Asp.net Core WebApi (就是是新建的一個包含),我的版本是3.1的,我給服務命名:DemoApi31,監聽埠5002
將服務註冊到Consul
curl --location --request PUT 'http://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
}
}'
註冊成功:
Dns解析驗證
# 如果沒安裝dig 安裝:yum install bind-utils
dig @172.16.1.30 -p 8600 Demoapi31.service.consul SRV
ok,我們這裡已經把服務註冊到consul,且能通過dns常解析到了,我們做跟kong的整合吧。
consul提供內建Dns解析和Rest Api 兩種方式整合做服務發現,我們這裡跟kong的整合選用的Dns方式。
kong整合consul做服務發現
因為consul的角色是dns伺服器,所以非常簡單,我們已註冊好的 DemoApi31
為例:
1、建立一個名為consul
的服務
DemoApi31.service.consul 是consu要求的格式
2、建立一個名為consul
的路由
驗證
訪問我們配置的kong路由:http://172.16.1.30:8000/consul/api/values
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": "http://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 http://*:5003
原始碼解析
/// <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 = $"http://{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順便去https://www.guidgen.com/ 生成一個
"Kong": {
//"Disable": false, //true=禁用
"Host": "http://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 http://*:5003
驗證
檢視kong管理後臺:
訪問 http://172.16.1.30:8000/auto/api/values
大功告成。
不通過Consul,直接配置路由到kong
StartUp.cs ConfigureServices方法
public void ConfigureServices(IServiceCollection services)
{
......
//刪掉這行services.AddConsul();
services.RouteRegistToKong();
}
配置變為
"Kong": {
//"Disable": false, //true=禁用
"Host": "http://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的整合文章,特此總結。期待您的點贊留意;
[參考]
https://www.cnblogs.com/stulzq/p/11942691.html