如何在 ASP.Net Core 中使用 Consul 来存储配置

栏目: ASP.NET · 发布时间: 7年前

内容简介:原文:作者: Nathanael[译者注:因急于分享给大家,所以本文翻译的很仓促,有些不准确的地方还望谅解]

原文: 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 OK
Content-Type: application/json
X-Consul-Index: 1071
X-Consul-Knownleader: true
X-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 OK

Content-Type: application/json

X-Consul-Index: 1071

X-Consul-Knownleader: true

X-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 的类,该类提供了一些样版代码(例如重载令牌的实现)。

这个类包含两个重要的东西:

/* 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。

Linux公社的RSS地址https://www.linuxidc.com/rssFeed.aspx

本文永久更新链接地址: https://www.linuxidc.com/Linux/2018-11/155390.htm


以上所述就是小编给大家介绍的《如何在 ASP.Net Core 中使用 Consul 来存储配置》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Kotlin实战

Kotlin实战

【美】Dmitry Jemerov(德米特里·詹莫瑞福)、【美】 Svetlana Isakova(斯维特拉娜·伊凡诺沃) / 覃宇、罗丽、李思阳、蒋扬海 / 电子工业出版社 / 2017-8 / 89.00

《Kotlin 实战》将从语言的基本特性开始,逐渐覆盖其更多的高级特性,尤其注重讲解如何将 Koltin 集成到已有 Java 工程实践及其背后的原理。本书分为两个部分。第一部分讲解如何开始使用 Kotlin 现有的库和API,包括基本语法、扩展函数和扩展属性、数据类和伴生对象、lambda 表达式,以及数据类型系统(着重讲解了可空性和集合的概念)。第二部分教你如何使用 Kotlin 构建自己的 ......一起来看看 《Kotlin实战》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具