Service Discovery And Health Checks In ASP.NET Core With Consul

在這篇文章中,咱們將快速瞭解一下服務發現是什麼,使用Consul在ASP.NET Core MVC框架中,並結合DnsClient.NET實現基於Dns的客戶端服務發現html

這篇文章的全部源代碼均可以在GitHub上Demo項目得到.node

Service Discovery

在現代微服務架構中,服務能夠在容器中運行,而且能夠動態啓動,中止和擴展。 這致使了一個很是動態的託管環境,可能有數百個實際端點,沒法手動配置或找到正確的端點。git

話雖這麼說,我相信服務發現不只適用於生活在容器中的粒狀微服務。它能夠被任何須須訪問其餘資源的應用程序使用。資源能夠是數據庫,其餘Web服務,也能夠是託管在其餘地方的網站的一部分。服務發現有助於擺脫特定於環境的配置文件!github

服務發現可用於解決此問題,但一般,有許多不一樣的方法來實現它web

  • 客戶端服務發現
    一種解決方案是擁有一箇中央服務註冊表,其中全部服務實例都在這裏註冊。客戶端必須實現邏輯以查詢他們須要的服務,最終驗證端點是否仍然存活而且可能將請求分發到多個端點。
  • 服務器端/負載平衡
    全部流量都經過負載均衡器,負載均衡器知道全部實際的,動態變化的端點,並相應地重定向全部請求

Consul是一個服務註冊表,可用於實現客戶端服務發現。redis

除了使用這種方法的許多強大功能和優勢以外,它的缺點是每一個客戶端應用程序都須要實現一些邏輯來使用此中央註冊表。這個邏輯可能很是具體,由於Consul和任何其餘技術都有自定義API和邏輯工做方式。數據庫

負載平衡也可能沒法自動完成。客戶端能夠查詢服務的全部可用/已註冊端點,而後決定選擇哪一個端點。
好的是Consul不只帶有REST API來查詢服務註冊表,它還提供DNS端點,返回標準SRV和TXT記錄。json

DNS端點確實關心服務運行情況,由於它不會返回不健康的服務實例。它還經過以交替順序返回記錄來進行負載平衡! 此外,它可能使服務具備更高的優先級,更接近客戶端。api

如今,讓咱們開始......緩存

Consul 安裝

Consul是由HashiCorp開發的軟件,它不只提供服務發現(如上所述),還提供「健康檢查」,並提供分佈式「密鑰值存儲」。

Consul旨在一個集羣中運行,至少有三個實例處理集羣環境中每一個節點上的集羣和「代理」的協調。應用程序始終只與本地代理通訊,這使得通訊速度很是快,並將網絡延遲降至最低。

可是,對於本地開發,您能夠在--dev模式下運行Consul,而不是設置完整集羣。 可是請記住這一點,爲了生產使用,須要作一些工做才能正確設置Consul。

### 下載和運行Consul

官方文檔有不少例子,而且很好地解釋瞭如何設置Consul。我不會詳細介紹,咱們只是將它做爲本地開發代理運行。

要開始使用,請下載Consul

使用consul agent --dev命令和參數來運行啓動Consul,這將在本地服務模式下啓動Consul而無需配置文件,而且只能在localhost上訪問。
訪問http://localhost:8500 ,這應該能夠打開Consul UI

Consul UI

註冊第一個服務

Consul提供了添加或修改服務註冊表的不一樣方法。一種選擇是將JSON配置文件放入Consul的config目錄中。下面的例子將註冊一個Redis服務:

{ 
    "service":{
        "name": "redis",
        "tags":[],
        "port": 6379
    }
}

另外一個更有趣的選擇是經過REST API。幸運的是,已有許多語言的客戶端庫可用於此REST API,咱們將使用https://github.com/PlayFab/consuldotnet,.Net Core也可使用

要經過代碼註冊新服務,請建立一個新的ConsulClient實例並註冊新的服務註冊

var client = new ConsulClient(); // uses default host:port which is localhost:8500

var agentReg = new AgentServiceRegistration()
{
    Address = "127.0.0.1",
    ID = "uniqueid",
    Name = "serviceName",
    Port = 5200
};

await client.Agent.ServiceRegister(agentReg);

重要的是要注意,即便服務再也不運行,該註冊理論上也將永遠存在於Consul集羣中。

await client.Agent.ServiceDeregister("uniqueid");

若是服務崩潰,則可能沒法始終手動取消註冊服務。這就是Consul的另外一個特點:健康檢查。

健康檢查 Health Checks

Consul中的監控檢查可用於監視羣集中的全部服務的狀態,還能夠從Consul註冊表中自動刪除不健康的服務端點註冊。能夠將Consul配置爲根據須要按期爲每一個註冊服務運行儘量多的運行情況檢查。

