XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

配置目录与热更新 ​

这一页讲宿主怎样装载一个挂载的配置目录:环境变量 XIHAN_CONFIG_PATH 指向哪里、目录里的文件按什么顺序装载、它们与 appsettings 和环境变量谁覆盖谁、文件改了之后哪些值会跟着变,以及容器里为什么要开轮询式文件监视。读完你能在本机模拟 Kubernetes 的 ConfigMap 挂载并看到热更新生效。前置知识是 配置模型与选项。

为什么需要 ​

镜像里的 appsettings.json 随镜像一起发布,改一个值就要重新构建镜像;环境变量能在部署时覆盖,但改了必须重启容器。Kubernetes 的做法是把配置做成 ConfigMap 挂成目录,kubelet 会在 ConfigMap 变化后更新目录里的文件。应用要利用这一点,得知道去哪个目录找文件、按什么顺序装载、怎样监听变化。

框架把这件事收进宿主:Hosting.AspNetCore 与 Hosting.CommandLine 的 AddXiHan 在装配内核之前读环境变量 XIHAN_CONFIG_PATH,把那个目录里的 JSON 文件作为可重载的配置源插进 appsettings.{环境}.json 之后。应用代码不用管目录在哪,只要通过 IOptionsMonitor<T> 读值,文件变化就会反映出来。xihan pack 生成的部署产物会一并设置这个环境变量与挂载。

配置目录的规则 ​

项值
环境变量XIHAN_CONFIG_PATH(常量 ConfigurationDirectory.Variable)
缺省挂载点/etc/xihan(xihan pack 应用清单里 config.mountPath 的缺省值)
查找规则内核的 ConfigurationDirectory.Resolve
装载方式每个文件一个 JSON 配置源,缺文件不报错,reloadOnChange 打开

ConfigurationDirectory.Resolve 找文件的规则:

  1. 只取目录顶层的 *.json,不进子目录;跳过以 . 开头的条目(ConfigMap 挂载工具放置的 ..data 等)。
  2. appsettings.json(常量 ConfigurationDirectory.BaseFileName)最先装载,其余文件按文件名的序数顺序随后装载,后装载的覆盖先装载的同名键。
  3. 目录不存在或环境变量为空时什么都不加,应用照常启动。

文件列表在启动时确定:运行中往目录里新增文件要重启才装载;已装载文件的内容变化会触发 IConfiguration 的重载令牌,IOptionsMonitor<T> 随之更新。

配置源的优先级 ​

这些文件紧接构建器里最后一个 appsettings 文件源(ConfigurationDirectory.IsApplicationSettings 认出的 appsettings.json 或 appsettings.{环境}.json)插入。WebApplication.CreateBuilder 与 Host.CreateApplicationBuilder 的配置源因此从低到高是:

顺序配置源
1appsettings.json
2appsettings.{环境}.json
3XIHAN_CONFIG_PATH 目录里的 JSON 文件
4用户机密(仅开发环境)
5环境变量
6命令行参数
7应用自己添加的配置源(远程配置、密钥等),按添加顺序

后者覆盖前者:挂载文件覆盖 appsettings;同一个键同时出现在挂载文件与环境变量里时,环境变量的值生效。xihan pack 把应用清单的 shutdown.graceSeconds 写成 Dockerfile 与 Deployment 里的环境变量 XiHan__Kernel__StopTimeoutSeconds,这个值因此不会被 ConfigMap 里的同名键改掉。构建器里没有 appsettings 文件源(应用关掉了默认配置)时,目录里的文件追加在末尾。

最小示例 ​

我们在本机模拟一次挂载:建一个目录放配置文件,用环境变量指向它,运行应用后改文件看结果。BannerOptions 沿用配置模型与选项的写法。

csharp
// Program.cs
using Microsoft.Extensions.Options;
using Shop;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Profiles.Standard;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<BannerOptions>(builder.Configuration.GetSection(BannerOptions.Section));

// AddXiHan 先装载 XIHAN_CONFIG_PATH 目录里的 JSON 文件并监听变化,再装配内核
builder.AddXiHan(xihan => xihan.UseStandardProfile());

var app = builder.Build();
app.UseXiHan();
app.MapXiHanHealth();

// 读 CurrentValue 而不是在启动时缓存,挂载文件更新后返回新值
app.MapGet("/config", (IOptionsMonitor<BannerOptions> banner, IConfiguration configuration) =>
    new ConfigView(
        banner.CurrentValue.Banner,
        configuration["XiHan:Kernel:StopTimeoutSeconds"],
        Environment.GetEnvironmentVariable(ConfigurationDirectory.Variable)));

