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. ajax json 异步请求

    function ajaxTest(){ if (true) { $.ajaxSettings.async = false; var dataJson; $.getJSON("/univer ...

  2. selection createTextRange setSelectionRange

    http://www.cnblogs.com/rainman/archive/2011/02/27/1966482.html http://www.zhangxinxu.com/wordpress/2 ...

  3. Wi-Fi基带芯片和Wi-Fi无线网卡设计方案

    转:http://wenku.baidu.com/link?url=Q0ImC 0IIG7qrbB8DpGrrU3aOYvxNYCyHsxukspH8XMCDYMjYMPSJq_TCISC5amsNY ...

  4. mysql错误总结-ERROR 1067 (42000): Invalid default value for TIMESTAMP

    1. ERROR 1067 (42000): Invalid default value for 'FAILD_TIME'   (对TIMESTAMP  类型的子段如果不设置缺省值或没有标志not n ...

  5. 一言(ヒトコト)Hitokoto API

    『想要成为无论多么悲伤的时候,也能够漂亮微笑的人吧.』 Hitokoto API 更新:2014.02.22 问题/反馈:api # hitokoto.us 数据获取:[ 数据获取 ] 调用举例:[  ...

  6. jar 冲突解决方案

    val urlOfClass = classOf[Nothing].getClassLoader.getResource("org/slf4j/spi/LocationAwareLogger ...

  7. spark HelloWorld程序(scala版)

    使用本地模式,不需要安装spark,引入相关JAR包即可: <dependency> <groupId>org.apache.spark</groupId> < ...

  8. hive学习7(条件函数case)

    case函数 语法: CASE WHEN a THEN b [WHEN c THEN d]* [ELSE e] END 说明:如果a为TRUE,则返回b:如果c为TRUE,则返回d:否则返回e 实例 ...

  9. mysql基础(3)-高级查询

    聚合函数 count 返回查询结果的条数 max 返回查询结果的最大值 min 返回查询结果的最小值 sum 返回查询结果的和 avg 返回查询结果的平均值   统计分数大于等于90的人数: mysq ...

  10. 关于es集群转换为单点后,主分片丢失的问题(健康检测状态为red)

    正在找解决方案 前后情况是, 之前是es双节点,之后更改为单节点,data中的数据都是双节点的,也许导致了单节点的状态不正常,删除了data目录下内容后,重启es,好了,这是测试环境,所以这么干的