1. 程式人生 > >Service Discovery And Health Checks In ASP.NET Core With Consul

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

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

這篇文章的所有原始碼都可以在GitHub上Demo專案獲得.

Service Discovery

在現代微服務架構中,服務可以在容器中執行,並且可以動態啟動,停止和擴充套件。 這導致了一個非常動態的託管環境,可能有數百個實際端點,無法手動配置或找到正確的端點。

話雖這麼說,我相信服務發現不僅適用於生活在容器中的粒狀微服務。它可以被任何必須訪問其他資源的應用程式使用。資源可以是資料庫,其他Web服務,也可以是託管在其他地方的網站的一部分。服務發現有助於擺脫特定於環境的配置檔案!

服務發現可用於解決此問題,但通常,有許多不同的方法來實現它

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

Consul是一個服務登錄檔,可用於實現客戶端服務發現。

除了使用這種方法的許多強大功能和優點之外,它的缺點是每個客戶端應用程式都需要實現一些邏輯來使用此中央登錄檔。這個邏輯可能非常具體,因為Consul和任何其他技術都有自定義API和邏輯工作方式。

負載平衡也可能無法自動完成。客戶端可以查詢服務的所有可用/已註冊端點,然後決定選擇哪個端點。
好的是Consul不僅帶有REST API來查詢服務登錄檔,它還提供DNS端點,返回標準SRV和TXT記錄。

DNS端點確實關心服務執行狀況,因為它不會返回不健康的服務例項。它還通過以交替順序返回記錄來進行負載平衡! 此外,它可能使服務具有更高的優先順序,更接近客戶端。

現在,讓我們開始......

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