[翻譯] 如何在 ASP.Net Core 中使用 Consul 來儲存配置

來源:互聯網
上載者:User
[翻譯] 如何在 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 值。

/|-- App1| |-- Dev| | |-- ConnectionStrings| | \-- Settings| |-- Staging| | |-- ConnectionStrings| | \-- Settings| \-- Prod|   |-- ConnectionStrings|   \-- Settings\-- App2  |-- Dev  | |-- ConnectionStrings  | \-- Settings  |-- Staging  | |-- ConnectionStrings  | \-- Settings  \-- Prod    |-- ConnectionStrings    \-- Settings

它提供了 REST API 以方便查詢,key 包含在查詢路徑中。例如,擷取 App1 在 Dev 環境中的配置的查詢如下所示:GET http://:8500/v1/kv/App1/Dev/Settings
響應如下:

HTTP/1.1 200 OKContent-Type: application/jsonX-Consul-Index: 1071X-Consul-Knownleader: trueX-Consul-Lastcontact: 0[    {        "LockIndex": 0,        "Key": "App1/Dev/Settings",        "Flags": 0,        "Value": "ewogIkludCI6NDIsCiAiT2JqZWN0IjogewogICJTdHJpbmciOiAidG90byIsCiAgIkJsYSI6IG51bGwsCiAgIk9iamVjdCI6IHsKICAgIkRhdGUiOiAiMjAxOC0wMi0yM1QxNjoyMTowMFoiCiAgfQogfQp9Cgo=",        "CreateIndex": 501,        "ModifyIndex": 1071    }]

也可以以遞迴方式查詢任何節點,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 值都使用了 Base 64 編碼,以避免 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 的類,該類提供了一些樣版代碼(例如重載令牌的實現)。
這個類包含兩個重要的東西:

/* Excerpt from the implementation */public abstract class ConfigurationProvider : IConfigurationProvider{    protected IDictionary<string, string> Data { get; set; }    public virtual void Load()    {    }}

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 class CartController : Controller{    [HttpPost]    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 環境中這將非常酷,你可以將所有配置集中在一個位置,並使用熱重新載入功能進行即時切換。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.