最基本的健康檢查讓Consul嘗試經過TCP鏈接到服務:

var tcpCheck = new AgentServiceCheck()
{
    DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
    Interval = TimeSpan.FromSeconds(30),
    TCP = $"127.0.0.1:{port}"
};

Consul還能夠檢查HTTP端點。在這種狀況下,只要端點返回HTTP狀態代碼200,服務就是健康的。
一個很是簡單的健康檢查控制器能夠像這樣實現:

[Route("[Controller]")]
public class HealthCheckController : Controller
{
    [HttpGet("")]
    [HttpHead("")]
    public IActionResult Ping()
    {
        return Ok();
    }
}

在此次註冊中,咱們如今必須經過指定AgentServiceCheck的Http屬性而不是Tcp屬性來將Consul指向該節點:

var httpCheck = new AgentServiceCheck()
{
    DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
    Interval = TimeSpan.FromSeconds(30),
    HTTP = $"http://127.0.0.1:{port}/HealthCheck"
};

更新以前註冊代碼,添加讓Consul每30秒運行一次健康檢查的部分。請注意,我還將檢查配置爲自動取消註冊服務實例,以防它被標記爲運行情況超過一分鐘。

var registration = new AgentServiceRegistration()
{
    Checks = new[] { tcpCheck, httpCheck },
    Address = "127.0.0.1",
    ID = id,
    Name = name,
    Port = port
};

await client.Agent.ServiceRegister(registration);

這些基本示例應該足以開始。可是,運行健康檢查能夠執行更復雜的操做,Consul支持運行小腳原本驗證響應。

Endpoint Name, ID and Port

您可能已經注意到,要註冊服務,咱們必須知道服務運行的實際端點(Endpoint),咱們必須給它一個Name和一個ID

ID應該是足夠惟一的字符串來標識服務實例,而Name應該是同一服務的全部實例的通用名稱。

其餘客戶端將使用Name來查詢服務註冊表,該ID僅用於引用確切的實例,例如取消註冊服務實例時。
可是咱們如何定義名稱和端口以及IP地址?

若是咱們本身使用Kestrel託管ASP.NET Core應用程序很簡單,由於咱們還在哪一個端口和地址上配置Kestrel。當使用IIS(或任何其餘反向代理)託管服務時,這種方法會分崩離析,由於在反向代理模式下,Kestrel使用了動態配置,而且實際的託管信息沒法在應用程序代碼中使用。(譯者注:IIS對外的端口和內部Kestrel的端口並不一致)

要了解如何使用Kestrel託管它,讓咱們建立一個空的ASP.NET Core web api項目。

運行dotnet new webapi或在Visual Studio中使用WebAPI模板。

這將建立一個Program.cs和Startup.cs。 修改Program.cs以建立主機。咱們將使用host.Start而不是host.Run,它不會阻塞線程。以後,咱們將註冊該服務並在服務中止時取消註冊:

var host = new WebHostBuilder()
    .UseKestrel()
    .UseUrls("http://localhost:5200")
    .UseContentRoot(Directory.GetCurrentDirectory())
    .UseStartup<Startup>()
    .Build();

host.Start();

var client = new ConsulClient();

var name = Assembly.GetEntryAssembly().GetName().Name;
var port = 5200;
var id = $"{name}:{port}";

var tcpCheck = new AgentServiceCheck()
{
    DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
    Interval = TimeSpan.FromSeconds(30),
    TCP = $"127.0.0.1:{port}"
};

var httpCheck = new AgentServiceCheck()
{
    DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
    Interval = TimeSpan.FromSeconds(30),
    HTTP = $"http://127.0.0.1:{port}/HealthCheck"
};

var registration = new AgentServiceRegistration()
{
    Checks = new[] { tcpCheck, httpCheck },
    Address = "127.0.0.1",
    ID = id,
    Name = name,
    Port = port
};

client.Agent.ServiceRegister(registration).GetAwaiter().GetResult();

Console.WriteLine("DataService started...");
Console.WriteLine("Press ESC to exit");

while (Console.ReadKey().Key != ConsoleKey.Escape)
{
}

client.Agent.ServiceDeregister(id).GetAwaiter().GetResult();

此處輸入圖片的描述

而且(若是您已添加運行情況檢查控制器),它將成功運行兩個運行情況檢查:

此處輸入圖片的描述

我使用程序集名稱做爲服務名稱,我正在硬編碼端口和IP地址。顯然,這須要是可配置的,阻止控制檯線程的解決方案也不是很好。

更復雜的方式

瞭解基礎知識以及註冊過程的工做原理,讓咱們稍微改進一下實現。

