服务发现—Asp.Net Core 结合Consul实现服务发现

服务注册与发现作为微服务基础设施中的一部分,应该是低代码侵入,只需要通过简单的配置就可开箱即用,普通业务开发人员无需关注的功能,不然的话,服务较多,服务实例较多的情况下,服务的管理将牵扯开发人员很大的精力。

Consul 的服务注册有两种方式,一种是通过 Client Agen 结合配置文件进行主动注册,一种通过 api 的方式进行注册,个人倾向于 api 方式,可以免去增加或减少服务时候需要去修改配置文件的情况。

Client Agent方式这里就不讲了,大家可以参考
.NET Core微服务之基于Consul实现服务治理(续)

1. Asp.Net Core通过 api 方式实现Consul服务注册与发现

Consul 的主要接口是一个 RESTful HTTP API。该 API 可以对节点、服务、检查、配置等执行基本的 CRUD 操作。

其中,服务注册的api是这个:/v1/agent/service/register

但是我们并不需要自己基于 http 请求去实现服务相关的操作,只需要引用两个开源的 nuget 包即可,里面已经基于官方提供的 RESTful HTTP API 封装好了 Consul 相关操作。

在这里插入图片描述
下面是 ASP.Net Core 接入 Consul 服务发现具体步骤

  1. 新建一个 ASP.Net Core Web api 应用

  2. 提供一个健康状态检查终结点

    这里利用 ASP.Net Core 自带的健康状态检查中间件,只需要在 Startup 类中简单配置即可

    public void ConfigureServices(IServiceCollection services)
    {
        // 应用健康状态检查
        services.AddHealthChecks();
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime hostApplicationLifetime)
    {
         // 启用健康状态检查中间件
         app.UseHealthChecks("/health");
     }
    

    启动应用后,通过 /health 路径即可访问健康状态检查接口,接口内部其实就是返回一个字符串,只要接口能够正常返回即说明应用正常。
    在这里插入图片描述

  3. 基于 Consul 和 Consul.AspNetCore 实现自动注册

    (1) 安装依赖的 nuget 包

    install-package Consul
    install-package Consul.AspNetCore
    

    基于 IServiceCollection 写一个扩展,进行 Consul 注册相关的配置和注入,主要是为了简化配置和代码

    public static class ConsulServiceCollectionExtensions
    {
       /// <summary>
       /// 向容器中添加Consul必要的依赖注入
       /// </summary>
       /// <param name="services"></param>
       /// <param name="configuration"></param>
       /// <returns></returns>
       public static IServiceCollection AddConsul(this IServiceCollection services, IConfiguration configuration)
       {
           // 配置consul服务注册信息
           var option = configuration.GetSection("Consul").Get<ConsulOption>();
           // 通过consul提供的注入方式注册consulClient
           services.AddConsul(options => options.Address = new Uri($"http://{option.ConsulIP}:{option.ConsulPort}"));
    
           // 通过consul提供的注入方式进行服务注册
           var httpCheck = new AgentServiceCheck()
           {
               DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后注册
               Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳间隔
               HTTP = $"http://{option.IP}:{option.Port}/health",//健康检查地址
               Timeout = TimeSpan.FromSeconds(5)
           };
    
           // Register service with consul
           services.AddConsulServiceRegistration(options => 
           {
               options.Checks = new[] { httpCheck };
               options.ID = Guid.NewGuid().ToString();
               options.Name = option.ServiceName;
               options.Address = option.IP;
               options.Port = option.Port;
               options.Meta = new Dictionary<string, string>() { { "Weight", option.Weight.HasValue ? option.Weight.Value.ToString() : "1" } };
               options.Tags = new[] { $"urlprefix-/{option.ServiceName}" }; //添加 
    urlprefix-/servicename 格式的 tag 标签,以便 Fabio 识别
           });
    
           services.AddSingleton<IConsulServerManager, DefaultConsulServerManager>();
           return services;
       }
    }
    

    通过查看 Consul 包的源码,可以发现这里的 AddConsulServiceRegistration 就是添加一个主机服务,在应用启动的时候向 consul 进行注册,在应用关闭的时候结束注册.
    在这里插入图片描述
    在这里插入图片描述
    而注册的时候就是调用 consul 提供的 RESTful api 接口
    在这里插入图片描述
    其中的ConsulOption类如下:

    public class ConsulOption
    {
        /// <summary>
        /// 当前应用IP
        /// </summary>
        public string IP { get; set; }
    
        /// <summary>
        /// 当前应用端口
        /// </summary>
        public int Port { get; set; }
    
        /// <summary>
        /// 当前服务名称
        /// </summary>
        public string ServiceName { get; set; }
    
        /// <summary>
        /// Consul集群IP
        /// </summary>
        public string ConsulIP { get; set; }
    
        /// <summary>
        /// Consul集群端口
        /// </summary>
        public int ConsulPort { get; set; }
    
        /// <summary>
        /// 负载均衡策略
        /// </summary>
        public string LBStrategy { get; set; }
    
        /// <summary>
        /// 权重
        /// </summary>
        public int? Weight { get; set; }
    }
    

    这里有几个注意点:

    1)AgentServiceCheck

    由于 consul 对服务状态可用状态检查是通过服务端轮询访问应用的,所以在服务注册时需要提供健康状态检查终结点,这也是 step2 中配置健康状态检查的原因

    2)Meta中的权重和 IConsulServerManager

    这两者是为了实现服务间调用问题的,后面会细讲。

  4. 在 startup 中进行注入

    public void ConfigureServices(IServiceCollection services)
     {
         // 应用健康状态检查
         services.AddHealthChecks();
         // 配置consul服务注册信息
         services.AddConsul(Configuration);
     }
    
  5. 在appsetting.json增加Consul配置节点

     "Consul": {
        "ConsulIP": "192.168.137.200",
        "ConsulPort": "8500",
        "ServiceName": "yyl.ClientService",
        "Ip": "192.168.2.103",
        "Port": "5000",
        "Weight": 1,
        "LBStrategy": "WeightRoundRobin",
        "FloderName": "consulConfig", //这两个节点可暂时忽略,是为了使用consul配置中心而设置的
        "FileName": "config" //这两个节点可暂时忽略,是为了使用consul配置中心而设置的
      }
    

    之后启动 web api 应用,在 consul 管理平台即可看到注册上去的服务
    在这里插入图片描述
    最基本的服务注册到此就已经完成了,我们可以通过服务注册管理页面清晰的看到现在我们有哪些服务,哪些服务是健康可用的。

