Ocelot+Consul實現微服務架構

Crazier發表於2020-09-08

API閘道器

  API 閘道器一般放到微服務的最前端,並且要讓API 閘道器變成由應用所發起的每個請求的入口。這樣就可以明顯的簡化客戶端實現和微服務應用程式之間的溝通方式。以前的話,客戶端不得不去請求微服務A,然後再到微服務B,然後是微服務C。客戶端需要去知道怎麼去一起來消費這三個不同的service。使用API閘道器,我們可以抽象所有這些複雜性,並建立客戶端們可以使用的優化後的端點,並向那些模組們發出請求。API閘道器的核心要點是:所有的客戶端和消費端都通過統一的閘道器接入微服務,在閘道器層處理所有的非業務功能(比如驗證、鑑權、監控、限流、請求合併...)

Ocelot

  Ocelot是一個使用.NET Core平臺上的一個API Gateway,這個專案的目標是在.NET上面執行微服務架構。它功能強大,包括了:路由、請求聚合、服務發現、認證、鑑權、限流熔斷、並內建了負載均衡器與Service Fabric、Butterfly Tracing整合,還引入了Polly來進行故障處理。

Polly

  Polly是一種.NET彈性和瞬態故障處理庫,允許我們以非常順暢和執行緒安全的方式來執諸如行重試,斷路,超時,故障恢復等策略。

  重試策略(Retry)
    重試策略針對的前置條件是短暫的故障延遲且在短暫的延遲之後能夠自我糾正。允許我們做的是能夠自動配置重試機制。

  斷路器(Circuit-breaker)
    斷路器策略針對的前置條件是當系統繁忙時,快速響應失敗總比讓使用者一直等待更好。保護系統故障免受過載,Polly可以幫其恢復。

  超時(Timeout)
    超時策略針對的前置條件是超過一定的等待時間,想要得到成功的結果是不可能的,保證呼叫者不必等待超時。

  隔板隔離(Bulkhead Isolation)

    隔板隔離針對的前置條件是當程式出現故障時,多個失敗一直在主機中對資源(例如執行緒/ CPU)一直佔用。下游系統故障也可能導致上游失敗。這兩個風險都將造成嚴重的後果。都說一粒老鼠子屎攪渾一鍋粥,而Polly則將受管制的操作限制在固定的資源池中,免其他資源受其影響。

  快取(Cache)
    快取策略針對的前置條件是資料不會很頻繁的進行更新,為了避免系統過載,首次載入資料時將響應資料進行快取,如果快取中存在則直接從快取中讀取。

  回退(Fallback)
    操作仍然會失敗,也就是說當發生這樣的事情時我們打算做什麼。也就是說定義失敗返回操作。

  策略包裝(PolicyWrap)

    策略包裝針對的前置條件是不同的故障需要不同的策略,也就意味著彈性靈活使用組合

Consul

  Consul是HashiCorp公司推出的開源工具,用於實現分散式系統的服務發現與配置,內建了服務註冊與發現框架、分佈一致性協議實現、健康檢查、Key/Value儲存、多資料中心方案、在Ocelot已經支援簡單的負載功能,也就是當下遊服務存在多個結點的時候,Ocelot能夠承擔起負載均衡的作用。但是它不提供健康檢查,服務的註冊也只能通過手動在配置檔案裡面新增完成。這不夠靈活並且在一定程度下會有風險。這個時候我們就可以用Consul來做服務發現並實現健康檢查。

微服務搭建

  一、設計思路

  二、安裝consul服務

    檔案結構資訊

      data

      conf

      consul.exe

    配置conf.json資訊 並執行 :consul agent -config-dir="E:/PersonCode/.Net Core/consul/conf/conf.json"

{
"datacenter": "wf",
"data_dir": "E:/PersonCode/.Net Core/consul/data",
"log_level": "INFO",
"server": true,
"ui": true,
"bind_addr": "192.168.14.8",
"client_addr": "127.0.0.1",
"advertise_addr": "192.168.14.8",
"bootstrap_expect": 1,
"ports":{
"http": 8500,
"dns": 8600,
"server": 8300,
"serf_lan": 8301,
"serf_wan": 8302
}
}

執行結果:

  三、建立多個API服務 並註冊consul

     1、建立 api專案 新增swagger Ui

     2、引用Consul-1.6.1.1版本

     3、新增Consul 服務配置

    "Consul": {
      "ServiceName": "Zfkr.WF.Core.API",
      "ServiceIP": "localhost",
      "ConsulClientUrl": "http://localhost:8500",
      "HealthCheckRelativeUrl": "/wf/Base/health",
      "HealthCheckIntervalInSecond": 5
    }

               4、新增Consul 服務註冊類(RegisterCansulExtension)

    5、新增服務註冊與中介軟體

 

   6、啟用api 多個服務 

      dotnet Zfkr.WF.Core.API.dll --urls=http://*:8001

      dotnet Zfkr.WF.Core.API.dll --urls=http://*:8002

      dotnet Zfkr.WF.Core.API.dll --urls=http://*:8003

