返回专辑
·Johan·4 分钟阅读

filesystem 与配置加载:路径与原子替换

std::filesystem 加载 YAML/JSON 配置的路径解析、热更新与失败回滚。

filesystem 与配置加载:路径与原子替换

1. 参数「有时生效有时默认」——读到半写入文件

现场 SSH 改 YAML,编辑器保存中途节点 reload——parse 到半行,部分键落默认,RViz 表现像「灵异调参」。根因不是 Nav2,是 读写缺乏原子性。写配置须 temp + flush + 同目录 rename;读者要么旧版要么新版,不见 partial read。

现场改 YAML 参数后重启节点,读到一半的旧文件——写配置时用原子替换(write temp + rename)避免 partial read。std::filesystem(C++17)让这类模式可移植,别手写字符串拼路径。Windows 上 rename 覆盖行为不同;NFS 原子性不保证——大的是纪律,不是 API 本身。

2. 原子替换

cpp
namespace fs = std::filesystem;

void atomic_write(const fs::path& target, std::string_view content) {
  const auto tmp = target.string() + ".tmp";
  {
    std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
    out << content;
    out.flush();
  }
  fs::rename(tmp, target);
}

tmp 与 target 必须在同一 writable mount——容器 read-only rootfs 时 config 挂 volume,否则 rename 失败要有明确错误码。Windows 先 remove 或封装 MoveFileEx 语义。写完后 fs::permissions 检查,避免 umask 导致敏感标定 group 可读。Linux 上同目录 rename 原子;跨 filesystem rename 不原子,别用。

3. 路径与 share 目录

禁止手写 / 拼路径;用 fs::pathament_index_cpp::get_package_share_directory

cpp
const fs::path cfg = share / "config" / "filters.yaml";
if (!fs::exists(cfg))
  throw std::runtime_error("missing " + cfg.string());

相对路径在不同 cwd 启动行为不同——launch 用 absolute 或 share 基准。符号链接 deploy 时 weakly_canonical 解析真实路径,防 field 改错文件。路径用 fs::path 拼接,禁止 / 硬编码——Windows CI 会教做人。

4. 加载、校验与热 reload

cpp
std::expected<Config, std::string> load(const fs::path& p) {
  if (!fs::exists(p)) return std::unexpected("missing: " + p.string());
  // parse + schema validate required keys
}

inotify/mtime 触发 reload:debounce 500ms 防编辑器双写。parse 到临时 struct,成功再 atomic swap pointer;失败 保留旧 config 并 alarm,别 half-load。YAML schema 校验 required keys;与 ros2 param 双源时以 launch 覆盖顺序为准,启动摘要打印关键有效值。watch mtime 变化后 reload:debounce 防编辑器双写。

5. 多机与 NFS

NFS 上 rename 原子性不保证——加 file lock 或版本号字段;多机 sync 参数时写清冲突策略。k8s emptyDir 挂载 volume 时 tmp 与 final 须同目录,规则与 bare metal 相同。容器 read-only 根文件系统下,config 必须 mount 到 writable volume。

6. 案例:并发写读 stress

运维脚本循环 atomic 写 params.yaml 与节点 reload 并行——未 atomic 写时偶发 parse exception 或半键生效。改 atomic_write 后 stress 一周无 partial read。这类 bug 低频但难复现,须专门 stress 而非等 field 踩。ros2 param dump 与 filesystem yaml 双源以 launch 为准。说明配置 IO 的 atomic 与参数 declare 的契约同等重要——一个管「读全」,一个管「读对」。

7. 失败症状与排查

参数「有时生效有时默认」——读到半写入文件。相对路径在不同 cwd 启动行为不同——launch 应用 absolute path 或 ament share。reload 失败应 rollback 上一版 mtime snapshot,别 half-load 新参数。config 热更新 debounce 500ms 防编辑器双写。

8. 覆盖顺序与版本捆绑

与 ROS 参数一样,filesystem yaml 也须固定覆盖顺序(launch > share 默认 > 内置默认),启动摘要打印最终生效路径与 hash。只回滚二进制不回滚 yaml(或相反)会出现「已回滚却依旧」——发布物捆绑代码 版本与 config 版本。车队灰度改 yaml 时,单台验证 parse + reload + 行为指标再扩批。

9. schema 与 CI

CI 检查 yaml required keys ⊆ schema;删除代码字段同步删 yaml。parse 失败信息须含文件路径与行号,别只 throw generic exception。集成测试用 temp 目录跑 atomic_write + 并发 read,挂 nightly 而非等 field。敏感标定(相机内参、轮半径)权限与 audit 与 filesystem 路径绑定,别 world-readable。config 目录变更走 PR review,与代码同权——「只改 yaml」仍是行为变更。

10. 与 declare_parameter 的双源

filesystem yaml 与 ros2 param 可能双源——团队须写清 precedence,启动摘要打印最终值。缺 declare 的键静默落默认,与「读到半文件」不同,但同样不可审计。长期应让关键参数进 declare,filesystem 只做 overlay 或 fleet 批量下发,别两套 truth。

10. 验收

  • 并发写读 stress:读者 never 见截断 yaml。
  • 缺文件、schema 错:启动失败,日志指名 path 与键。
  • reload 失败回滚上一版;有 mtime snapshot 或版本号。
  • Windows CI 与 Linux 路径拼接均过。

fleet 下发 config bundle 版本号,节点启动 log 打印 path 与 sha256,事故复盘能指认哪套 yaml 生效。与 declare 双源时 precedence 写进 launch 注释,别靠口头 tradition。Windows CI 必跑 path 拼接与 atomic rename 用例,别等客户现场教。NFS 多机同步须 file lock 或版本字段,rename alone 不够。

← 全部文章

johan's blog