2. Consul 服务发现

在微服务架构中,很多时候需要调用很多服务才能完成一项功能。服务之间如何互相调用就变成微服务架构中的一个关键问题。微服务内部间的调用有多种方式,包括 RPC、基于消息队列的事件驱动,还有最基本的RESTful Api。

对于 RESTful Api,当一个服务需要调用另一个服务时,都是先获取调用服务的基本信息,如ip、端口等,需要知道当前有哪些服务,哪些服务是可用的,这时就需要服务发现,去获取注册中心中可用的服务。

Consul 的服务发现通过 API 的方式提供,通过服务名称在 web 浏览器中就可以看到当前服务的相关信息。
在这里插入图片描述
当然,通过 Consul 包,我们在应用中也可以快捷的调用这些服务发现相关的接口,而不用自己去构建 http 请求。

这里最基本的用法是注入 IConsulClient 接口,通过其中的 IHealthEndpoint 用于获取健康可用的服务,这里新建了一个 ServerController 作为服务发现的测试。

[ApiController]
[Route("/api/[Controller]")]
public class ServerController : ControllerBase
{
    private readonly IConsulClient _consulClient;
    private readonly IConsulServerManager _consulServerManager;
    public ServerController(IConsulServerManager consulServerManager, IConsulClient consulClient)
    {
        _consulServerManager = consulServerManager;
		_consulClient = consulClient;
    }

    [HttpGet]
    [Route("")]
    public async Task<string> Get()
    {
        return await _consulServerManager.GetServerAsync("yyl.ClientService1");
    }

    public async Task GetServiceAsync()
    {
        var result = await _consulClient.Health.Service("yyl.ClientService1");
    }
}

微服务内部各个服务的相互调用,我们公司在工作中的实践是不通过网关,各个服务之间自由调用,这里我自己写了一个 IConsulServerManager 接口及其实现,主要是处理服务间调用的负载均衡。一般情况下,相同的服务会启动多个实例,在我们需要调用一个服务的时候,应该调用哪个实例,这时负载均衡策略是很有必要的,如果没有负载均衡可能导致所有请求集中在一个实例,导致其压力过大,而其他实例闲置。

IConsulServerManager 及其实现类代码如下,这里实现了随机、轮询、加权随机、加权轮询等几种简单的方式

public interface IConsulServerManager
{
    Task<string> GetServerAsync(string serviceName);

    Task<AgentService> GetServerInfoAsync(string serviceName);

    Task<IList<AgentService>> GetServerListAsync(string serviceName);
}
public class DefaultConsulServerManager : IConsulServerManager
{
    private readonly ConsulOption _consulOption;
    private readonly IConsulClient _consulClient;
    