public static class RegisterCansulExtension
{
public static void RegisterToConsul(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime)
{
lifetime.ApplicationStarted.Register(() =>
{
string serviceName = configuration.GetValue<string>("Consul:ServiceName");
string serviceIP = configuration.GetValue<string>("Consul:ServiceIP");
string consulClientUrl = configuration.GetValue<string>("Consul:ConsulClientUrl");
string healthCheckRelativeUrl = configuration.GetValue<string>("Consul:HealthCheckRelativeUrl");
int healthCheckIntervalInSecond = configuration.GetValue<int>("Consul:HealthCheckIntervalInSecond");

ICollection<string> listenUrls = app.ServerFeatures.Get<IServerAddressesFeature>().Addresses;

if (string.IsNullOrWhiteSpace(serviceName))
{
throw new Exception("Please use --serviceName=yourServiceName to set serviceName");
}
if (string.IsNullOrEmpty(consulClientUrl))
{
consulClientUrl = "http://127.0.0.1:8500";
}
if (string.IsNullOrWhiteSpace(healthCheckRelativeUrl))
{
healthCheckRelativeUrl = "health";
}
healthCheckRelativeUrl = healthCheckRelativeUrl.TrimStart('/');
if (healthCheckIntervalInSecond <= 0)
{
healthCheckIntervalInSecond = 1;
}


string protocol;
int servicePort = 0;
if (!TryGetServiceUrl(listenUrls, out protocol, ref serviceIP, out servicePort, out var errorMsg))
{
throw new Exception(errorMsg);
}

var consulClient = new ConsulClient(ConsulClientConfiguration => ConsulClientConfiguration.Address = new Uri(consulClientUrl));

var httpCheck = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(10),//服務啟動多久後註冊
Interval = TimeSpan.FromSeconds(healthCheckIntervalInSecond),
HTTP = $"{protocol}://{serviceIP}:{servicePort}/{healthCheckRelativeUrl}",
Timeout = TimeSpan.FromSeconds(2)
};

// 生成註冊請求
var registration = new AgentServiceRegistration()
{
Checks = new[] { httpCheck },
ID = Guid.NewGuid().ToString(),
Name = serviceName,
Address = serviceIP,
Port = servicePort,
Meta = new Dictionary<string, string>() { ["Protocol"] = protocol },
Tags = new[] { $"{protocol}" }
};
consulClient.Agent.ServiceRegister(registration).Wait();

//服務停止時, 主動發出登出
lifetime.ApplicationStopping.Register(() =>
{
try
{
consulClient.Agent.ServiceDeregister(registration.ID).Wait();
}
catch
{ }
});
});
}


private static bool TryGetServiceUrl(ICollection<string> listenUrls, out string protocol, ref string serviceIP, out int port, out string errorMsg)
{
protocol = null;
port = 0;
errorMsg = null;
if (!string.IsNullOrWhiteSpace(serviceIP)) // 如果提供了對外服務的IP, 只需要檢測是否在listenUrls裡面即可
{
foreach (var listenUrl in listenUrls)
{
Uri uri = new Uri(listenUrl);
protocol = uri.Scheme;
var ipAddress = uri.Host;
port = uri.Port;

if (ipAddress == serviceIP || ipAddress == "0.0.0.0" || ipAddress == "[::]")
{
return true;
}
}
errorMsg = $"The serviceIP that you provide is not in urls={string.Join(',', listenUrls)}";
return false;
}
else // 沒有提供對外服務的IP, 需要查詢本機所有的可用IP, 看看有沒有在 listenUrls 裡面的
{
var allIPAddressOfCurrentMachine = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()
.Select(p => p.GetIPProperties())
.SelectMany(p => p.UnicastAddresses)
// 這裡排除了 127.0.0.1 loopback 地址
.Where(p => p.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && !System.Net.IPAddress.IsLoopback(p.Address))
.Select(p => p.Address.ToString()).ToArray();
var uris = listenUrls.Select(listenUrl => new Uri(listenUrl)).ToArray();
// 本機所有可用IP與listenUrls進行匹配, 如果listenUrl是"0.0.0.0"或"[::]", 則任意IP都符合匹配
var matches = allIPAddressOfCurrentMachine.SelectMany(ip =>
uris.Where(uri => ip == uri.Host || uri.Host == "0.0.0.0" || uri.Host == "[::]")
.Select(uri => new { Protocol = uri.Scheme, ServiceIP = ip, Port = uri.Port })
).ToList();

if (matches.Count == 0)
{
errorMsg = $"This machine has IP address=[{string.Join(',', allIPAddressOfCurrentMachine)}], urls={string.Join(',', listenUrls)}, none match.";
return false;
}
else if (matches.Count == 1)
{
protocol = matches[0].Protocol;
serviceIP = matches[0].ServiceIP;
port = matches[0].Port;
return true;
}
else
{
errorMsg = $"Please use --serviceIP=yourChosenIP to specify one IP, which one provide service: {string.Join(",", matches)}.";
return false;
}
}
}

   四、建立API閘道器

    1、建立.core Api 專案 並引用相關包檔案

       Consul  1.6.11

       Ocelot   16.0.1

       Ocelot.Provider.Consul 16.0.1

    2、新增ocelot.json檔案 並配置

    注意:"ServiceName": "Zfkr.WF.Core.API", 服務名稱必須與第三步中的 註冊服務時的 服務名稱一致,若未註冊服務 可配置DownstreamHostAndPorts實現負載,之所以要使用Consul 是因為Ocelot自身 沒有無法 實現健康檢查  服務自動新增與移除

