科学计算的可复现性危机由来已久:软件依赖版本漂移、编译器升级导致数值结果变化、不同集群 MPI 库 ABI 不兼容——这些问题让「昨天还能跑通的工作流今天复现不了」成为 HPC 用户的日常。容器技术把整个软件栈(OS 层、编译器、库、应用)打包成不可变镜像,从根本上解决了环境漂移问题。然而 HPC 集群的安全模型与 Docker 的守护进程架构存在根本冲突,于是 Singularity(现更名 Apptainer) 应运而生,成为超算中心的容器事实标准。本文深入其架构与生产实践。
1. HPC 为什么需要容器
超算中心普遍采用「登录节点编译、计算节点运行」的分离模式,软件栈由系统管理员统一维护。这种模式带来三个痛点:
| 痛点 | 具体表现 | 容器化后 |
|---|---|---|
| 环境漂移 | module 默认版本变化、共享库被升级 | 镜像内依赖完全固定 |
| 用户隔离 | 普通用户无 root 权限,无法安装系统级依赖 | 镜像构建即用户自己的环境 |
| 批量复制 | 在数百节点复制一致环境成本高 | SIF 单文件分发,cp 即部署 |
用户级的根本诉求是:在自己的空间里构建整个软件栈,再原样搬到计算节点上运行,且不要求任何特权。这正是 Singularity 的设计起点——它诞生于 2016 年,目标直指超算环境的「无特权 + 单文件镜像」。
一句话:HPC 容器解决的核心问题是环境可复现性,而它的架构设计前提是超算中心的多租户安全模型。
2. Singularity/Apptainer 核心架构与 SIF 格式
Singularity 项目在 2021 年分叉:原社区版改名为 Apptainer(捐赠给 Linux Foundation,多数超算中心采用),商业化分支由 Sylabs 维护,命名为 SingularityCE。两者核心机制一致,命令也基本兼容。
与 Docker 不同,Singularity 是**无守护进程(daemonless)**的:
+--------------------+ +--------------------+
| Docker 模型 | | Singularity 模型 |
| | | |
| client ──→ daemon ──→ 容器 | 用户进程 ──→ 容器 |
| (root 特权) | | (无 root 需要) |
+--------------------+ +--------------------+
↑ ↑
dockerd 管理一切 用户身份直接运行,
进程就是镜像内进程
每次 singularity exec 都由用户自身身份直接启动容器内进程,无需中间特权守护进程。镜像文件是只读的 SIF(Singularity Image Format),一个自包含的二进制文件:
+-----------------------------------------------------------+
| SIF 单文件结构(squashfs) |
+-----------------------------------------------------------+
| SIF 头部 + 签名区(可验证完整性与作者) |
| metadata(定义文件、环境变量、runscript) |
| squashfs 文件系统(只读,压缩) |
| overlay 层(运行时 writable 临时层,自动清理) |
+-----------------------------------------------------------+
SIF 的「单文件 + 只读 + 签名」设计带来三个生产级特性:镜像不可变(运行时不会被意外篡改)、可通过 cp/rsync 在数千节点间秒级分发、可通过签名校验确保镜像来源可信。
关于可写性:SIF 本身只读,运行时若有写需求(如临时文件、缓存),Singularity 会自动挂载一个 overlay 临时层(默认 tmpfs),容器退出即销毁。这一设计保证了「镜像永不脏」——任何运行时的状态污染都不会回写到 SIF 文件本身,这与 Docker 可写容器层形成鲜明对比。需要持久写入时,显式使用 --writable-tmpfs(临时)或 --bind 挂载宿主机目录(持久)。
Singularity 的命令行入口分为五个,覆盖全部使用场景:
| 命令 | 作用 | 类比 Docker |
|---|---|---|
singularity build | 从 def 文件/Registry 构建 SIF | docker build |
singularity pull | 从 Registry 拉取并转换为 SIF | docker pull |
singularity exec | 在镜像内执行单条命令 | docker run |
singularity shell | 进入镜像交互式 shell | docker run -it |
singularity run | 执行镜像的 runscript 入口 | docker run(ENTRYPOINT) |
3. 构建 Singularity 镜像:def 文件与 build
构建镜像的核心是 定义文件(Definition File),它描述从基础镜像开始的每一步。以下是一个带 Python 3.11 与 NumPy 的示例:
Bootstrap: docker
From: ubuntu:22.04
%post
apt-get update && apt-get install -y python3.11 python3-pip
pip3 install --no-cache-dir numpy==1.26.4 scipy==1.12.0
%environment
export OMP_NUM_THREADS=4
export OMPI_MCA_btl=^openib
%runscript
exec python3 "$@"
%labels
Author Leeting Yan
Version 1.0.0
构建并验证:
# 构建(需要 root 或 fakeroot,因为要挂载文件系统)
singularity build myenv.sif myenv.def
# 交互式 shell 进入镜像
singularity shell myenv.sif
# 直接运行 runscript
singularity run myenv.sif script.py
# 执行任意命令
singularity exec myenv.sif python3 -c "import numpy; print(numpy.__version__)"
def 文件支持更多的生命周期钩子。除 %post、%environment、%runscript 外,常用的还有 %files(构建期拷贝文件进镜像)与 %test(构建后自动运行验证)。完整示例:
Bootstrap: docker
From: ubuntu:22.04
%files
./configure_site.json /opt/app/config.json
%post
apt-get update && apt-get install -y gcc make
cd /opt && tar xzf /opt/src/app.tar.gz
cd /opt/app && ./configure --prefix=/opt/app && make -j8
%test
/opt/app/bin/app --self-test
%labels
Author Leeting Yan
Version 1.0.0
%test 段会在 singularity build 结束时自动执行,任何失败都会导致构建失败——这是把「镜像能跑」这一验收标准写进构建流程的最佳实践。
一句话:def 文件 = 环境构建说明书,SIF 文件 = 固化产物;构建一次,处处运行,
%test把验证也写进构建。
4. 与 Docker 的架构差异
| 维度 | Docker | Singularity/Apptainer |
|---|---|---|
| 进程模型 | client/server,dockerd 守护进程 | 直接 exec,无守护进程 |
| 特权要求 | 构建与运行均需 root/特权组 | 构建需 root 或 fakeroot,运行无需特权 |
| 镜像格式 | OCI Image 多层 tar + manifest | SIF 单文件(squashfs + 元数据) |
| 用户身份 | 默认容器内 root | 保留宿主机 UID/GID,用户即自己 |
| GPU 支持 | 需 nvidia-container-toolkit | --nv(NVIDIA)/ --rocm(AMD)自动注入 |
| 网络 | 独立 network namespace + bridge | 默认共享宿主网络栈(HPC 需要) |
| 适用场景 | 微服务、多租户隔离 | 超算批处理、MPI 大规模作业 |
最关键的两点差异:身份模型(Singularity 容器内进程就是用户自己,文件权限、配额、scratch 目录访问天然正确)与网络栈(直接使用 InfiniBand、Lustre 等 HPC 基础设施,无需 NAT/端口映射)。
5. 从 Dockerfile 到 SIF:镜像转换
HPC 团队常见的迁移路径是:团队已经维护了 Docker 镜像,需要原样迁移到超算集群。Singularity 支持直接从 Docker Registry 拉取转换,无需重写:
# 方式一:直接拉取 Docker Hub 镜像转换为 SIF
singularity pull nv-hpc.sif docker://nvcr.io/nvidia/hpc-benchmarks:24.03
# 方式二:拉取后构建(可再叠加 def 文件的修改)
singularity build nv-hpc.sif docker://nvcr.io/nvidia/hpc-benchmarks:24.03
# 方式三:从本地 Docker daemon 转换(镜像已在本地)
docker save nv-hpc:latest -o nv-hpc.tar
singularity build nv-hpc.sif docker-archive://nv-hpc.tar
调试阶段可使用可写沙箱(sandbox)模式,把 SIF 展开成目录,修改后重新打包:
# 展开为沙箱目录
singularity build --sandbox nv-hpc-sandbox docker://nvcr.io/nvidia/hpc-benchmarks:24.03
# 在沙箱中修改(需要 fakeroot)
singularity exec --writable nv-hpc-sandbox bash -c "apt-get install -y htop"
# 重新固化回 SIF
singularity build nv-hpc.sif nv-hpc-sandbox
转换时的常见坑:Docker 镜像的 ENTRYPOINT 会被映射到 %runscript;多平台镜像需明确 --platform linux/amd64;含特权 sysctl/device 挂载的镜像转换后可能丢失这些能力。
6. MPI 容器集成:混合模型
MPI 容器是 HPC 容器化中最容易踩坑的部分。核心难点:MPI 库必须能够跨节点通信,而容器内 MPICH 与宿主机 Open MPI 的 PMI 接口、网络传输层必须兼容。行业公认的最佳实践是混合模型(Hybrid Model):
计算节点 A 计算节点 B
+----------------+ +----------------+
| slurm / mpirun | ← PMIx → | slurm / mpirun |
| Open MPI (宿主机) | | Open MPI (宿主机) |
| ↕ | IB/RDMA | ↕ |
| singularity | | singularity |
| 容器内应用 | | 容器内应用 |
+----------------+ +----------------+
原则:MPI 进程管理器(slurm/mpirun)跑在宿主机,容器只装应用和其数学库依赖。具体做法:
#!/bin/bash
#SBATCH --job-name=mpi_container
#SBATCH --partition=gpu-a100
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=8
#SBATCH --gres=gpu:8
# 宿主机加载 MPI,保证跨节点 PMI 一致性
module load openmpi/5.0
# 容器内不装 MPI,直接用宿主机的 mpirun 启动容器内可执行文件
mpirun -np 16 singularity exec --nv myenv.sif ./solve_3d --input grid.dat
若容器内确实需要自带 MPI(例如 App 依赖特定 MPI 版本),必须保证容器内 MPI 与宿主机 PMI 通信栈一致,通常通过 --env PMIX_MCA_* 或 --bind /opt/pmix 桥接。此模式调试成本高,非必要不推荐。
一句话:MPI 容器的最稳路径是「宿主机管通信、容器管应用」,让 PMI/网络层跑在容器外。
7. Conda 环境打包
Python 科学计算栈(PyTorch、NumPy、SciPy)依赖复杂,与 Conda 结合是标配。在 def 文件中安装 Miniconda 并构建隔离环境:
Bootstrap: docker
From: ubuntu:22.04
%post
apt-get update && apt-get install -y wget bzip2
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh -b -p /opt/conda
export PATH=/opt/conda/bin:$PATH
conda create -y -n pyenv python=3.11
conda install -y -n pyenv -c pytorch pytorch=2.3.0 torchvision \
numpy scipy matplotlib
# 清理缓存减小镜像体积
conda clean -y --all
%environment
export PATH=/opt/conda/envs/pyenv/bin:$PATH
export CONDA_DEFAULT_ENV=pyenv
运行验证:
singularity exec myenv.sif python -c \
"import torch; print(torch.cuda.is_available(), torch.__version__)"
注意:Conda 环境体积大(PyTorch 全栈可达 6-8GB),构建时务必 conda clean 与 pip 缓存清理;运行时应使用 --nv(或 --rocm)注入 GPU 驱动库,避免 CUDA 版本与宿主机驱动不匹配。
8. 可复现性与缓存管理
可复现性的三层保障:镜像固化(SIF 只读不可变)、依赖锁定(def 文件中版本精确到小版本)、来源可验证(SIF 签名与哈希)。
镜像的命名本身就是版本管理:建议采用「项目-日期-标签」的语义化命名,并在 %labels 中记录关键元数据:
| 元数据项 | 推荐记录内容 | 查询命令 |
|---|---|---|
| 基础镜像 | ubuntu:22.04、nvcr.io/nvidia/pytorch:24.03 | singularity inspect --labels |
| 依赖版本 | NumPy 1.26.4、OpenMPI 5.0.3 | %post 中固化 |
| 构建时间 | 2026-09-26T10:00:00Z | SIF 元数据 |
| 作者签名 | 密钥指纹 | singularity verify |
| 源文件哈希 | def 文件的 sha256 | git 提交记录 |
# 校验镜像完整性(验证签名与内容哈希)
singularity verify myenv.sif
# 查看镜像元数据(构建来源、标签、环境变量)
singularity inspect myenv.sif
# 导出/导入镜像用于归档
singularity build myenv-20260926.sif myenv.sif # 重新打标签
sha256sum myenv.sif > myenv.sif.sha256
Singularity 的构建缓存位于 $APPTAINER_CACHEDIR(默认 ~/.apptainer/cache,旧版为 ~/.singularity/cache),分为 OCI 层缓存与已构建镜像缓存:
# 查看缓存占用
singularity cache list -v
# 清理部分缓存
singularity cache clean --type oci
# 指定自定义缓存目录(多用户共享缓存,大幅加速重复构建)
export APPTAINER_CACHEDIR=/shared/apptainer-cache
singularity build myenv.sif myenv.def
多用户集群共享缓存目录(并设置 umask 保证可写)可让团队里反复构建同一基础镜像的耗时从分钟级降到秒级。
9. Slurm 作业中的容器使用
结合调度器,容器化作业与普通作业提交并无本质区别,核心是正确传递资源与 GPU:
#!/bin/bash
#SBATCH --job-name=cntr_ai
#SBATCH --partition=gpu-a100
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --gres=gpu:4
#SBATCH --time=04:00:00
#SBATCH --output=job-%j.log
# 注入 GPU(--nv 自动挂载驱动与 CUDA 库)
singularity exec --nv \
--bind /shared/data:/data,${PWD}:/workspace \
--env HYDRA_FULL_ERROR=1 \
nv-pytorch.sif python /workspace/train.py --config /data/config.yaml
生产环境建议在 Slurm 的 Prolog 中统一执行 singularity verify 与镜像预热(预读页面缓存),Epilog 中清理 $SCRATCH 残留,避免镜像碎片化占用节点本地盘。
一句话:容器化不改变调度范式——
sbatch+singularity exec即可,GPU 注入用--nv,数据用--bind显式挂载。
10. 权限模型与安全实践
Singularity 的安全性建立在「最小特权」之上,但管理员仍需理解其两种运行模式的差异:
| 模式 | 机制 | 管理员配置 | 风险 |
|---|---|---|---|
| setuid 模式 | 通过 singularity setuid 二进制辅助挂载镜像 | 安装时启用,squashfs 挂载需特权 | 配置不当可致权限提升 |
| 非特权模式(unprivileged) | 使用 Linux 用户命名空间(userns) | 内核 kernel.unprivileged_userns_clone=1 | 兼容性依赖内核版本 |
生产安全实践清单:
- 镜像签名强制:在 Slurm
Prolog中执行singularity verify --require-signed,未签名镜像直接拒绝调度,阻断供应链投毒。 - 禁用危险选项:集群级配置
singularity.conf中关闭allow setuid的不必要挂载,限制--writable仅在特权用户可用。 - 网络可控:HPC 容器默认共享宿主网络栈,恶意镜像可访问集群内网。用
--net+ 自定义网络插件(Apptainer 的 net plugin)或 eBPF 策略约束出网流量。 - 镜像来源白名单:
singularity pull仅允许从受信任 Registry(内部 Harbor/Nexus)拉取,禁止随意docker://源。 - 用户命名空间审计:监控 userns 滥用(
auditctl -w /proc/sys/kernel/unprivileged_userns_clone),并在升级内核后回归测试。
一句话:容器的安全边界 = 镜像签名(可信来源)+ 最小挂载(最小特权)+ 网络约束(可控出网)。
总结
| 维度 | 核心要点 |
|---|---|
| 架构 | 无守护进程、单文件 SIF、保留宿主用户身份 |
| 构建 | def 文件声明式描述,build 固化,sandbox 调试 |
| 迁移 | singularity pull/build docker://... 直接从 Docker 生态进入 HPC |
| MPI | 混合模型,宿主 MPI + 容器应用,最稳最推荐 |
| Python | def 内嵌 Miniconda,锁定版本,注意体积控制 |
| 可复现 | SIF 只读 + 签名校验 + 依赖锁定 + 共享缓存 |
| 调度 | sbatch + --nv + --bind,Prolog/Epilog 做验证与清理 |
容器已成为现代超算软件分发的默认形态。Singularity/Apptainer 以极低的架构复杂度实现了「用户级软件栈 + 全集群一致环境」的双重目标。配合 Slurm 集群调度 与 InfiniBand 网络 的知识,你就能在超算中心以接近本地开发的体验运行大规模并行作业。关于底层 GPU 环境,可进一步阅读 AMD ROCm GPU 编程 了解异构平台的容器化差异。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。