目標

  • 能夠經過appsettings.json配置服務名稱
  • 主機和端口不該該是硬編碼的
  • 使用Microsoft.Extensions.Configuration和Options來正確配置咱們須要的全部內容
  • 將註冊設置爲Startup管道的一部分

Configuration

我定義了一個新的POCOs的配置文件在appsetting.json文件中,以下所示:

{
...
  "ServiceDiscovery": {
    "ServiceName": "DataService",
    "Consul": {
      "HttpEndpoint": "http://127.0.0.1:8500",
      "DnsEndpoint": {
        "Address": "127.0.0.1",
        "Port": 8600
      }
    }
  }
}

C#:

public class ServiceDisvoveryOptions
{
    public string ServiceName { get; set; }

    public ConsulOptions Consul { get; set; }
}

public class ConsulOptions
{
    public string HttpEndpoint { get; set; }

    public DnsEndpoint DnsEndpoint { get; set; }
}

public class DnsEndpoint
{
    public string Address { get; set; }

    public int Port { get; set; }

    public IPEndPoint ToIPEndPoint()
    {
        return new IPEndPoint(IPAddress.Parse(Address), Port);
    }
}

而後在Startup.ConfigureServices方法中進行配置:

services.AddOptions();
services.Configure<ServiceDisvoveryOptions>(Configuration.GetSection("ServiceDiscovery"));

使用此配置來設置consul客戶端:

services.AddSingleton<IConsulClient>(p => new ConsulClient(cfg =>
{
    var serviceConfiguration = p.GetRequiredService<IOptions<ServiceDisvoveryOptions>>().Value;

    if (!string.IsNullOrEmpty(serviceConfiguration.Consul.HttpEndpoint))
    {
        // if not configured, the client will use the default value "127.0.0.1:8500"
        cfg.Address = new Uri(serviceConfiguration.Consul.HttpEndpoint);
    }
}));

ConsulClient不必定須要配置,若是沒有指定,它將使用默認地址(localhost:8500)。

動態服務註冊

只要使用Kestrel在某個端口上託管服務,就可使用app.Properties["server.Features"]來肯定託管服務的位置。如上所述,若是使用IIS集成或任何其餘反向代理,此解決方案將再也不起做用,而且必須使用服務可訪問的實際端點來在Consul中註冊服務,而且在啓動期間沒法獲取該信息。

若是要將IIS集成與服務發現一塊兒使用,請不要使用如下代碼。而是經過配置配置端點,或手動註冊服務。

不管如何,對於Kestrel,咱們能夠執行如下操做:獲取URIs kestrel託管服務(這不適用於像UseUrls("*:5000")這樣的通配符,而後循環地址以在Consul中註冊全部地址:

ublic void Configure(
        IApplicationBuilder app,
        IApplicationLifetime appLife,
        ILoggerFactory loggerFactory,
        IOptions<ServiceDisvoveryOptions> serviceOptions,
        IConsulClient consul)
    {
        ...

        var features = app.Properties["server.Features"] as FeatureCollection;
        var addresses = features.Get<IServerAddressesFeature>()
            .Addresses
            .Select(p => new Uri(p));

        foreach (var address in addresses)
        {
            var serviceId = $"{serviceOptions.Value.ServiceName}_{address.Host}:{address.Port}";

            var httpCheck = new AgentServiceCheck()
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1),
                Interval = TimeSpan.FromSeconds(30),
                HTTP = new Uri(address, "HealthCheck").OriginalString
            };

            var registration = new AgentServiceRegistration()
            {
                Checks = new[] { httpCheck },
                Address = address.Host,
                ID = serviceId,
                Name = serviceOptions.Value.ServiceName,
                Port = address.Port
            };

            consul.Agent.ServiceRegister(registration).GetAwaiter().GetResult();

            appLife.ApplicationStopping.Register(() =>
            {
                consul.Agent.ServiceDeregister(serviceId).GetAwaiter().GetResult();
            });
        }

        ...

serviceId必須足夠獨特,以便稍後再次找到該服務的特定實例,以取消註冊它。我正在使用主機和端口以及實際的服務名稱的鏈接方式,這應該足夠好了。

這樣咱們就達到了全部的目標,雖然在啓動的時候寫了不少的代碼,不過咱們能夠重構一下使用擴展方法來改善。

查詢服務註冊信息

新服務正在運行並在Consul中註冊,如今應該很容易經過Consul API或DNS找到它。

使用Consul客戶端查詢

使用Consul客戶端,咱們可使用兩種Consul服務

  • 使用Catalog端點,它提供有關服務的原始信息,這個將返回未過濾的結果
var consulResult = await _consul.Catalog.Service(_options.Value.ServiceName);
  • 使用Health端點,它將返回已通過濾過的結果