{

"Routes": [
/*Swagger 閘道器*/
{
"DownstreamPathTemplate": "/swagger/WorkFlow/swagger.json",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 8001
}
],
"LoadBalancer": "RoundRobin",
"UpstreamPathTemplate": "/WorkFlow/swagger/WorkFlow/swagger.json",
"UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ]
},
//{
// "UseServiceDiscovery": true, // 使用服務發現
// "DownstreamPathTemplate": "/swagger/TaskSchedul/swagger.json",
// "DownstreamScheme": "http",
// "DownstreamHostAndPorts": [
// {
// "Host": "localhost",
// "Port": 8002
// }
// ],
// "LoadBalancer": "RoundRobin",
// "UpstreamPathTemplate": "/TaskSchedul/swagger/TaskSchedul/swagger.json",
// "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ]
//},
/*Api閘道器*/
{
"UseServiceDiscovery": true, //啟用服務發現,若Ocelot集合Consul必須配置此項
"DownstreamPathTemplate": "/WF/{url}",
"DownstreamScheme": "http",
//"DownstreamHostAndPorts": [
// {
// "Host": "localhost",
// "Port": 8001
// },
// {
// "Host": "localhost",
// "Port": 8003
// }
//],
"UpstreamPathTemplate": "/WF/{url}",
"UpstreamHttpMethod": [ "Get", "Post" ],
"ServiceName": "Zfkr.WF.Core.API", //服務名稱
"LoadBalancerOptions": {
"Type": "RoundRobin"
}
}
//,
//{
// "DownstreamPathTemplate": "/TS/{url}",
// "DownstreamScheme": "http",
// "DownstreamHostAndPorts": [
// {
// "Host": "localhost",
// "Port": 8002
// }
// ],
// "ServiceName": "node-2", // 服務名稱
// "UseServiceDiscovery": true,
// "UpstreamPathTemplate": "/TS/{url}",
// "UpstreamHttpMethod": [ "Get", "Post" ],
// "LoadBalancerOptions": {
// "Type": "LeastConnection"
// }
//}
],

"GlobalConfiguration": {
"BaseUrl": "http://localhost:8899", //閘道器對外地址
"ReRouteIsCaseSensitive": false, //是否區分路由字母大小寫
"ServiceDiscoveryProvider": { //服務發現提供者,配置Consul地址
"Host": "localhost", //Consul主機名稱
"Port": 8500, //Consul埠號
"Type": "Consul" //必須指定Consul服務發現型別
}
//,
//"限流相關配置"
//"RateLimitOptions": {
// "ClientIdHeader": "ClientId",
// "QuotaExceededMessage": "RateLimit SCscHero", //限流響應提示
// "RateLimitCounterPrefix": "ocelot",
// "DisableRateLimitHeaders": false,
// "HttpStatusCode": 429
//}
}
}

    3、新增Consul註冊類 並註冊到中介軟體,新增健康檢查服務

    3.1、新增appsettings 配置資訊

    "Consul": {
      "ServiceName": "Gateway-9988-Service",
      "Datacenter": "wf",
      "ServiceIP": "localhost",
      "ConsulClientUrl": "http://localhost:8500",
      "HealthCheckRelativeUrl": "/wf/Base/health",
      "HealthCheckIntervalInSecond": 5
    }

    3.2、新增Consul註冊類

