前言:
配置中心对于日益增长的服务集群来说是十分必要的,能有效方便我们对配置的管理,我们常见的配置中心有Apollo, Nacos等,本篇文章基于Apllo架构设计简单实现功能。
参考文档:Apollo配置中心设计
原理:
1.IOptionMontior对appsetting.json热更新的支持,底层还是FileWatch基于onchange实现;
2.Newtonsoft.Json的强大JObject对象,方便我们解析json就行个性化处理;
(强推B站Up主 十月的寒流 关于Newtonsoft的各种使用)
3. 基于File实现的读写文件
4.基于Consul的服务注册以及发现(这里大家可以具体实现自己想要的服务中心)
代码实现:
想基于nuget包给后续需要的服务使用,所以会对其进行一些精简。
基础服务
1.通过MinimalApi方式注入每个服务器,暴露一个同意接口,后续方便我们基于service address+service port + "固定url" 方式调用。
下面对于configuration等update比较粗糙,可以自定义实现自己的细节。
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MinimalOnlineChat.extension;
public static class UseConfigHotReloadExtension
{
/// <summary>
/// 将服务注册到注册中心,Consul,然后里面写一个服务去实现管理和分配每个service,保留接口给portal,用户可以选择服务列表,然后我们根据服务列表的具体ip+port +param去调用各个服务url,然后让其hot-reload.
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
public static IApplicationBuilder UseConfigHotReload(this IApplicationBuilder app)
{
var application = app as WebApplication;
application?.MapPost("/api/updateAppConfig", (string jsonParam, string? environmentParam,[FromServices]IConfiguration configuration) =>
{
try
{
string environment = string.Empty;
if (!string.IsNullOrEmpty(environmentParam))
{
environment = "." + environmentParam;
}
JObject destJObject = JObject.Parse(jsonParam);
var appSettingsPath = Path.Combine(Directory.GetCurrentDirectory(), $"appsettings{environment}.json");
var json = File.ReadAllText(appSettingsPath);
//当前appsetting.json里面的数据。
var sourceJObject = JObject.Parse(json);
MergeJObjects(sourceJObject, destJObject);
string output = JsonConvert.SerializeObject(sourceJObject, Formatting.Indented);
File.WriteAllText(appSettingsPath, output);
//触发配置重新加载
(configuration as IConfigurationRoot)?.Reload();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
throw;
}
});
return app;
}
private static void MergeJObjects(JObject sourceJObject, JObject destJObject)
{
foreach (var destProperty in destJObject.Properties())
{
if (sourceJObject.TryGetValue(destProperty.Name, out JToken sourceToken))
{
if (destProperty.Value is JObject nestedDestObject && sourceToken is JObject nestedSourceObject)
{
//如果属性值是 JObject, 则递归处理
MergeJObjects(nestedSourceObject, nestedDestObject);
}
else
{
// 更新 destJObject 中的值
sourceJObject[destProperty.Name] = destProperty.Value;
}
}
else
{
// 如果 sourceJObject 中不存在同名属性, 则在 destJObject 中添加新的属性
sourceJObject.Add(destProperty.Name, destProperty.Value);
}
}
}
}
2.Program.cs 注入服务:
//感兴趣可以看看里面的实现细节,已经帮我们注入IOptionsMonitor了
builder.Services.AddOptions();
........
var app = builder.Build();
.......
//配置在请求管道里面,是我们注册的MinimalAPi生效
app.UseConfigHotReload();
以上就可以简单简单实现我们对指定环境的appsetting.json进行更新了,如果基于本地单服务实现的话这里就足够啦,下面将介绍如何引入Consul做我们的配置中心.
Consul微服务:
1.先本地安装Consul,如果有需要集群部署的话,可以借助Docker部署,大家可以去参考其他大佬的实现;
2.1 定义Consul配置映射实体:
public class ConsulOption
{
public string? IP { get; set; }
public string? Port { get; set; }
/// <summary>
/// 微服务名称
/// </summary>
public string? ServiceName { get; set; }
/// <summary>
/// consul对应的请求地址
/// </summary>
public string? ConsulHost { get; set; }
/// <summary>
/// consul数据中心,默认dc1
/// </summary>
public string? ConsulDataCenter { get; set; }
}
2.2 appsetting.json配置我们的Consul配置:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConsulOption": {
"IP": "localhost",
"Port": "5029",
"ServiceName": "HotReloadService",
"ConsulHost": "http://localhost:8500",
"ConsulDataCenter": "dc1"
},
"AllowedHosts": "*",
"Test": "dddddd"
}
3.连接Consul,并将当前server注册到Consul上
using Consul;
using Microsoft.Extensions.Options;
using MinimalOnlineChat.entity;
namespace MinimalOnlineChat.extension;
public static class ServiceExtension
{
public static WebApplication UseConsulRegistry(this WebApplication webApplication, IHostApplicationLifetime builder)
{
var optionsMonitor = webApplication.Services.GetService<IOptionsMonitor<ConsulOption>>();
var consulOption = optionsMonitor!.CurrentValue;
//获取心跳监测的Ip和port
var consulOptionIp = webApplication.Configuration["ip"] ?? consulOption.IP;
var port = webApplication.Configuration["Port"] ?? consulOption.Port;
//生成serviceId
var id = Guid.NewGuid().ToString();
var client = webApplication.Services.GetService<IConsulClient>() ?? new ConsulClient(c=>
{
c.Address = new Uri(consulOption.ConsulHost!);
c.Datacenter = consulOption.ConsulDataCenter;
});
//把服务注册到consul上
client.Agent.ServiceRegister(new AgentServiceRegistration()
{
ID = id,
Name = consulOption.ServiceName,
Address = consulOptionIp,
Port = Convert.ToInt32(port),
Check = new AgentServiceCheck
{
Interval = TimeSpan.FromSeconds(12),
HTTP = $"http://{consulOptionIp}:{port}/api/health",
Timeout = TimeSpan.FromSeconds(5),
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(20)
}
});
builder.ApplicationStopped.Register(async () =>
{
Console.WriteLine("服务注销");
await client.Agent.ServiceDeregister(id);
});
return webApplication;
}
}
4.Program.cs Minimal Api暴露一个接口与Consul服务进行健康检查.
app.MapGet("/api/health", () =>
{
Console.WriteLine("Ok");
return new
{
Message = "Ok"
};
});
5.依赖注入服务到容器中,配置请求管道
builder.Services.Configure<ConsulOption>(builder.Configuration.GetSection("ConsulOption"));
//注入IConsulClient,方便我们后续使用(注册,接触,获取service 列表)
//下面的Uri应该从配置文件读,相信大家都会啦
builder.Services.AddSingleton<IConsulClient>(new ConsulClient(x =>
{
x.Address = new Uri("http://localhost:8500");
x.Datacenter = "dc1";
}));
.................
var app = builder.Build();
.................
app.UseConsulRegistry(app.Lifetime);
.................
6.获取服务列表,拼接请求字符串:
//1.依赖注入IConsulClient ,我们使用其来获取服务列表
private readonly IConfiguration _configuration;
private static int _version = 0;
private IConsulClient _client;
public HotReloadController(IConfiguration configuration,IConsulClient client)
{
_configuration = configuration;
_client = client;
}
............................
[HttpGet]
public async Task QueryServiceNode()
{
var queryResult = await _client.Agent.Services();
//这里实现你自己具体想要掉用哪个服务,通常这个接口暴露给用户UI去调用的选择更新哪个服务的config.
}
............................
以上就是全部代码: 上面未实现的有版本控制管理,移除本地appsetting(因为我们是基于已传值foreach,所以我们需要引入版本管理比较删除非必要的KV)
该上面的小技巧已经发到Nuget上了,大家需要的话可以直接引用:
dotnet add package ConfigHotReload --version 1.0.0
后面会完善各种细节更新到github上,如果大家感兴趣或者有更好的idea 欢迎star留言。
Ending......