Net Core 中使用 Consul 来存储配置

https://www.cnblogs.com/Rwing/p/consul-configuration-aspnet-core.html

原文: 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("Key").Substring(_path.Length + 1),

k.Value("Value") != null ? JToken.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(k.Value("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 _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 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 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("CONSUL_URL") }, configuration.GetValue("CONSUL_PATH"));

})

.UseStartup()

.Build();

现在,可以使用 ASP.Net Core 的标准配置模式了,例如 Options。

public void ConfigureServices(IServiceCollection services)

{

services.AddMvc();

services.AddOptions();

services.Configure(Configuration.GetSection("Settings"));

services.Configure(Configuration.GetSection("FeatureFlags"));

services.Configure(Configuration.GetSection("FeatureFlags"));

services.Configure(Configuration.GetSection("FeatureFlags"));

}

要在我们的代码中使用它们,请注意如何使用 options ,对于可以动态重新加载的 options,使用 IOptions 将获得初始值。反之,ASP.Net Core 需要使用 IOptionsSnapshot。

这种情况对于功能切换非常棒,因为您可以通过更改 Consul 中的值来启用或禁用新功能,并且在不重新发布的情况下,用户就可以使用这些新功能。同样的,如果某个功能出现 bug,你可以禁用它,而无需回滚或热修复。

public class CartController : Controller

{

[HttpPost]

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

Net Core 中使用 Consul 来存储配置的更多相关文章

  1. [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置

    [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置 原文: USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET COR ...

  2. Consul在.Net Core中初体验

    Consul在.Net Core中初体验 简介 在阅读本文前我想您应该对微服务架构有一个基本的或者模糊的了解 Consul是一个服务管理软件,它其实有很多组件,包括服务发现配置共享键值对存储等 本文主 ...

  3. Api网关Kong集成Consul做服务发现及在Asp.Net Core中的使用

    写在前面   Api网关我们之前是用 .netcore写的 Ocelot的,使用后并没有完全达到我们的预期,花了些时间了解后觉得kong可能是个更合适的选择. 简单说下kong对比ocelot打动我的 ...

  4. NET Core + Ocelot + IdentityServer4 + Consul

    .NET Core + Ocelot + IdentityServer4 + Consul 基础架构实现 先决条件 关于 Ocelot 针对使用 .NET 开发微服务架构或者面向服务架构提供一个统一访 ...

  5. Spring Cloud config中,使用数据库存储配置信息

    主要内容 在springcloud config中,使用数据库存储配置信息. 系统默认采用git的方式,此处我们介绍使用jdbc的方式存储配置信息 准备数据库 数据库我们使用mysql. 新建库 p- ...

  6. 【转】.NET Core + Ocelot + IdentityServer4 + Consul 基础架构实现

    作者:Zhang_Xiang 原文地址:.NET Core + Ocelot + IdentityServer4 + Consul 基础架构实现 先决条件 关于 Ocelot 针对使用 .NET 开发 ...

  7. .net core集成使用consul

    快速启动一个consul集群可以参考:使用docker快速部署一个consul集群 .net core集成使用consul是通过consul提供出来api接口来实现的,可以分成两个部分来说明:配置集成 ...

  8. .NET Core中的认证管理解析

    .NET Core中的认证管理解析 0x00 问题来源 在新建.NET Core的Web项目时选择“使用个人用户账户”就可以创建一个带有用户和权限管理的项目,已经准备好了用户注册.登录等很多页面,也可 ...

  9. ASP.NET Core 中的那些认证中间件及一些重要知识点

    前言 在读这篇文章之间,建议先看一下我的 ASP.NET Core 之 Identity 入门系列(一,二,三)奠定一下基础. 有关于 Authentication 的知识太广,所以本篇介绍几个在 A ...

随机推荐

  1. iOS objc_setAssociatedObject 关联对象的学习

    今天看了FDTemplateLayoutCell的源码,类别里面相当频繁使用了关联对象,做笔记!!!学套路 主要函数: void objc_setAssociatedObject(id object, ...

  2. OC自动释放池autoreleasepool介绍

    自动释放池的机制是:它使得应用在创建新对象时,系统能够有效地管理应用所使用的内存. @autoreleasepool { statements } 在创建新对象时,并且系统未启动ARC特性,那么在使用 ...

  3. 验证——正则<37>

    1,郵箱合法性驗證 /* * 郵箱合法性驗證 * @method matchTel * @papram{string} str,電子郵箱 * @return{boolean} * */ functio ...

  4. 输入框去除默认的文字,jquery方法

    需求:所有的输入框获取焦点时,去掉默认的提示文字,失去焦点时如果输入框为空,恢复默认的提示文字. 解决方案:jquery方法,以下有三种,按照利弊,我建议最后一种. 先看html代码: <inp ...

  5. 分布式任务调度平台XXL-Job集群版搭建

    如果集群: 保存后: 启动两个 job 第二个job的配置: # web port server.port=8082 # log config logging.config=classpath:log ...

  6. 算法总结之 在单链表和双链表中删除倒数第k个节点

    分别实现两个函数,一个可以删除单链表中倒数第k个节点,另一个可以删除双链表中倒数第k个节点 思路: 如果链表为空,或者k<1 参数无效 除此之外 让链表从头开始走到尾,每移动一步,就让k的值减1 ...

  7. [转载]Spring配置文件详解一:

    原文地址:与base-package="com.xx">Spring配置文件详解一:<context:annotation-config/>与<contex ...

  8. Spring初学之FactoryBean配置Bean

    实体bean: Car.java: package spring.beans.factorybean; public class Car { private String name; private ...

  9. 在CentOS6.4中安装配置LAMP环境的详细步骤 - Leroy-LIZH

    本文详细介绍了CentOS6.4系统中安装LAMP服务并对其进行配置的过程,即安装Apache+PHP+Mysql,参照了网上大神的设置,其他Linux发行系统可以参考~ 在本文中部分命令操作需要ro ...

  10. XSS 跨站攻击

    未整理完 介绍 XSS (Cross site Scripting),跨站脚本攻击,为了区分层叠样式表css,所以叫了XSS. 对于跨站脚本攻击,黑客界共识是:跨站脚本攻击是新型的"缓冲区溢 ...