背景
在分布式或者微服務系統里,通過配置文件來管理配置內容,是一件比較令人痛苦的事情,再謹慎也有濕鞋的時候,這就是在項目架構發展的過程中,配置中心存在的意義。
其實配置中心的組件已經有非常出名的案例,比如攜程的阿波羅配置中心(https://github.com/ctripcorp/apollo)
為什么又造輪子,因為不想發布項目的時候到處切管理平台。
基本要求
作為一個通用的配置組件,需要支持如下功能:
1、客戶端定時刷新獲信最新配置信息並進行熱更新
2、配置有更新服務端主動推送重載或更新命令至客戶端進行配置獲取
所以涉及相對應組件如下:
1、支持廣播的消息通知組件,目前使用redis(StackExchange.Redis)、Zookeeper(Rabbit.Zookeeper)實現客戶端全局監聽服務,服務端可以推送不同組建不同的命令
2、支持定時獲取最新配置,目前使用HostedService實現全局統一啟動,客戶端實現全局啟動接口,接口使用Timer進行定時獲取配置
3、支持net core原生IConfiguration接口獲取配置中心數據
服務端設計
管理服務端主要實現:
1、三表增刪改查
2、配置內容表,每次新增或者修改,當前配置信息版本號為,所以配置最大版本號然后加一
3、應用表列表增加主動通知功能
配置查詢服務端
主要提供配置信息的查詢接口
1、接口入參如下
public class QueryConfigInput { [NotEmpty("config_001","AppId不能為空")] public string AppId { set; get; } public long Version { set; get; } [NotEmpty("config_002", "簽名不能為空")] public string Sign { set; get; } [NotEmpty("config_005", "NamespaceName不能為空")] public string NamespaceName { set; get; } public string Env { set; get; } }
2、查詢邏輯
2.1 入參基本驗證
2.2 AppId 密鑰進行簽名驗證
2.3 請求配置環境定位
2.4 查詢當前請求應用和共有配置應用
2.5 查詢大於當前查詢版本號的配置信息並返回
配置中心客戶端
客戶端主要實現原理和功能
1、配置信息請求,當前Http請求,需根據配置信息組合請求url,然后請求獲取配置,每次請求帶上當前配置最大版本號(在以后請求時只獲取有更新的配置)
2、配置信息本地存儲(容災),第一次獲取成功后,把配置信息進行版本文件存儲,以后的請求中當有配置更新時再進行文件存儲。
3、當配置請求失敗時進行本地文件配置信息的還原應用。
4、配置定時獲取
5、客戶端接收更新或者重載命令
6、原生IConfiguration配置查詢支持
部分功能介紹
客戶端參數
"ConfigServer": { "AppId": "PinzhiGO", "AppSercet": "xxxxxxxxxxxxx", "ServerUrl": "http://10.10.188.136:18081/", // 配置查詢服務端地址 "NamespaceName": "Pinzhi.Identity.WebApi", "Env": "dev", "RefreshInteval": 300 },
原生IConfiguration配置查詢
查看AddJsonFile源碼,可以發現實現自定義配置源,需要集成和實現ConfigurationProvider和IConfigurationSource兩個方法
代碼如下
public class BucketConfigurationProvider : ConfigurationProvider, IDataChangeListener, IConfigurationSource { private readonly ConfigurationHelper _configurationHelper; public BucketConfigurationProvider(BucketConfigOptions options) { _configurationHelper = new ConfigurationHelper(options); Data = new ConcurrentDictionary<string, string>(); } public override void Load() { DataChangeListenerDictionary.Add(this); Data = _configurationHelper.Get().ConfigureAwait(false).GetAwaiter().GetResult(); } private void SetData(ConcurrentDictionary<string, string> changeData) { foreach(var dic in changeData) { if (Data.ContainsKey(dic.Key)) Data[dic.Key] = dic.Value; else Data.Add(dic); } // Data = new Dictionary<string, string>(_configRepository.Data, StringComparer.OrdinalIgnoreCase); } public void OnDataChange(ConcurrentDictionary<string, string> changeData) { SetData(changeData); OnReload(); } public IConfigurationProvider Build(IConfigurationBuilder builder) => this; }
當有配置更新時,我們需要更新到ConfigurationProvider的Data中,所以我們需要實現自定義接口IDataChangeListener的OnDataChange方法,當客戶端請求發現有配置更新時,會調用接口的OnDataChange把最新的配置信息傳遞進來。
啟用原生IConfiguration方法如下:
.ConfigureAppConfiguration((hostingContext, _config) => { _config .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) .AddEnvironmentVariables(); // 添加環境變量 var option = new BucketConfigOptions(); _config.Build().GetSection("ConfigServer").Bind(option); _config.AddBucketConfig(option); })
定時配置獲取
常規做法是寫一個hostedservice的方法,然后寫一個timer去定時獲取,由於其他的組件可能都需要有定時的情況,我們統一處理了一下定時的任務,每個組件實現IExecutionService接口,然后組件會在啟動的時候循環調用IExecutionService的StartAsync的方法,組件包Bucket.Config.HostedService,原理比較簡單,使用代碼如下:
// 添加全局定時任務 services.AddBucketHostedService(builder => { builder.AddAuthorize().AddConfig().AddErrorCode(); });
public class AspNetCoreHostedService : IBucketAgentStartup { private readonly IEnumerable<IExecutionService> _services; public AspNetCoreHostedService(IEnumerable<IExecutionService> services) { _services = services; } public async Task StartAsync(CancellationToken cancellationToken = default(CancellationToken)) { foreach (var service in _services) await service.StartAsync(cancellationToken); } public async Task StopAsync(CancellationToken cancellationToken = default(CancellationToken)) { foreach (var service in _services) await service.StopAsync(cancellationToken); } }
組件命令監聽
和上面原則一樣,也進行了統一的封裝,目前監聽主要實現了redis和zookeeper,下面舉例redis
組件監聽需實現接口
public interface IBucketListener { string ListenerName { get; } Task ExecuteAsync(string commandText); }
命令序列化實體
public class NetworkCommand { public string NotifyComponent { set; get; } public string CommandText { set; get; } } public enum NetworkCommandType { /// <summary> /// 更新 /// </summary> Refresh, /// <summary> /// 重載 /// </summary> Reload, }
在hostedservice啟動時實現
public Task StartAsync(CancellationToken cancellationToken = default(CancellationToken)) { _subscriber = _redisClient.GetSubscriber(_redisListenerOptions.ConnectionString); return _subscriber.SubscribeAsync(RedisListenerKey, (channel, message) => { var command = JsonConvert.DeserializeObject<Bucket.Values.NetworkCommand>(message); _extractCommand.ExtractCommandMessage(command); }); }
在接口IExtractCommand里會根據各個監聽組件的ListenerName進行對應的調用
使用方法如下:
// 添加應用監聽 services.AddListener(builder => { //builder.UseRedis(); builder.UseZookeeper(); builder.AddAuthorize().AddConfig().AddErrorCode(); });
所以對應組件實現的命令監聽只要關心自身邏輯即可嗎,代碼如下
public class BucketConfigListener : IBucketListener { public string ListenerName => "Bucket.Config"; private readonly IDataRepository _dataRepository; public BucketConfigListener(IDataRepository dataRepository) { _dataRepository = dataRepository; } public async Task ExecuteAsync(string commandText) { if (!string.IsNullOrWhiteSpace(commandText) && commandText == NetworkCommandType.Refresh.ToString()) await _dataRepository.Get(); if (!string.IsNullOrWhiteSpace(commandText) && commandText == NetworkCommandType.Reload.ToString()) await _dataRepository.Get(true); } }
配置中心使用配置如下
.ConfigureAppConfiguration((hostingContext, _config) => { _config .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) .AddEnvironmentVariables(); // 添加環境變量 var option = new BucketConfigOptions(); _config.Build().GetSection("ConfigServer").Bind(option); _config.AddBucketConfig(option); }) // ConfigureServices // 添加配置服務 services.AddConfigServer(Configuration); // 添加應用監聽 services.AddListener(builder => { //builder.UseRedis(); builder.UseZookeeper(); builder.AddAuthorize().AddConfig().AddErrorCode(); }); // 添加全局定時任務 services.AddBucketHostedService(builder => { builder.AddAuthorize().AddConfig().AddErrorCode(); }); //使用 private readonly IConfiguration _configuration; private readonly IConfig _config; public AuthController(IConfiguration configuration, IConfig config) { _configuration= configuration; _config= config; } // 獲取值 _configuration.GetValue<string>("qqqq"); _config.StringGet("qqqq");
Appsettings.json相關配置信息轉移至配置中心
由於配置中心客戶端實現了原生的IConfiguration,所以appsetting的相關配置我們完全可以移至配置中心中,由於appsetting使用的是json,所以在配置中心服務端配置信息的Key需要轉換,舉例:
"BucketListener": { "Redis": { "ConnectionString": "127.0.0.1:6379,allowadmin=true", "ListenerKey": "Bucket.Sample" }, "Zookeeper": { "ConnectionString": "localhost:2181", "ListenerKey": "Bucket.Sample" } }
在配置中心key如下:
BucketListener:Redis:ConnectionString
BucketListener:Redis:ListenerKey
......
數組使用如下:
DbConfig:0:Name
DbConfig:0:DbType
DbConfig:1:Name
DbConfig:1:DbType
總結
個人寫作水平有限,涉及的東西也很多,篇幅有限所以只做了大體介紹,忘諒解
本章涉及源碼
https://github.com/q315523275/FamilyBucket/tree/master/src/Config 客戶端組件