在這篇文章中,我們將快速了解一下服務發現是什么,使用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提供了添加或修改服務注冊表的不同方法。一種選擇是將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.NET的ResolveServiceAsync執行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