public static class RegisterCansulExtension
{
public static void RegisterToConsul(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime)
{
lifetime.ApplicationStarted.Register(() =>
{
string serviceName = configuration.GetValue<string>("Consul:ServiceName");
string serviceIP = configuration.GetValue<string>("Consul:ServiceIP");
string consulClientUrl = configuration.GetValue<string>("Consul:ConsulClientUrl");
string healthCheckRelativeUrl = configuration.GetValue<string>("Consul:HealthCheckRelativeUrl");
int healthCheckIntervalInSecond = configuration.GetValue<int>("Consul:HealthCheckIntervalInSecond");

ICollection<string> listenUrls = app.ServerFeatures.Get<IServerAddressesFeature>().Addresses;

if (string.IsNullOrWhiteSpace(serviceName))
{
throw new Exception("Please use --serviceName=yourServiceName to set serviceName");
}
if (string.IsNullOrEmpty(consulClientUrl))
{
consulClientUrl = "http://127.0.0.1:8500";
}
if (string.IsNullOrWhiteSpace(healthCheckRelativeUrl))
{
healthCheckRelativeUrl = "health";
}
healthCheckRelativeUrl = healthCheckRelativeUrl.TrimStart('/');
if (healthCheckIntervalInSecond <= 0)
{
healthCheckIntervalInSecond = 1;
}

string protocol;
int servicePort = 0;
if (!TryGetServiceUrl(listenUrls, out protocol, ref serviceIP, out servicePort, out var errorMsg))
{
throw new Exception(errorMsg);
}

var consulClient = new ConsulClient(ConsulClientConfiguration => ConsulClientConfiguration.Address = new Uri(consulClientUrl));

var httpCheck = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(10),//服務啟動多久後註冊
Interval = TimeSpan.FromSeconds(healthCheckIntervalInSecond),
HTTP = $"{protocol}://{serviceIP}:{servicePort}/{healthCheckRelativeUrl}",
Timeout = TimeSpan.FromSeconds(2)
};

// 生成註冊請求
var registration = new AgentServiceRegistration()
{
Checks = new[] { httpCheck },
ID = Guid.NewGuid().ToString(),
Name = serviceName,
Address = serviceIP,
Port = servicePort,
Meta = new Dictionary<string, string>() { ["Protocol"] = protocol },
Tags = new[] { $"{protocol}" }
};
consulClient.Agent.ServiceRegister(registration).Wait();

//服務停止時, 主動發出登出
lifetime.ApplicationStopping.Register(() =>
{
try
{
consulClient.Agent.ServiceDeregister(registration.ID).Wait();
}
catch
{ }
});
});
}


private static bool TryGetServiceUrl(ICollection<string> listenUrls, out string protocol, ref string serviceIP, out int port, out string errorMsg)
{
protocol = null;
port = 0;
errorMsg = null;
if (!string.IsNullOrWhiteSpace(serviceIP)) // 如果提供了對外服務的IP, 只需要檢測是否在listenUrls裡面即可
{
foreach (var listenUrl in listenUrls)
{
Uri uri = new Uri(listenUrl);
protocol = uri.Scheme;
var ipAddress = uri.Host;
port = uri.Port;

if (ipAddress == serviceIP || ipAddress == "0.0.0.0" || ipAddress == "[::]")
{
return true;
}
}
errorMsg = $"The serviceIP that you provide is not in urls={string.Join(',', listenUrls)}";
return false;
}
else // 沒有提供對外服務的IP, 需要查詢本機所有的可用IP, 看看有沒有在 listenUrls 裡面的
{
var allIPAddressOfCurrentMachine = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()
.Select(p => p.GetIPProperties())
.SelectMany(p => p.UnicastAddresses)
// 這裡排除了 127.0.0.1 loopback 地址
.Where(p => p.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && !System.Net.IPAddress.IsLoopback(p.Address))
.Select(p => p.Address.ToString()).ToArray();
var uris = listenUrls.Select(listenUrl => new Uri(listenUrl)).ToArray();
// 本機所有可用IP與listenUrls進行匹配, 如果listenUrl是"0.0.0.0"或"[::]", 則任意IP都符合匹配
var matches = allIPAddressOfCurrentMachine.SelectMany(ip =>
uris.Where(uri => ip == uri.Host || uri.Host == "0.0.0.0" || uri.Host == "[::]")
.Select(uri => new { Protocol = uri.Scheme, ServiceIP = ip, Port = uri.Port })
).ToList();

if (matches.Count == 0)
{
errorMsg = $"This machine has IP address=[{string.Join(',', allIPAddressOfCurrentMachine)}], urls={string.Join(',', listenUrls)}, none match.";
return false;
}
else if (matches.Count == 1)
{
protocol = matches[0].Protocol;
serviceIP = matches[0].ServiceIP;
port = matches[0].Port;
return true;
}
else
{
errorMsg = $"Please use --serviceIP=yourChosenIP to specify one IP, which one provide service: {string.Join(",", matches)}.";
return false;
}
}
}
}

    

      4、執行閘道器服務 dotnet Zfkr.WF.Core.Gateway.dll --urls=http://*:9988

 

相關文章