Consul Key/Value存储

Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其它分布式服务注册与发现的方案,Consul 的方案更“一站式”,内置了服务注册与发现框架、分布式一致性协议实现、健康检查、Key/Value 存储(配置中心)、多数据中心方案,不再需要依赖其它工具(比如 ZooKeeper 等),使用起来也较为简单。

请注意这里,Consul内置了Key/Value存储,这里的Key/Value存储也可以当作简单的配置中心使用,如果使用了Consul作为服务注册发现,不想再额外引入其他中间件的情况下,可以将一些公共配置信息配置到Consul,然后通过Consul提供的 HTTP API来获取对应Key的Value。

需要注意的是,Consul遵循ACP原则中的CP原则(一致性+分离容忍),保证数据强一致性,所以当数据在同步时或者Leader挂掉,Server在重新选举Leader过程中,会出现集群不可用。

还有一点缺点就是,Consul不支持配置信息历史版本管理。

通过默认端口8500进入consul管理页面,点击左侧的Key/Value菜单,可以看到Consul的配置信息管理页面

在这里插入图片描述
点击Create创建配置信息,这里可以直接创建配置信息,也可以创建文件夹,如果是创建文件夹的话,要以 / 结尾
在这里插入图片描述
我们可以利用多重文件夹的方式,来区分不同微服务、不同应用、不同环境的配置文件。
在这里插入图片描述

.Net Core集成Consul配置中心

Consul提供了一系列的RESTful HTTP API以供用接入Consul的应用对对节点、服务、检查、配置等执行基本的 CRUD 操作。

其中,Key/Value存储相关的Api在这里可以看到:KV Store - HTTP API | Consul by HashiCorp

我们可以直接通过postman获取到Consul中的配置信息:
在这里插入图片描述
其中value是配置信息字符串,直接通过url获取到的是经过base64加密的结果。