var healthResult = await _consul.Health.Service(_options.Value.ServiceName, tag: null, passingOnly: true);

這裏須要注意的重要一點是,這些端點返回的服務列表(若是多個實例正在運行)將始終採用相同的順序。您必須實現邏輯,以便不會一直調用相同的服務端點,並在全部端點之間傳播流量。

一樣,這就是咱們可使用DNS的方式。除了創建負載平衡以外,優勢還在於,咱們沒必要再進行另外一次昂貴的http調用,而且而且把最終結果緩存一小段時間。使用DNS,咱們只需幾行代碼就能夠實現這一切。

使用DNS查詢

讓咱們用dig命令檢查DNS端點,以瞭解響應的樣子:

要求SRV記錄的域名語法是<servicename>.consul.service,這意味着咱們可使用dig @127.0.0.1 -p 8600 dataservice.service.consul SRV查詢咱們的dataService

; <<>> DiG 9.11.0-P2 <<>> @127.0.0.1 -p 8600 dataservice.service.consul SRV
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25053
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;dataservice.service.consul.    IN      SRV

;; ANSWER SECTION:
dataservice.service.consul. 0   IN      SRV     1 1 5200 machinename.node.eu-west.consul.

;; ADDITIONAL SECTION:
machinename.node.eu-west.consul. 0 IN      CNAME   localhost.

;; Query time: 0 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Tue Apr 25 21:08:19 DST 2017
;; MSG SIZE  rcvd: 109

咱們獲取SRV記錄中的端口,相應的CNAME記錄包含咱們用於註冊服務的主機名或地址.

Consul DNS端點還容許咱們查詢標籤並限制查詢僅查看一個特定的數據中心。 要查詢標記,咱們必須在標記和服務名稱前加上_: _<tag>._<serviceName>.service.consul,要指定數據中心查詢,將根域更改成<servicename>.service.<datacenter>.consul.

DNS負載均衡

DNS端點經過以交替順序返回結果來執行負載均衡。若是我在另外一個端口上啓動另外一個服務實例,咱們獲得:

;; QUESTION SECTION:
;dataservice.service.consul.    IN      SRV

;; ANSWER SECTION:
dataservice.service.consul. 0   IN      SRV     1 1 5200 machinename.node.eu-west.consul.
dataservice.service.consul. 0   IN      SRV     1 1 5300 machinename.node.eu-west.consul.

;; ADDITIONAL SECTION:
machinename.node.eu-west.consul. 0 IN      CNAME   localhost.
machinename.node.eu-west.consul. 0 IN      CNAME   localhost.

若是您運行查詢幾回,您將看到答案以不一樣的順序返回。

使用DnsClient

要經過C#代碼查詢DNS,我將使用個人DnsClient庫。我將ResolveService擴展方法添加到庫中,以使這些SRV查找很是簡單。
安裝DnsClient NuGet包後,我只需在DI中註冊一個DnsLookup客戶端:

services.AddSingleton<IDnsQuery>(p =>
{
    return new LookupClient(IPAddress.Parse("127.0.0.1"), 8600);
});
private readonly IDnsQuery _dns;
private readonly IOptions<ServiceDisvoveryOptions> _options;

public SomeController(IDnsQuery dns, IOptions<ServiceDisvoveryOptions> options)
{
    _dns = dns ?? throw new ArgumentNullException(nameof(dns));
    _options = options ?? throw new ArgumentNullException(nameof(options));
}

[HttpGet("")]
[HttpHead("")]
public async Task<IActionResult> DoSomething()
{
    var result = await _dns.ResolveServiceAsync("service.consul", _options.Value.ServiceName);
    ...
}

DnsClient.NETResolveServiceAsync執行DNS SRV查找,匹配CNAME記錄併爲包含主機名和端口(以及使用的地址)的每一個條目返回一個對象。
如今,咱們可使用簡單的HttpClient調用(或生成的客戶端)來調用服務:

var address = result.First().AddressList.FirstOrDefault();
var port = result.First().Port;

using (var client = new HttpClient())
{
    var serviceResult = await client.GetStringAsync($"http://{address}:{port}/Values");
}

結論

Consul是一個偉大,靈活和穩定的工具。我喜歡它的API和使用模式並非固定的,你能夠有不少選擇來使用服務註冊和其餘功能。與此同時,它的性能表現也是很是優異。
在今天來講,由於有了衆多的工具,在.NET中使用Consul也是很是簡單方便。若是你的程序有不一樣部分須要通信,那我肯定它能夠幫助你。

我在GitHub上整理了一個包含完整演示項目,把你的想法在評論中告訴我

原文地址:http://michaco.net/blog/ServiceDiscoveryAndHealthChecksInAspNetCoreWithConsul

相關文章
相關標籤/搜索