如何使用 config-rs 分层加载?

解读

在国内互联网后端面试中,**“分层加载”**通常指:

  1. 代码里硬编码一套默认值
  2. 通过 config-rs 依次读取环境变量本地文件(如 config/local.yaml)、远程配置中心(Nacos、Apollo、Etcd 等);
  3. 后加载的层覆盖先加载的层,最终得到一份合并后Settings 结构体;
  4. 整个过程在编译期可验证字段类型,在启动期报错而非运行期抛异常。

面试官想确认:

  • 你是否理解 config-rsSource 链式 API
  • 能否把“环境区分(dev/test/prod)”与“敏感信息(ak/sk、db 密码)”拆到不同层;
  • 是否知道用 serde 扁平化 + Option 来处理缺失字段;
  • 是否能在不重启进程的前提下支持热更新(加分项)。

知识点

  1. LayeredSourceconfig::Config::builder() 多次 .add_source(),顺序即优先级。
  2. FileFormatFile::new("config/default", FileFormat::Yaml) 支持 yaml、toml、json、ini。
  3. EnvironmentEnvironment::with_prefix("APP").separator("__")APP_SERVER__PORT 映射为 settings.server.port
  4. Deserializationconfig.try_deserialize::<Settings>() 一次性转成强类型结构体。
  5. 合并策略:后加的 Source 会递归覆盖叶子节点,数组默认替换而非追加。
  6. 国内常见坑
    • Windows 开发机路径大小写不敏感,线上 Linux 区分,导致 Config.toml 找不到;
    • 配置中心返回 JSON 带 BOM,config-rs 解析失败,需先 strip_prefix("\u{feff}")
    • 阿里云的 ACM(Nacos) 需自己实现 Source trait,把 get_string 改成异步拉取并缓存。

答案

下面给出一份可直接落地的最小可运行示例,覆盖“代码默认值 → 文件 → 环境变量”三层,符合国内大多数 Rust 后端项目规范。

1. 定义 Settings 结构体

use serde::{Deserialize, Serialize};
use std::net::IpAddr;

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Database {
    pub url: String,
    pub max_conn: u32,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Server {
    pub host: IpAddr,
    pub port: u16,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Settings {
    pub server: Server,
    pub database: Database,
    pub log_level: String,
}

impl Default for Settings {
    fn default() -> Self {
        Settings {
            server: Server {
                host: [127, 0, 0, 1].into(),
                port: 8080,
            },
            database: Database {
                url: "postgres://postgres:postgres@localhost:5432/mydb".into(),
                max_conn: 10,
            },
            log_level: "info".into(),
        }
    }
}

2. 分层加载函数

use config::{Config, ConfigError, Environment, File};

impl Settings {
    pub fn new() -> Result<Self, ConfigError> {
        let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into());

        let s = Config::builder()
            // 第 1 层:代码内默认值
            .add_source(Config::try_from(&Settings::default())?)
            // 第 2 层:config/default.yaml
            .add_source(File::new("config/default", FileFormat::Yaml))
            // 第 3 层:config/development.yaml 或 production.yaml
            .add_source(File::new(&format!("config/{}", run_mode), FileFormat::Yaml).required(false))
            // 第 4 层:本地敏感配置,gitignore 掉
            .add_source(File::new("config/local", FileFormat::Yaml).required(false))
            // 第 5 层:环境变量,前缀 APP_
            .add_source(
                Environment::with_prefix("APP")
                    .separator("__")
                    .try_parsing(true),
            )
            .build()?;

        s.try_deserialize()
    }
}

3. main.rs 使用

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let settings = Settings::new()?;
    println!("{:#?}", settings);
    Ok(())
}

4. 目录结构

.
├── Cargo.toml
├── config
│   ├── default.yaml
│   ├── development.yaml
│   ├── production.yaml
│   └── local.yaml   # 加入 .gitignore
└── src
    ├── main.rs
    └── settings.rs

5. 运行演示

# 默认加载 development.yaml
cargo run

# 用环境变量覆盖端口
APP_SERVER__PORT=9090 RUN_MODE=production cargo run

关键点回顾

  • 顺序即优先级:环境变量 > local.yaml > production.yaml > default.yaml > 代码默认值。
  • required(false) 让文件缺失时不报错,适合 local 层。
  • Option 字段可省略,但建议顶层结构体保持必填,用 #[serde(default)] 给子字段默认值,防止启动失败。
  • 线上容器化时,把 敏感字段 通过 Kubernetes Secret 注入为环境变量,实现“配置与镜像分离”,满足等保要求。

拓展思考

  1. 热更新
    config-rs 本身无 watch 能力,可结合 notify crate 监听文件变更,收到事件后重新 Settings::new() 并发送给业务层;注意要把新的 Arc<Settings> 原子替换,避免并发读写。

  2. 远程配置中心
    在阿里/腾讯机房内网,可基于 nacos-sdk-rust 实现自定义 Source,异步拉取配置后 tokio::sync::RwLock 缓存;上线前在 灰度机器 验证 config-rs 合并结果,防止字段名大小写不一致导致回滚。

  3. 多活单元化
    单元化场景下,需把“单元标识”(如 center-a、center-b)作为一层 Source,放在环境变量最前,保证不同单元加载不同 DB 分片Redis 集群 地址;否则双活切换时可能连错库。

  4. 编译期检查
    build.rsconfig/default.yaml 转成 const SETTINGS: &str = include_str!(...),在编译阶段 serde_yaml::from_str::<Settings> 提前失败,杜绝上线才发现字段缺失的低级错误。