app.Run();

public sealed record ConfigView(string Banner, string? StopTimeout, string? ConfigPath);

准备目录与文件,再带着环境变量启动(PowerShell 里用 $env:XIHAN_CONFIG_PATH = "$PWD\mounted"):

bash
mkdir -p ./mounted
cat > ./mounted/appsettings.json <<'EOF'
{ "Sample": { "Banner": "Hello from mounted file" }, "XiHan": { "Kernel": { "StopTimeoutSeconds": 20 } } }
EOF
export XIHAN_CONFIG_PATH=$PWD/mounted
dotnet run

curl localhost:5000/config 返回:

json
{"banner":"Hello from mounted file","stopTimeout":"20","configPath":"/path/to/mounted"}

不停应用,把文件里的 Banner 改成 Changed,保存后再请求一次,banner 已经是 Changed,而 stopTimeout 即使改了也只是配置树里的值变了:内核在装配时已经读过 KernelOptions,停止超时仍是 20 秒。要以 Native AOT 发布这个示例,ConfigView 需要登记到 JsonSerializerContext,见 JSON 序列化与 Native AOT。

容器与 Kubernetes 里的热更新 ​

应用清单 xihan.app.json 的 config.files 列出要做成 ConfigMap 的文件,xihan pack 据此生成 ConfigMap(<name>-config),只读挂到 mountPath,并在 Deployment 里设两个环境变量:

环境变量值作用
XIHAN_CONFIG_PATHmountPath(缺省 /etc/xihan)宿主把这个目录里的 JSON 文件作为可重载配置源
DOTNET_USE_POLLING_FILE_WATCHER1ConfigMap 以切换符号链接的方式更新,文件监视必须轮询

ConfigMap 更新要用轮询监视

kubelet 更新 ConfigMap 时切换的是符号链接,默认的文件系统通知收不到变化,IOptionsMonitor<T> 永远不会更新。容器里要设 DOTNET_USE_POLLING_FILE_WATCHER=1。自己写 Deployment 而不用 xihan pack 时,别漏掉这一项。

kubelet 同步 ConfigMap 有延迟,kubectl edit configmap 之后通常要一分钟左右端点才返回新值。清单字段与产物的完整说明见应用清单与 xihan pack与 Kubernetes。

热更新的边界 ​

热更新只作用于读 IConfiguration 或 IOptionsMonitor<T> 的代码:

  • 框架包的选项在装配时已经读完,改挂载文件里的 XiHan:* 键不会改变正在运行的特性,要滚动重启才生效。
  • IOptions<T> 只绑定一次;需要跟随变化就注入 IOptionsMonitor<T>,读 CurrentValue 或用 OnChange 注册回调。
  • 运行中新增的文件要重启才装载;已有文件改内容才会触发重载。
  • 环境变量与命令行参数的优先级高于挂载文件,被它们覆盖的键改了文件也不会变。

Native AOT 下的绑定与重载 ​

Configure<T>(IConfiguration) 默认按反射绑定,AOT 下不可用。在项目文件里打开配置绑定源生成器,绑定代码在编译期生成,重载照常:

xml
<PropertyGroup>
  <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>

常见问题 ​

改了文件但 IOptionsMonitor 没变 ​

按顺序排查:注入的是 IOptionsMonitor<T> 而不是 IOptions<T>;文件在目录顶层且扩展名是 .json;同一个键没有被环境变量或命令行参数覆盖;容器里设置了 DOTNET_USE_POLLING_FILE_WATCHER=1;文件不是启动后才新增的。

能把 XiHan:* 的键放进挂载文件吗 ​

能,它们在启动时与 appsettings 一样被读取,适合按环境调整 XiHan:Kernel:StopTimeoutSeconds、XiHan:Caching:Redis:KeyPrefix 这类值。但改动要重启才对框架生效。

目录里能放子目录或 YAML 吗 ​

不能。只认目录顶层的 *.json,子目录与其他扩展名都跳过。要分多份文件就都放在顶层,按文件名排序决定覆盖顺序。

开发机上怎样关掉 ​

不设置 XIHAN_CONFIG_PATH 即可,目录不存在或变量为空时什么都不加。

命令行宿主也支持吗 ​

支持。Hosting.CommandLine 的 AddXiHan 用同一套规则装载目录,见命令行宿主。

相关包与下一步 ​

Released under The MIT License