    private ConcurrentDictionary<string, int> ServerCalls = new ConcurrentDictionary<string, int>();
    public DefaultConsulServerManager(IConsulClient consulClient, IOptions<ConsulOption> options)
    {
        _consulClient = consulClient;
        _consulOption = options.Value;
    }

    /// <summary>
    /// 根据负载均衡策略获取服务地址
    /// </summary>
    /// <param name="serviceName"></param>
    /// <returns></returns>
    public async Task<string> GetServerAsync(string serviceName)
    {
        var service = await GetServerInfoAsync(serviceName);
        return $"http://{ service.Address }:{ service.Port }";
    }

    /// <summary>
    /// 根据负载均衡策略获取服务信息
    /// </summary>
    /// <param name="serviceName"></param>
    /// <returns></returns>
    public async Task<AgentService> GetServerInfoAsync(string serviceName)
    {
        var services = await GetServerListAsync(serviceName);

        // 安装负载均衡策略进行返回服务地址
        return BalancingRoute(services, serviceName);
    }

    /// <summary>
    /// 获取服务列表
    /// </summary>
    /// <param name="serviceName"></param>
    /// <returns></returns>
    public async Task<IList<AgentService>> GetServerListAsync(string serviceName)
    {
        var result = await _consulClient.Health.Service(serviceName);

        if (result.StatusCode != System.Net.HttpStatusCode.OK)
        {
            throw new ConsulRequestException("获取服务信息失败!", result.StatusCode);
        }

        return result.Response.Select(s => s.Service).ToList();
    }


    private AgentService BalancingRoute(IList<AgentService> services, string key)
    {
        if(services == null || !services.Any())
        {
            throw new ArgumentNullException(nameof(services), $"当前未找到{key}可用服务!");
        }

        switch (_consulOption.LBStrategy)
        {
            case "First":
                return First(services);
            case "Random":
                return Random(services);
            case "RoundRobin":
                return RoundRobin(services, key);
            case "WeightRandom":
                return WeightRandom(services);
            case "WeightRoundRobin":
                return WeightRoundRobin(services, key);
            default:
                return RoundRobin(services, key);
        }
    }

    /// <summary>
    /// 第一个
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    private AgentService First(IList<AgentService> services)
    {
        return services.First();
    }

    /// <summary>
    /// 随机
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    private AgentService Random(IList<AgentService> services)
    {
        return services.ElementAt(new Random().Next(0, services.Count()));
    }

    /// <summary>
    /// 轮询
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    private AgentService RoundRobin(IList<AgentService> services, string key)
    {
        var count = ServerCalls.GetOrAdd(key, 0);
        var service = services.ElementAt(count++ % services.Count());
        ServerCalls[key] = count;
        return service;
    }

    /// <summary>
    /// 加权随机
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public AgentService WeightRandom(IList<AgentService> services)
    {
        var pairs = services.SelectMany(s =>
        {
            var weight = 1;
            if (s.Meta.ContainsKey("Weight") && int.TryParse(s.Meta["Weight"], out int w))
            {
                weight = w;
            }
            var result = new List<AgentService>();
            for (int i = 0; i < weight; i++)
            {
                result.Add(s);
            }
            return result;
        }).ToList();
        return Random(pairs);
    }

    /// <summary>
    /// 加权轮询
    /// </summary>
    /// <param name="services"></param>
    /// <param name="key"></param>
    /// <returns></returns>
    public AgentService WeightRoundRobin(IList<AgentService> services, string key)
    {
        var pairs = services.SelectMany(s =>
        {
            var weight = 1;
            if (s.Meta.ContainsKey("Weight") && int.TryParse(s.Meta["Weight"], out int w))
            {
                weight = w;
            }
            var result = new List<AgentService>();
            for (int i = 0; i < weight; i++)
            {
                result.Add(s);
            }
            return result;
        }).ToList();
        return RoundRobin(pairs, key);
    }
}

结合上面的依赖注入扩展和 appsetting.json 中的配置,可以实现内部服务调用间的负载均衡。

测试结果如下:
在这里插入图片描述
在这里插入图片描述
Consul 包中还提供了其他通过 Api 方式对 Consul 服务端进行调用的封装,大家可以研究,或者在工作实践中去了解和使用。

Consul 服务注册与发现demo代码可在以下地址查看

生产环境实践中,可以将 Consul 相关操作独立成一个类库,打包成nuget包,这一部分是可以在不同服务中开箱即用的。

微服务系列文章:
上一篇:服务发现—Consul部署
下一篇: 服务间的通讯—工作实践

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