服务发现—Asp.net core 结合consul实现服务发现

服务发现—Asp.net core 结合consul实现服务发现

  • Asp.net core通过api方式实现consul服务注册与发现
  • Consul 服务发现

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

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

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

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接入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路径即可访问健康状态检查接口,接口内部其实就是返回一个字符串,只要接口能够正常返回即说明应用正常。
    服务发现—Asp.net core 结合consul实现服务发现_第2张图片

  3. 基于 Consul和Consul.AspNetCore实现自动注册
    (1) 安装依赖的nuget包

    install-package Consul
    install-package Consul.AspNetCore
    

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

    public static class ConsulServiceCollectionExtensions
    {
       /// 
       /// 向容器中添加Consul必要的依赖注入
       /// 
       /// 
       /// 
       /// 
       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进行注册,在应用关闭的时候结束注册.
    服务发现—Asp.net core 结合consul实现服务发现_第3张图片
    服务发现—Asp.net core 结合consul实现服务发现_第4张图片
    而注册的时候就是调用consul提供的RESTful api接口
    服务发现—Asp.net core 结合consul实现服务发现_第5张图片
    其中的ConsulOption类如下:

    public class ConsulOption
    {
        /// 
        /// 当前应用IP
        /// 
        public string IP { get; set; }
    
        /// 
        /// 当前应用端口
        /// 
        public int Port { get; set; }
    
        /// 
        /// 当前服务名称
        /// 
        public string ServiceName { get; set; }
    
        /// 
        /// Consul集群IP
        /// 
        public string ConsulIP { get; set; }
    
        /// 
        /// Consul集群端口
        /// 
        public int ConsulPort { get; set; }
    
        /// 
        /// 负载均衡策略
        /// 
        public string LBStrategy { get; set; }
    
        /// 
        /// 权重
        /// 
        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管理平台即可看到注册上去的服务
    服务发现—Asp.net core 结合consul实现服务发现_第6张图片
    最基本的服务注册到此就已经完成了,我们可以通过服务注册管理页面清晰的看到现在我们有哪些服务,哪些服务是健康可用的。

Consul 服务发现

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

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

Consul的服务发现通过API的方式提供,通过服务名称在web浏览器中就可以看到当前服务的相关信息。
服务发现—Asp.net core 结合consul实现服务发现_第7张图片
当然,通过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;
    }

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

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

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

    /// 
    /// 获取服务列表
    /// 
    /// 
    /// 
    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);
        }
    }

    /// 
    /// 第一个
    /// 
    /// 
    /// 
    private AgentService First(IList<AgentService> services)
    {
        return services.First();
    }

    /// 
    /// 随机
    /// 
    /// 
    /// 
    private AgentService Random(IList<AgentService> services)
    {
        return services.ElementAt(new Random().Next(0, services.Count()));
    }

    /// 
    /// 轮询
    /// 
    /// 
    /// 
    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;
    }

    /// 
    /// 加权随机
    /// 
    /// 
    /// 
    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);
    }

    /// 
    /// 加权轮询
    /// 
    /// 
    /// 
    /// 
    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中的配置,可以实现内部服务调用间的负载均衡。

测试结果如下:
服务发现—Asp.net core 结合consul实现服务发现_第8张图片
服务发现—Asp.net core 结合consul实现服务发现_第9张图片
Consul包中还提供了其他通过Api方式对Consul服务端进行调用的封装,大家可以研究,或者在工作实践中去了解和使用。

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

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

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

你可能感兴趣的:(微服务架构学习与实践总结,微服务,consul,服务发现,.net,core)