Project NOMAD 磁盘信息采集器迁移指南:从主机级采集脚本到 disk-collector Sidecar 容器
Project NOMAD 的 "Command Center"(管理后台)需要展示宿主机磁盘用量与可用空间,这一数据曾由运行在宿主机上的后台脚本负责采集。由于该方法在主机重启、进程被杀等场景下表现脆弱,项目引入了独立的 disk-collector sidecar 容器取而代之。migrate-disk-collector.md 正是对这一迁移过程的权威说明:它解释了迁移原因、演示了如何手工修改 compose.yml,并提供了自动化的迁移脚本。
读完本文,你将理解新旧两种磁盘采集架构的本质差异,掌握 disk-collector 服务在 compose 中的完整配置语义,并能用一行命令(或完全手工)安全地完成迁移。
为什么需要迁移:主机级采集方案的四个缺陷
旧方案(collect_disk_info.sh)是在宿主机上以 nohup 方式运行的无限循环脚本:每 300 秒执行一次 lsblk 与 df,把结果写成 /tmp/nomad-disk-info.json,再由 admin 容器通过 bind mount 读取该文件。这套设计存在以下问题(摘自 migrate-disk-collector.md):
- 进程易失:主机进程可能崩溃或被 kill,导致磁盘信息陈旧甚至缺失,没有任何自愈机制。
/tmp在重启后被清空:主机重启会清空/tmp,此时 Docker 会在原挂载点上创建一个目录而非文件,bind mount 直接失效,admin 容器无法读取任何内容。- 与主机强耦合:必须在宿主机上维护一个常驻进程,限制了后续部署形态的灵活性(例如更自由地分发、迁移安装方式)。
- 独立于容器生命周期:主机进程不受 compose 栈统一管理,难以随栈启停、更新或回滚。
新的 disk-collector sidecar 正是针对以上问题设计的:磁盘采集逻辑进入容器化组件,随 compose 栈统一编排,通过只读挂载宿主根文件系统持续工作,不再依赖 /tmp 上的任何文件。
为什么 NOMAD 需要这份磁盘信息
磁盘信息在 NOMAD 的 "Command Center" 中用于展示磁盘使用情况与可用容量。如文档所述,它不是核心功能的关键依赖,但为两类用户提供了显著更好的体验:
- 存储空间有限、需要随时关注剩余容量的用户;
- 不熟悉命令行与 Linux 管理的用户——他们可以在图形界面中直接看到磁盘状态,无需登录宿主机执行
df。
从源码看,这份数据的消费链路位于 admin/app/services/system_service.ts:admin 的 getSystemInfo() 会读取容器内 /storage/nomad-disk-info.json(该路径即宿主机 /opt/project-nomad/storage 的挂载点),将 JSON 解析为 NomadDiskInfoRaw 结构(包含 diskLayout 与 fsSize),随后通过 calculateDiskUsage() 计算每个物理磁盘的占用比例,最终由 /api/system/info 等接口提供给前端 UI 渲染。所以,迁移的关键约束是:新的采集方必须把文件写到同一个位置(/opt/project-nomad/storage/nomad-disk-info.json),admin 容器才能无感地继续读到数据。
为什么用独立的 sidecar 容器
新方案把采集逻辑放进单独的容器,而不是并入 admin 主容器或留在主机上,主要基于三点考量(migrate-disk-collector.md):
- 稳定性(Stability):采集器与 admin 主容器彼此隔离,任一容器故障都不会拖垮对方。
- 安全性(Security):admin 容器已通过 Docker socket、存储目录与
host.docker.internal拥有较高的宿主机访问能力,未来 NOMAD 还可能引入多用户与更多网络暴露。将磁盘采集(即便只是只读访问宿主机文件系统)隔离到功能范围极小的独立容器中,可以显著收窄宿主机文件系统暴露面。 - 模块化(Modularity):既然磁盘信息不是核心功能,把它做成 sidecar 后,不需要此功能的用户可以直接不运行该容器,admin 与其他服务完全不受影响;sidecar 自身的迭代也不必改动 admin 镜像。
从项目镜像编排也能印证这一点:sidecar 采用与 Prometheus node-exporter 相同的 /:ro,rslave 只读宿主根挂载模式,无需 privileged 或 SYS_ADMIN 等扩展能力(见 migrate-disk-collector.sh 中的设计说明),从设计上就把权限诉求压到最低。
迁移脚本做了什么:六步全流程拆解
自动化迁移脚本 migrate-disk-collector.sh 会修改 /opt/project-nomad/compose.yml 以新增服务并移除旧的 bind mount,然后重启完整 compose 栈。执行前脚本会先做一组前置检查(必须以 bash 运行、具备 sudo 权限、Docker 已安装且运行中、存在 compose 文件),并要求交互式确认。整体流程分为六步:
| 步骤 | 动作 | 脚本实现要点 |
|---|---|---|
| 1 | 停止旧主机进程 | 读取 nomad-collect-disk-info.pid 记录并 kill 对应进程(见 stop_old_host_process) |
| 2 | 备份 compose.yml | 生成带时间戳的备份 compose.yml.bak.<yyyyMMddHHmmss>(见 backup_compose_file) |
| 3 | 移除 admin 服务上的旧 bind mount | 用 sed 删除包含 /tmp/nomad-disk-info.json 的 volume 行(见 remove_old_bind_mount) |
| 4 | 插入 disk-collector 服务块 | 用 awk 在顶层 volumes: 键之前插入服务定义(见 add_disk_collector_service) |
| 5 | 拉取镜像并重启栈 | docker compose -p project-nomad pull + up -d,重建成 admin 容器以丢弃旧挂载(见 restart_stack) |
| 6 | 验证容器运行 | 检查名为 nomad_disk_collector 的容器处于 running 状态(见 verify_disk_collector_running) |
其中每一步都内置了幂等与失败保护:若旧 bind mount 已不存在则直接跳过;若 disk-collector 服务已存在则跳过插入;任一步失败都会输出明确提示并中止,且备份文件会保留以便回滚。脚本结束时会提示:sidecar 每 2 分钟更新一次数据,首个采集周期约在启动后 5 秒内完成,届时 /api/system/info 即可返回磁盘数据。
手工迁移:compose.yml 增改对照
如果你不希望运行迁移脚本,也可以完全手工完成同样的改动。文档与脚本共同给出的最终配置结构如下(该配置与当前仓库中的 management_compose.yaml 中的 disk-collector 服务一致):
disk-collector:
image: ghcr.io/crosstalk-solutions/project-nomad-disk-collector:latest
pull_policy: always
container_name: nomad_disk_collector
restart: unless-stopped
volumes:
- /:/host:ro,rslave # Read-only view of host FS with rslave propagation so /sys and /proc submounts are visible
- /opt/project-nomad/storage:/storage
同时在 admin 服务的 volumes 中删除这一行旧的 bind mount:
- /tmp/nomad-disk-info.json:/app/storage/nomad-disk-info.json
配置参数逐项解读
| 配置项 | 值 | 作用与语义 |
|---|---|---|
image |
ghcr.io/crosstalk-solutions/project-nomad-disk-collector:latest |
sidecar 镜像地址,随更新流程跟随 latest 标签拉取 |
pull_policy: always |
—— | 每次拉起前强制拉取最新镜像,确保与升级流程联动 |
container_name |
nomad_disk_collector |
固定容器名,验证与日志排查均依赖该名称 |
restart: unless-stopped |
—— | 异常退出自动重启(除非被显式停止),与 admin 等服务的重启策略保持一致 |
volumes 第一项 |
/:host:ro,rslave |
只读挂载宿主机根文件系统到容器内 /host;rslave 传播使 /sys、/proc 等子挂载点可见,这是读取真实宿主机视图的关键,且无需扩展权限 |
volumes 第二项 |
/opt/project-nomad/storage:/storage |
将 NOMAD 存储目录挂到容器 /storage,采集结果直接写入宿主机存储目录下的 JSON 文件 |
关于第二条 volume 有一条重要的实操提醒(见 management_compose.yaml):如果你重新定位过存储目录,必须把这里的主机路径与 admin 服务、NOMAD_STORAGE_PATH 设置成完全一致(路径区分大小写),否则 UI 中显示的磁盘用量会指向错误的位置。迁移文档也明确建议:执行任何修改前先备份 compose.yml。
Sidecar 内部的采集逻辑与文件格式
理解 sidecar 如何产出数据,有助于排查迁移后的问题。容器镜像基于 alpine:3.20,仅额外安装 util-linux(提供 lsblk)与 bash(见 sidecar-disk-collector/Dockerfile),镜像体积与攻击面都被刻意压到最小。
核心循环位于 collect-disk-info.sh,每 120 秒执行一轮:
- 磁盘布局采集:
lsblk --sysroot /host --json -b -o NAME,SIZE,TYPE,MODEL,SERIAL,VENDOR,ROTA,TRAN。--sysroot指向只读挂载的宿主根,-b强制以字节为单位输出,JSON 结构对应管理端解析的diskLayout.blockdevices。 - 文件系统用量采集:读取
/host/proc/1/mounts(PID 1 即宿主机 init 进程,代表宿主机根挂载命名空间),逐行解析真实文件系统,并用df -P -B1获取字节精度的size/used/available/use。注释中专门解释了为何不读取/host/proc/mounts——它是指向/proc/self/mounts的软链,反映的是容器自身的命名空间,而非宿主机的挂载表。 - 过滤伪文件系统与噪声:跳过
tmpfs/devtmpfs/squashfs/overlay/proc/sysfs/cgroup等虚拟文件系统,也跳过 Docker 为单文件注入的 bind mount(如/etc/resolv.conf),避免虚假容量。若宿主挂载表不可读,则退化为直接对/storage执行df作为兜底。 - 原子写盘:先写临时文件
nomad-disk-info.json.tmp,再mv覆盖目标文件,避免读取方拿到半截 JSON。启动时若目标文件不存在,还会先写入一个合法的空占位结构,保证 admin 容器首次解析不报错。
最终 JSON 结构与旧脚本(collect_disk_info.sh)保持一致:
{
"diskLayout": { "blockdevices": [ ... ] },
"fsSize": [ ... ]
}
admin 端读取后,calculateDiskUsage() 会按 type === 'disk' 过滤物理磁盘,对同一设备在多个挂载点重复出现的情况按最大 size 去重(这是 Docker bind mount 造成的常见重复),再为每块磁盘汇总分区用量并计算 percentUsed。前端 useDiskDisplayData.ts 最终把这份数据渲染成磁盘与可用空间的直观展示。
迁移后如何验证
脚本第 6 步自带了容器级验证:docker ps --filter "name=^nomad_disk_collector$" --filter "status=running"。除此之外,你还可以按以下顺序确认整个链路正常:
# 1. 容器在跑
docker ps --filter name=nomad_disk_collector
# 2. 查看采集日志(能看到 "Disk info updated successfully.")
docker logs nomad_disk_collector
# 3. 确认 JSON 文件已由 sidecar 写入宿主机存储目录(由容器内 /storage 映射而来)
cat /opt/project-nomad/storage/nomad-disk-info.json
# 4. 确认 admin 容器内也能读到同一文件(admin 的 /app/storage 映射同一目录)
docker exec nomad_admin cat /app/storage/nomad-disk-info.json
文件存在且内容含 diskLayout/fsSize 后,调用 /api/system/info 即可在响应中获得经过 calculateDiskUsage 计算后的磁盘数组;脚本提示首个采集周期约在 sidecar 启动后 5 秒内完成,若容器状态异常,可直接用 docker logs nomad_disk_collector 排查(这也是脚本验证失败时给出的首要排查命令)。
不运行迁移脚本时的注意事项
迁移文档特别强调:若你自定义过 compose.yml 或 NOMAD 的存储布局(非默认路径),请优先手工迁移而非运行自动化脚本。原因是脚本第 4 步依赖在顶层 volumes: 键前插入服务块这一假设,并且会自动重启整个 compose 栈;对存储路径做过重新定位的用户,需要按 management_compose.yaml 中三处联动(admin volume、NOMAD_STORAGE_PATH、disk-collector volume)的要求保持路径完全一致。无论走脚本还是手工,先备份 compose.yml 都是不可省略的第一步——脚本在第 2 步会自动生成带时间戳的备份,手工操作时请自行完成等价备份。
迁移完成后,Project NOMAD 的磁盘信息展示将由一个随 compose 栈统一编排、只读访问宿主、无需特权模式的轻量 sidecar 持续供给,彻底摆脱对宿主机后台进程与 /tmp 临时文件的依赖——这也是项目为更灵活的后续部署形态提前铺平的一条路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00