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

相关文章
相关标签/搜索