配置目录与热更新
这一页讲宿主怎样装载一个挂载的配置目录:环境变量 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 找文件的规则:
- 只取目录顶层的
*.json,不进子目录;跳过以.开头的条目(ConfigMap 挂载工具放置的..data等)。 appsettings.json(常量ConfigurationDirectory.BaseFileName)最先装载,其余文件按文件名的序数顺序随后装载,后装载的覆盖先装载的同名键。- 目录不存在或环境变量为空时什么都不加,应用照常启动。
文件列表在启动时确定:运行中往目录里新增文件要重启才装载;已装载文件的内容变化会触发 IConfiguration 的重载令牌,IOptionsMonitor<T> 随之更新。
配置源的优先级
这些文件紧接构建器里最后一个 appsettings 文件源(ConfigurationDirectory.IsApplicationSettings 认出的 appsettings.json 或 appsettings.{环境}.json)插入。WebApplication.CreateBuilder 与 Host.CreateApplicationBuilder 的配置源因此从低到高是:
| 顺序 | 配置源 |
|---|---|
| 1 | appsettings.json |
| 2 | appsettings.{环境}.json |
| 3 | XIHAN_CONFIG_PATH 目录里的 JSON 文件 |
| 4 | 用户机密(仅开发环境) |
| 5 | 环境变量 |
| 6 | 命令行参数 |
| 7 | 应用自己添加的配置源(远程配置、密钥等),按添加顺序 |
后者覆盖前者:挂载文件覆盖 appsettings;同一个键同时出现在挂载文件与环境变量里时,环境变量的值生效。xihan pack 把应用清单的 shutdown.graceSeconds 写成 Dockerfile 与 Deployment 里的环境变量 XiHan__Kernel__StopTimeoutSeconds,这个值因此不会被 ConfigMap 里的同名键改掉。构建器里没有 appsettings 文件源(应用关掉了默认配置)时,目录里的文件追加在末尾。
最小示例
我们在本机模拟一次挂载:建一个目录放配置文件,用环境变量指向它,运行应用后改文件看结果。BannerOptions 沿用配置模型与选项的写法。
// 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"):
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 runcurl localhost:5000/config 返回:
{"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_PATH | mountPath(缺省 /etc/xihan) | 宿主把这个目录里的 JSON 文件作为可重载配置源 |
DOTNET_USE_POLLING_FILE_WATCHER | 1 | ConfigMap 以切换符号链接的方式更新,文件监视必须轮询 |
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 下不可用。在项目文件里打开配置绑定源生成器,绑定代码在编译期生成,重载照常:
<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 用同一套规则装载目录,见命令行宿主。
相关包与下一步
- Kernel:
ConfigurationDirectory与KernelOptions - Hosting.AspNetCore、Hosting.CommandLine:装载配置目录的两个宿主
- 应用清单与 xihan pack:
config.files与config.mountPath - 下一页:资源绑定