而在 .Net Core 中,之前已经讲到,我们并不需要自己基于http请求去实现服务相关的操作,只需要引用Consul.AspNetCore包即可,里面已经基于官方提供的RESTful HTTP API封装好了consul相关操作。

  1. 安装依赖包

    Install-package Consul.AspNetCore
    
  2. 配置Consul依赖注入
    在starup.cs中添加Consul依赖

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddConsul(Configuration);
    }
    

    services.AddConsul(Configuration);是自己写的一个扩展,不是Consul.AspNetCore中的方法,目的是为了直接从配置文件中读取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}"));
            return services;
        }
    }
    

    配置信息如下:

    "Consul": {
        "ConsulIP": "192.168.137.200",
        "ConsulPort": "8500",
       "FloderName": "test/service1",
        "FileName": "appsetting-dev"
      }
    

    这里只演示key/Value存储的获取和使用,所以就不将当前应用注册到Consul中了。

  3. 获取Consul中的配置信息
    注册了consulClient之后,我们就可以通过IConsulClient接口对Consul集群中的Key/Value存储进行操作了。

    [Route("/api/[Controller]")]
    public class ConfigController : ControllerBase
    {
        private readonly IConsulClient _consulClient;
        private readonly IConfiguration _configuration;
        public ConfigController(IConsulClient consulClient,
            IConfiguration configuration)
        {
            _consulClient = consulClient;
            _configuration = configuration;
        }
    
        [HttpGet]
        [Route("")]
        public async Task<string> Get(string key)
        {
            var result = await _consulClient.KV.Get(key);
            if (result.StatusCode != System.Net.HttpStatusCode.OK)
            {
                throw new ConsulRequestException("获取服务信息失败!", result.StatusCode);
            }
            var kvs = result.Response;
            return Encoding.UTF8.GetString(kvs.Value);
        }
    
        [HttpGet]
        [Route("list")]
        public async Task<IList<KVPair>> GetList(string prefix)
        {
            var result = await _consulClient.KV.List(prefix);
            if (result.StatusCode != System.Net.HttpStatusCode.OK)
            {
                throw new ConsulRequestException("获取服务信息失败!", result.StatusCode);
            }
            var kvs = result.Response;
            return kvs.ToList();
        }
    }
    

    在这里插入图片描述

  4. 应用启动时加载Consul配置信息,并进行热更新
    通过上面的方式已经可以获取到Consul Key/Value存储中的配置信息了,但是我们肯定不希望每次需要使用配置信息的时候这样去获取,而是希望和.Net Core中的Configuration结合,在启动的时候将配置信息加载到应用中,并且当Consul中的配置信息修改时,本地的配置能够更新。

    以下的代码结合.Net Core的配置管理机制以及IConsulClient实现这样的功能, .Net Core的配置管理允许我们实现自己的配置信息提供者,加入到IConfigurationBuilder中,作为我们平常使用的IConfiguration的配置来源,所有第三方的配置信息提供程序都是这么实现的。

    (1) 实现 IConfigurationSource,提供Consul配置源

    public class ConsulConfigurationSource : IConfigurationSource
    {
        /// <summary>
        /// Consul集群IP
        /// </summary>
        public string ConsulIP { get; set; }
    
        /// <summary>
        /// Consul集群端口
        /// </summary>
        public int ConsulPort { get; set; }
    
        /// <summary>
        /// 配置文件夹名称。类似于命名空间
        /// </summary>
        public string FloderName { get; set; }
    
        /// <summary>
        /// 配置文件名称集合
        /// </summary>
        public string FileName { get; set; }
    
      // 这里需要一个配置信息提供者
        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new ConsulConfigurationProvider(this);
        }
    }
    

    (2) 继承 ConsulConfigurationProvider 实现ConsulConfigurationProvider

    public class ConsulConfigurationProvider : ConfigurationProvider, IDisposable
    {
       protected readonly ConsulConfigurationSource ConfigurationSource;
       protected readonly ConcurrentDictionary<string, byte[]> ConfigCaches = new ConcurrentDictionary<string, byte[]>();
       protected readonly Dictionary<string, Timer> Timers = new Dictionary<string, Timer>();
       protected readonly IConsulClient _consulClient;
       public ConsulConfigurationProvider(ConsulConfigurationSource configurationSource)
       {
           ConfigurationSource = configurationSource;
           _consulClient = new ConsulClient(x => x.Address = new Uri($"http://{ConfigurationSource.ConsulIP}:{ConfigurationSource.ConsulPort}"));
       }
    
       public override void Load()
       {
           LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
       }
    
       private async Task LoadAsync()
       {
           var kv = await GetRemoteConfiguration();
           // 记录当前的配置信息
           ConfigCaches[GetConfigKey()] = kv.Value;
           var targetKvs = Flatten(kv);
           
           //将配置的信息转移到Data中,后面的覆盖前面的
           foreach (var item in targetKvs)
           {
               Data[ConfigurationPath.Combine(item.Key.Split("/"))] = item.Value;
           }
    
           // 启动轮询, 监听配置信息的改变
           await ListenToConfigurationChanged();
       }
    
       /// <summary>
       /// 调用 consul 配置中心api, 加载远程配置
       /// </summary>
       /// <returns></returns>
       private async Task<KVPair> GetRemoteConfiguration()
       {
           var targetKeys = string.Join("/", ConfigurationSource.FloderName, ConfigurationSource.FileName);
           var result = await _consulClient.KV.Get(targetKeys);
           if (result.StatusCode != System.Net.HttpStatusCode.OK)
           {
               throw new ConsulRequestException("获取服务信息失败!", result.StatusCode);
           }
    
           var kvs = result.Response;
           return kvs;
       }
    
       /// <summary>
       /// 长轮询,获取远程配置文件的变化
       /// </summary>
       /// <returns></returns>
       private async Task ListenToConfigurationChanged()
       {
           var timer = new Timer(async x =>
           {
               var config = await GetRemoteConfiguration();
               // 和当前缓存的配置新比较
               var configCache = ConfigCaches[GetConfigKey()];
    
               // TODO: 如果两者不同,则重新加载配置信息
               var targetKvs = Flatten(config);
               //将配置的信息转移到Data中,后面的覆盖前面的
               foreach (var item in targetKvs)
               {
                   Data[ConfigurationPath.Combine(item.Key.Split("/"))] = item.Value;
               }
               OnReload();
           }, "", 0, 8000);
           Timers[GetConfigKey()] = timer;
       }
    
       /// <summary>
       /// 处理配置文件
       /// </summary>
       /// <param name="tuple"></param>
       /// <param name="prefixKey"></param>
       /// <returns></returns>
       private IEnumerable<KeyValuePair<string, string>> Flatten(KVPair kv)
       {
           var content = Encoding.UTF8.GetString(kv.Value);
           // 反序列化,将配置文件字符串转换为对象树
           var data = JToken.Parse(content);
           // 通过对象树构建Data
           return Flatten(KeyValuePair.Create(string.Empty, data));
       }
    
       /// <summary>
       /// 递归遍历配置文件,读取json中的每一个键值
       /// </summary>
       /// <param name="tuple"></param>
       /// <param name="prefixKey"></param>
       /// <returns></returns>
       private IEnumerable<KeyValuePair<string, string>> Flatten(KeyValuePair<string, JToken> tuple)
       {
           if (!(tuple.Value is JObject value))
           {
               yield break;
           }
    
           foreach (var property in value)
           {
               var propertyKey = string.IsNullOrEmpty(tuple.Key) ? property.Key : string.Join("/", tuple.Key, property.Key);
               switch(property.Value.Type)
               {
                   case JTokenType.Object:
                       foreach (var item in Flatten(KeyValuePair.Create(propertyKey, property.Value)))
                       {
                           yield return item;
                       }
                       break;
                   case JTokenType.Array:
                       break;
                   default:
                       yield return KeyValuePair.Create(propertyKey, property.Value.Value<string>());
                       break;
               }
           }
       }
    
       /// <summary>
       /// 缓存信息key
       /// </summary>
       /// <returns></returns>
       private string GetConfigKey()
       {
           return $"{ConfigurationSource.FloderName}-{ConfigurationSource.FileName}";
       }
    
       public void Dispose()
       {
           foreach (var timer in Timers)
           {
               timer.Value.Dispose();
           }
       }
    }
    

    (3)提供向 IConfigurationBuilder 添加配置源的扩展方法

    public static class ConsulConfigurationExtensions
    {
        /// <summary>
        /// 通过配置信息,连接consul配置中心,加载配置信息
        /// </summary>
        /// <param name="configurationBuilder"></param>
        /// <returns></returns>
        public static IConfigurationBuilder AddConsul(this IConfigurationBuilder configurationBuilder, IConfiguration configuration)
        {
            var configurationSource = new ConsulConfigurationSource();
            configuration.Bind(configurationSource);
            return configurationBuilder.Add(configurationSource);
        }
    
        /// <summary>
        /// 通过配置信息,连接consul配置中心,加载配置信息
        /// </summary>
        /// <param name="configurationBuilder"></param>
        /// <returns></returns>
        public static IConfigurationBuilder AddConsul(this IConfigurationBuilder configurationBuilder, Action<ConsulConfigurationSource>  action)
        {
            var configurationSource = new ConsulConfigurationSource();
            action.Invoke(configurationSource);
            return configurationBuilder.Add(configurationSource);
        }
    }
    

    (4)在应用启动的时候添加Consul配置源

    public static IHostBuilder CreateHostBuilder(string[] args) =>
              Host.CreateDefaultBuilder(args)
                  .ConfigureAppConfiguration((context, builder) => 
                  { 
                      var config = builder.Build();
                      builder.AddConsul(config.GetSection("Consul"));
                  })
                  .ConfigureWebHostDefaults(webBuilder =>
                  {
                      webBuilder.UseStartup<Startup>();
                  });
    

    (5)通过IConfiguration获取Consul配置

    [Route("/api/[Controller]")]
     public class ConfigController : ControllerBase
     {
         private readonly IConsulClient _consulClient;
         private readonly IConfiguration _configuration;
         public ConfigController(IConsulClient consulClient,
             IConfiguration configuration)
         {
             _consulClient = consulClient;
             _configuration = configuration;
         }[HttpGet]
         [Route("GetConfigByIConfiguration")]
         public string GetConfigByIConfiguration()
         {
             var con = _configuration["AppName"];
             return con;
         }
    }
    

    在这里插入图片描述
    其实上面的这部分.Net Core配置管理与Consul Key/Value存储集成的实现也可以不用自己写,.Net Core生态圈中已经有开源的包可以使用,那就是 Winton.Extensions.Configuration.Consul,大家可以通过链接到github中了解。

    最基本的使用如下:

    Install-package Winton.Extensions.Configuration.Consul
    
    public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((context, builder) => 
                {
                    //var config = builder.Build();
                    //builder.AddConsul(config.GetSection("Consul"));
                    builder.AddConsul("/test/service1/appsetting-dev", options => 
                    {
                        options.ConsulConfigurationOptions = cco => { cco.Address = new Uri("http://192.168.137.200:8500"); }; // 1、consul地址
                        options.Optional = true; // 2、配置选项
                        options.ReloadOnChange = true; // 3、配置文件更新后重新加载
                        options.OnLoadException = exceptionContext => { exceptionContext.Ignore = true; }; // 4、忽略异常
                    });
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    
    var con = _configuration["test:service1:appsetting-dev:AppName"];
    

微服务系列文章:
上一篇:配置中心—nacos配置中心
下一篇:API网关—Ocelot

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