如何在 ASP.Net Core 中使用 Consul 来存储配置
原文:?USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET CORE
作者: Nathanael
[譯者注:因急于分享給大家,所以本文翻譯的很倉促,有些不準確的地方還望諒解]
來自 Hashicorp 公司的 Consul 是一個用于分布式架構的工具,可以用來做服務發現、運行健康檢查和 kv 存儲。本文詳細介紹了如何使用 Consul 通過實現 ConfigurationProvider 在 ASP.Net Core 中存儲配置。
為什么使用工具來存儲配置?
通常,.Net 應用程序中的配置存儲在配置文件中,例如 App.config、Web.config 或 appsettings.json。從 ASP.Net Core 開始,出現了一個新的可擴展配置框架,它允許將配置存儲在配置文件之外,并從命令行、環境變量等等中檢索它們。
配置文件的問題是它們很難管理。實際上,我們通常最終做法是使用配置文件和對應的轉換文件,來覆蓋每個環境。它們需要與 dll 一起部署,因此,更改配置意味著重新部署配置文件和 dll 。不太方便。
使用單獨的工具集中化可以讓我們做兩件事:
在所有機器上具有相同的配置
能夠在不重新部署任何內容的情況下更改值(對于功能啟用關閉很有用)
Consul 介紹
本文的目的不是討論?Consul,而是專注于如何將其與 ASP.Net Core 集成。
但是,簡單介紹一下還是有必要的。Consul 有一個 Key/Value 存儲功能,它是按層次組織的,可以創建文件夾來映射不同的應用程序、環境等等。這是一個將在本文中使用的層次結構的示例。每個節點都可以包含 JSON 值。
它提供了 REST API 以方便查詢,key 包含在查詢路徑中。例如,獲取 App1 在 Dev 環境中的配置的查詢如下所示:GET?http://:8500/v1/kv/App1/Dev/Settings
響應如下:
也可以以遞歸方式查詢任何節點,GET?http://:8500/v1/kv/App1/Dev?recurse 返回 :
HTTP/1.1 200 OKContent-Type: application/jsonX-Consul-Index: 1071X-Consul-Knownleader: trueX-Consul-Lastcontact: 0[{ ? ? ? ?"LockIndex": 0, ? ? ? ?"Key": "App1/Dev/", ? ? ? ?"Flags": 0, ? ? ? ?"Value": null, ? ? ? ?"CreateIndex": 75, ? ? ? ?"ModifyIndex": 75},{ ? ? ? ?"LockIndex": 0, ? ? ? ?"Key": "App1/Dev/ConnectionStrings", ? ? ? ?"Flags": 0, ? ? ? ?"Value": "ewoiRGF0YWJhc2UiOiAiU2VydmVyPXRjcDpkYmRldi5kYXRhYmFzZS53aW5kb3dzLm5ldDtEYXRhYmFzZT1teURhdGFCYXNlO1VzZXIgSUQ9W0xvZ2luRm9yRGJdQFtzZXJ2ZXJOYW1lXTtQYXNzd29yZD1teVBhc3N3b3JkO1RydXN0ZWRfQ29ubmVjdGlvbj1GYWxzZTtFbmNyeXB0PVRydWU7IiwKIlN0b3JhZ2UiOiJEZWZhdWx0RW5kcG9pbnRzUHJvdG9jb2w9aHR0cHM7QWNjb3VudE5hbWU9ZGV2YWNjb3VudDtBY2NvdW50S2V5PW15S2V5OyIKfQ==", ? ? ? ?"CreateIndex": 155, ? ? ? ?"ModifyIndex": 155},{ ? ? ? ?"LockIndex": 0, ? ? ? ?"Key": "App1/Dev/Settings", ? ? ? ?"Flags": 0, ? ? ? ?"Value": "ewogIkludCI6NDIsCiAiT2JqZWN0IjogewogICJTdHJpbmciOiAidG90byIsCiAgIkJsYSI6IG51bGwsCiAgIk9iamVjdCI6IHsKICAgIkRhdGUiOiAiMjAxOC0wMi0yM1QxNjoyMTowMFoiCiAgfQogfQp9Cgo=", ? ? ? ?"CreateIndex": 501, ? ? ? ?"ModifyIndex": 1071} ]我們可以看到許多內容通過這個響應,首先我們可以看到每個 key 的 value 值都使用了 Base64 編碼,以避免 value 值和 JSON 本身混淆,然后我們注意到屬性“Index”在 JSON 和 HTTP 頭中都有。 這些屬性是一種時間戳,它們可以我們知道是否或何時創建或更新的 value。它們可以幫助我們知道是否需要重新加載這些配置了。
ASP.Net Core 配置系統
這個配置的基礎結構依賴于 Microsoft.Extensions.Configuration.Abstractions NuGet包中的一些內容。首先,IConfigurationProvider 是用于提供配置值的接口,然后IConfigurationSource 用于提供已實現上述接口的 provider 的實例。
您可以在?ASP.Net GitHub?上查看一些實現。
與直接實現 IConfigurationProvider 相比,可以在 Microsoft.Extensions.Configuration 包中繼承一個名為 ConfigurationProvider 的類,該類提供了一些樣版代碼(例如重載令牌的實現)。
這個類包含兩個重要的東西:
Data 是包含所有鍵和值的字典,Load 是應用程序開始時使用的方法,正如其名稱所示,它從某處(配置文件或我們的 consul 實例)加載配置并填充字典。
在 ASP.Net Core 中加載 consul 配置
我們第一個想到的方法就是利用 HttpClient 去獲取 consul 中的配置。然后,由于配置在層級式的,像一棵樹,我們需要把它展開,以便放入字典中,是不是很簡單?
首先,實現 Load 方法,但是我們需要一個異步的方法,原始方法會阻塞,所以加入一個異步的 LoadAsync 方法
public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();然后,我們將以遞歸的方式查詢 consul 以獲取配置值。它使用類中定義的一些對象,例如_consulUrls,這是一個數組用來保存 consul 實例們的 url(用于故障轉移),_path 是鍵的前綴(例如App1/Dev)。一旦我們得到 json ,我們迭代每個鍵值對,解碼 Base64 字符串,然后展平所有鍵和JSON對象。
private async Task<IDictionary<string, string>> ExecuteQueryAsync() { ? ?int consulUrlIndex = 0; ? ?while (true){ ? ? ? ?try{ ? ? ? ? ? ?using (var httpClient = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }, true)) ? ? ? ? ? ?using (var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_consulUrls[consulUrlIndex], "?recurse=true"))) ? ? ? ? ? ?using (var response = await httpClient.SendAsync(request)){response.EnsureSuccessStatusCode(); ? ? ? ? ? ? ? ?var tokens = JToken.Parse(await response.Content.ReadAsStringAsync()); ? ? ? ? ? ? ? ?return tokens.Select(k => KeyValuePair.Create(k.Value<string>("Key").Substring(_path.Length + 1),k.Value<string>("Value") != null ? JToken.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(k.Value<string>("Value")))) : null)).Where(v => !string.IsNullOrWhiteSpace(v.Key)).SelectMany(Flatten).ToDictionary(v => ConfigurationPath.Combine(v.Key.Split('/')), v => v.Value, StringComparer.OrdinalIgnoreCase);}} ? ? ? ?catch{consulUrlIndex++; ? ? ? ? ? ?if (consulUrlIndex >= _consulUrls.Count) ? ? ? ? ? ? ? ?throw;}} }使鍵值變平的方法是對樹進行簡單的深度優先搜索。
private static IEnumerable<KeyValuePair<string, string>> Flatten(KeyValuePair<string, JToken> tuple) { ? ?if (!(tuple.Value is JObject value)) ? ? ? ?yield break; ? ?foreach (var property in value){ ? ? ? ?var propertyKey = $"{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;}} }包含構造方法和私有字段的完整的類代碼如下:
public class SimpleConsulConfigurationProvider : ConfigurationProvider{ ? ?private readonly string _path; ? ?private readonly IReadOnlyList<Uri> _consulUrls; ? ?public SimpleConsulConfigurationProvider(IEnumerable<Uri> consulUrls, string path) ? ?{_path = path;_consulUrls = consulUrls.Select(u => new Uri(u, $"v1/kv/{path}")).ToList(); ? ? ? ?if (_consulUrls.Count <= 0){ ? ? ? ? ? ?throw new ArgumentOutOfRangeException(nameof(consulUrls));}} ? ?public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult(); ? ?private async Task LoadAsync() ? ?{Data = await ExecuteQueryAsync();} ? ?private async Task<IDictionary<string, string>> ExecuteQueryAsync(){ ? ? ? ?int consulUrlIndex = 0; ? ? ? ?while (true){ ? ? ? ? ? ?try{ ? ? ? ? ? ? ? ?var requestUri = "?recurse=true"; ? ? ? ? ? ? ? ?using (var httpClient = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }, true)) ? ? ? ? ? ? ? ?using (var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_consulUrls[consulUrlIndex], requestUri))) ? ? ? ? ? ? ? ?using (var response = await httpClient.SendAsync(request)){response.EnsureSuccessStatusCode(); ? ? ? ? ? ? ? ? ? ?var tokens = JToken.Parse(await response.Content.ReadAsStringAsync()); ? ? ? ? ? ? ? ? ? ?return tokens.Select(k => KeyValuePair.Create(k.Value<string>("Key").Substring(_path.Length + 1),k.Value<string>("Value") != null ? JToken.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(k.Value<string>("Value")))) : null)).Where(v => !string.IsNullOrWhiteSpace(v.Key)).SelectMany(Flatten).ToDictionary(v => ConfigurationPath.Combine(v.Key.Split('/')), v => v.Value, StringComparer.OrdinalIgnoreCase);}} ? ? ? ? ? ?catch{consulUrlIndex = consulUrlIndex + 1; ? ? ? ? ? ? ? ?if (consulUrlIndex >= _consulUrls.Count) ? ? ? ? ? ? ? ? ? ?throw;}}} ? ?private static IEnumerable<KeyValuePair<string, string>> Flatten(KeyValuePair<string, JToken> tuple){ ? ? ? ?if (!(tuple.Value is JObject value)) ? ? ? ? ? ?yield break; ? ? ? ?foreach (var property in value){ ? ? ? ? ? ?var propertyKey = $"{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;}}} }動態重新加載配置
我們可以進一步使用 consul 的變更通知。它只是通過添加一個參數(最后一個索引配置的值)來工作的,HTTP 請求會一直阻塞,直到下一次配置變更(或 HttpClient 超時)。
與前面的類相比,我們只需添加一個方法 ListenToConfigurationChanges,以便在后臺監聽 consul 的阻塞 HTTP 。
public class ConsulConfigurationProvider : ConfigurationProvider{ ? ?private const string ConsulIndexHeader = "X-Consul-Index"; ? ?private readonly string _path; ? ?private readonly HttpClient _httpClient; ? ?private readonly IReadOnlyList<Uri> _consulUrls; ? ?private readonly Task _configurationListeningTask; ? ?private int _consulUrlIndex; ? ?private int _failureCount; ? ?private int _consulConfigurationIndex; ? ?public ConsulConfigurationProvider(IEnumerable<Uri> consulUrls, string path) ? ?{_path = path;_consulUrls = consulUrls.Select(u => new Uri(u, $"v1/kv/{path}")).ToList(); ? ? ? ?if (_consulUrls.Count <= 0){ ? ? ? ? ? ?throw new ArgumentOutOfRangeException(nameof(consulUrls));}_httpClient = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }, true);_configurationListeningTask = new Task(ListenToConfigurationChanges);} ? ?public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult(); ? ?private async Task LoadAsync() ? ?{Data = await ExecuteQueryAsync(); ? ? ? ?if (_configurationListeningTask.Status == TaskStatus.Created)_configurationListeningTask.Start();} ? ?private async void ListenToConfigurationChanges() ? ?{ ? ? ? ?while (true){ ? ? ? ? ? ?try{ ? ? ? ? ? ? ? ?if (_failureCount > _consulUrls.Count){_failureCount = 0; ? ? ? ? ? ? ? ? ? ?await Task.Delay(TimeSpan.FromMinutes(1));}Data = await ExecuteQueryAsync(true);OnReload();_failureCount = 0;} ? ? ? ? ? ?catch (TaskCanceledException){_failureCount = 0;} ? ? ? ? ? ?catch{_consulUrlIndex = (_consulUrlIndex + 1) % _consulUrls.Count;_failureCount++;}}} ? ?private async Task<IDictionary<string, string>> ExecuteQueryAsync(bool isBlocking = false){ ? ? ? ?var requestUri = isBlocking ? $"?recurse=true&index={_consulConfigurationIndex}" : "?recurse=true"; ? ? ? ?using (var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_consulUrls[_consulUrlIndex], requestUri))) ? ? ? ?using (var response = await _httpClient.SendAsync(request)){response.EnsureSuccessStatusCode(); ? ? ? ? ? ?if (response.Headers.Contains(ConsulIndexHeader)){ ? ? ? ? ? ? ? ?var indexValue = response.Headers.GetValues(ConsulIndexHeader).FirstOrDefault(); ? ? ? ? ? ? ? ?int.TryParse(indexValue, out _consulConfigurationIndex);} ? ? ? ? ? ?var tokens = JToken.Parse(await response.Content.ReadAsStringAsync()); ? ? ? ? ? ?return tokens.Select(k => KeyValuePair.Create(k.Value<string>("Key").Substring(_path.Length + 1),k.Value<string>("Value") != null ? JToken.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(k.Value<string>("Value")))) : null)).Where(v => !string.IsNullOrWhiteSpace(v.Key)).SelectMany(Flatten).ToDictionary(v => ConfigurationPath.Combine(v.Key.Split('/')), v => v.Value, StringComparer.OrdinalIgnoreCase);}} ? ?private static IEnumerable<KeyValuePair<string, string>> Flatten(KeyValuePair<string, JToken> tuple){ ? ? ? ?if (!(tuple.Value is JObject value)) ? ? ? ? ? ?yield break; ? ? ? ?foreach (var property in value){ ? ? ? ? ? ?var propertyKey = $"{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;}}} }組合在一起
我們現在有了一個 ConfigurationProvider, 讓我們再寫一個 ConfigurationSource 來創建 我們的 provider.
public class ConsulConfigurationSource : IConfigurationSource{ ? ?public IEnumerable<Uri> ConsulUrls { get; } ? ?public string Path { get; } ? ?public ConsulConfigurationSource(IEnumerable<Uri> consulUrls, string path) ? ?{ConsulUrls = consulUrls;Path = path;} ? ?public IConfigurationProvider Build(IConfigurationBuilder builder) ? ?{ ? ? ? ?return new ConsulConfigurationProvider(ConsulUrls, Path);} }以及一些擴展方法 :
public static class ConsulConfigurationExtensions{public static IConfigurationBuilder AddConsul(this IConfigurationBuilder configurationBuilder, IEnumerable<Uri> consulUrls, string consulPath){ ? ? ? ?return configurationBuilder.Add(new ConsulConfigurationSource(consulUrls, consulPath));} ? ?public static IConfigurationBuilder AddConsul(this IConfigurationBuilder configurationBuilder, IEnumerable<string> consulUrls, string consulPath){ ? ? ? ?return configurationBuilder.AddConsul(consulUrls.Select(u => new Uri(u)), consulPath);} }現在可以在 Program.cs 中添加 Consul,使用其他的來源(例如環境變量或命令行參數)來向 consul 提供 url
public static IWebHost BuildWebHost(string[] args) =>WebHost.CreateDefaultBuilder(args).ConfigureAppConfiguration(cb =>{ ? ? ? ? ? ?var configuration = cb.Build();cb.AddConsul(new[] { configuration.GetValue<Uri>("CONSUL_URL") }, configuration.GetValue<string>("CONSUL_PATH"));}).UseStartup<Startup>().Build();現在,可以使用 ASP.Net Core 的標準配置模式了,例如 Options。
public void ConfigureServices(IServiceCollection services){services.AddMvc();services.AddOptions();services.Configure<AppSettingsOptions>(Configuration.GetSection("Settings"));services.Configure<AccountingFeaturesOptions>(Configuration.GetSection("FeatureFlags"));services.Configure<CartFeaturesOptions>(Configuration.GetSection("FeatureFlags"));services.Configure<CatalogFeaturesOptions>(Configuration.GetSection("FeatureFlags")); }要在我們的代碼中使用它們,請注意如何使用 options ,對于可以動態重新加載的 options,使用 IOptions?將獲得初始值。反之,ASP.Net Core 需要使用 IOptionsSnapshot。
這種情況對于功能切換非常棒,因為您可以通過更改 Consul 中的值來啟用或禁用新功能,并且在不重新發布的情況下,用戶就可以使用這些新功能。同樣的,如果某個功能出現 bug,你可以禁用它,而無需回滾或熱修復。
? ?public IActionResult AddProduct([FromServices]IOptionsSnapshot<CartFeaturesOptions> options, [FromBody] Product product) ? ?{ ?
? ? ?var cart = _cartService.GetCart(this.User);cart.Add(product); ? ? ?
? ? ??if (options.Value.UseCartAdvisorFeature){ViewBag.CartAdvice = _cartAdvisor.GiveAdvice(cart);} ? ? ? ?return View(cart);} }
尾聲
這幾行代碼允許我們在 ASP.Net Core 應用程序中添加對 consul 配置的支持。事實上,任何應用程序(甚至使用 Microsoft.Extensions.Configuration 包的經典 .Net 應用程序)都可以從中受益。在 DevOps 環境中這將非常酷,你可以將所有配置集中在一個位置,并使用熱重新加載功能進行實時切換。
原文鏈接:https://www.cnblogs.com/Rwing/p/consul-configuration-aspnet-core.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com 
總結
以上是生活随笔為你收集整理的如何在 ASP.Net Core 中使用 Consul 来存储配置的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 【招聘(重庆)】新空间(重庆)科技有限公
 - 下一篇: 关于.NET Core是否应该支持WCF