一、問題起源
系統(tǒng)從 CentOS7 升級到 Rocky8 后由于 intel 2016 版的 mpi 依賴的 glibc 與系統(tǒng)的 2.28 版的 ABI 不兼容,導致之前在 CentOS7 下編譯出的 vasp 在 Rocky8 下 mpirun 運行時出錯,一運行就報 *** buffer overflow detected *** 錯誤。雖然串行運行時正常,但是無法并行就相當于無法使用。這樣就帶來了兩個問題:
- intel 2016 版編譯器與 Rocky8 的 glibc ABI 不兼容,而且所依賴的 gcc 頭文件版本也不對,導致編譯出錯。并不是一定無法編譯,而是當涉及與高版本不兼容的內(nèi)容時會出錯。比如編譯一個簡單的源文件時通常沒什么問題,但是編譯 vasp 就會出錯!同樣的設置在 CentOS7 下就能正常編譯??梢娫?Rocky8 下直接用 intel 2016 編譯大型程序通常會遇到問題。
- 即使是之前在 CentOS7 下編譯好的程序,在 Rocky8 下 mpirun 并行運行時也會出錯!
那么就徹底不能用了嗎? 也不是!
二、容器
1. podman 容器中編譯
上面第一個問題可通過 podman 容器解決,用 podman 創(chuàng)建一個 CentOS7 的容器,映射宿主機上的 intel 2016 及 vasp 源文件,然后在容器中編譯。或者干脆在別的裝有 CentOS7 的機器上用 intel 2016 編譯好拷過來??傊?,就是還是要在 CentOS7 下編譯。如果不需要重新編譯的話,那么已有的程序不用動。
2. apptainer 容器中運行
podman 容器更像 docker,雖然不需要后臺服務程序,但它仍不方便在計算集群上供多人使用。因為文件權(quán)限、個人設置等等各種原因吧。而 apptainer (epel 源里就有)則是專門針對高性能計算集群用戶量身定做的容器方案,比 podman 更輕量級且更透明。它不像 podman 或 docker 一樣區(qū)分鏡像與容器的概念,它只有鏡像(當然叫容器也行,反正都一樣),用戶不能修改,只能直接調(diào)用。總之,apptainer 就是專門為特定程序提供運行環(huán)境打包而設計的。
(1) 創(chuàng)建鏡像
通常通過 .def 文件創(chuàng)建鏡像,比如 centos7_base.def 內(nèi)容如下:
Bootstrap: docker
From: centos:7
%post
# 使用 USTC 科大 vault 源代替已失效的官方源
sed -i 's|mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-Base.repo
sed -i 's|#baseurl=http://mirror.centos.org/centos/|baseurl=https://mirrors.ustc.edu.cn/centos-vault/|g' /etc/yum.repos.d/CentOS-Base.repo
yum -y clean all
yum -y makecache
yum -y install \
which tar gzip bzip2 \
gcc gcc-c++ gcc-gfortran \
numactl numactl-devel \
environment-modules \
openssh-clients
# 用于掛載 /public
mkdir -p /public
%environment
export MODULEPATH=/public/software/modules
source /usr/share/Modules/init/bash
然后用如下命令就可以創(chuàng)建鏡像文件 centos7.sif 了
apptainer build centos7.sif centos7_base.def
有了鏡像文件后普通用戶就可以直接使用該鏡像文件運行了,其中 xxxx 是用戶想在容器里運行的命令,--bind 用于映射文件或目錄,類似 docker 里的 -v。
apptainer exec --bind /public:/public centos7.sif xxxx
不過,上面創(chuàng)建鏡像的過程會失敗,因為他要到 docker-hub 上去下載,然而卻連不上。好在 apptainer 可以直接導入 podman 或 docker 導出的鏡像文件。Rocky 8 下可先用 podman 拉取鏡像,然后再導出為 centos7.tar:
podman pull quay.io/centos/centos:7
podman save -o centos7.tar centos:7
有了 centos7 的鏡像,可以如下創(chuàng)建 apptainer 的鏡像 centos7.sif
apptainer build centos7.sif docker-archive://centos7.tar
但是這樣的 centos7.sif 里的內(nèi)容是未經(jīng)配置的,通常并符合我們的需求,但它又無法修改了。所以,最理想的辦法是先創(chuàng)建一個 “沙箱” 鏡像,其實就是一個目錄,用于存放鏡像的文件:
apptainer build --sandbox centos7_sbox docker-archive://centos7.tar
然后就有了一個 centos7_sbox 目錄。此時可以通過如下命令進入該沙箱,對其進行修改(需加 --writable 選項)。
apptainer shell --writable centos7_sbox
執(zhí)行上述命令后會進入一個以 Apptainer> 開頭的 shell 中,這其實就是 centos7_sbox 所代表的沙箱容器(或鏡像)的 shell。先修改 yum 源,改為科大的 http://mirrors.ustc.edu.cn/centos-vault,這樣就可以 yum 安裝所需程序了,比如 gcc gcc-c++ gcc-gfortran environment-modules openssh-clients 等。然后設置 module,在 /etc/profile.d 目錄下創(chuàng)建 module.sh 文件,內(nèi)容如下(此處需根據(jù)自己實際目錄調(diào)整):
export MODULEPATH=/public/software/modules
source /usr/share/Modules/init/bash
再裝一些可能用到的程序就基本差不多了。需注意:對于鏡像中本沒有的路徑,apptainer 會自動映射宿主機的路徑!比如,Apptainer> 下的 /root 路徑就是我真實宿主機的 /root!所以千萬不要不小心刪了宿主機的東西!沙箱鏡像設置完后就可以退出了,然后就可根據(jù)此沙箱鏡像創(chuàng)建 .sif 只讀鏡像了:
apptainer build centos7.sif centos7_sbox
(2) 運行鏡像
把 centos7.sif 放到一個普通用戶能訪問的地方,比如 /public/apptainers/,然后普通用戶就可以運行容器中的命令了,比如運行 lsb_release -a 命令會輸出 CentOS7 信息(前提是在配置沙箱時安裝了 redhat-lsb-core 包)
apptainer exec --bind /public:/public /public/apptainers/centos7.sif lsb_release -a
LSB Version: :core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description: CentOS Linux release 7.9.2009 (Core)
Release: 7.9.2009
Codename: Core
需注意的是,若運行的命令是 uname -a 則顯示的是宿主機的內(nèi)核信息,這一點和 podman 以及 docker 等是一樣的。有一點需要注意,雖然 moudle 已經(jīng)安裝并配置好了,但如果直接運行
apptainer exec --bind /public:/public /public/apptainers/centos7.sif module list
會提示 FATAL: "module": executable file not found in $PATH 錯誤,這是因為 apptainer exec 進入的是一個 非登錄的 shell,并不會觸發(fā)加載 /etc/profile.d 下的腳本,需使用 bash -l 主動登錄才行,并將運行的命令以 -c 參數(shù)傳入,所以應該如下使用:
apptainer exec --bind /public:/public /public/apptainers/centos7.sif bash -lc "module list"
Currently Loaded Modulefiles:
1) compiler/intel/2016u3 2) apps/vasp/5.4.4-i16
它會自動從宿主機上我的用戶目錄下讀取 ~/.bashrc 設置,容器中的工作目錄就是宿主機里的當前工作目錄。所以將上述命令中的 "module list" 直接替換為 "mpirun -n 16 vasp_std" 它運行的就是 intel 2016 版在 CentOS7 下編譯出的 vasp_std 文件,可以正常運行!為了便于使用,將 apptainer 命令封裝在 runi16 腳本里,內(nèi)容如下(這樣運行有問題,見第(3)部分):
#!/bin/bash
apptainer exec --bind /public:/public /public/apptainers/centos7.sif bash -lc "$*"
在每個節(jié)點上都裝上 apptainer,把 runi16 拷到每個節(jié)點的 /usr/local/bin 下,就可以直接通過 runi16 vasp_std 直接通過容器提供的 CentOS7 環(huán)境來運行之前的 intel 2016 編譯的 vasp_std 了。如果是在腳本中提交并行任務,只需如下運行即可
mpirun -n 64 runi16 vasp_std #核數(shù)根據(jù)實際情況調(diào)整
注意需使用 mpirun(實際調(diào)用的是 mpiexec.hydra,不要使用基于 mpd 的 mpiexec,也不要使用 slurm 的 srun,它用的應該是 mpiexec,會提示 mpd 進程未運行等問題)。然后我到一個計算節(jié)點上去運行試一下,結(jié)果發(fā)現(xiàn)提示如下錯誤:
INFO: Cleanup error: while unmounting /var/lib/apptainer/mnt/session/final directory:
no such file or directory, while unmounting /var/lib/apptainer/mnt/session/rootfs directory: no such file or directory
FATAL: container creation failed: open /etc/resolv.conf: no such file or directory
原來是因為計算節(jié)點上沒有 /etc/resolv.conf 文件,apptainer 會自動掛載宿主機的該文件,若宿主機沒有則報此錯誤。解決辦法也很簡單,用 root touch 一個空的 /etc/resolv.conf 即可。然后再運行就正常!
至此,通過 apptainer 部署 CentOS7 運行環(huán)境的容器完美運行之前 CentOS7 遺留下來的由 intel 2016 編譯的程序。用戶只需在相應程序前用 runi16 腳本封裝一下來運行即可,不用關(guān)心容器的細節(jié)!
(3) 并不完美
明明早上已成功運行 mpirun -n 64 runi16 vasp_std,而且還提交了任務試了一下,成功算完。但是,下午再試同樣的命令又不能運行了。此時不論是上面命令還是 runi16 mpirun -n 64 vasp_std 都報同樣的 buffer overflow 錯誤,顯然是又出現(xiàn)了 glibc ABI 不兼容的問題!可我明明已經(jīng)用了 apptainer 容器了啊!系統(tǒng)設置也沒有變!到了晚上,莫名其妙的 runi16 mpirun -n 64 vasp_std 又能運行了!看來這樣運行并不穩(wěn)定!問 AI,它說:
Apptainer 采用 “集成而非隔離” 的理念,默認情況下,容器內(nèi)的應用程序會直接使用宿主機系統(tǒng)的動態(tài)鏈接庫,包括GLIBC。這是 Apptainer 針對高性能計算(HPC)環(huán)境的一項核心設計特性。Apptainer 容器的設計使其默認優(yōu)先使用宿主機的 GLIBC,這帶來了出色的性能和兼容性,但也需要注意宿主機與容器內(nèi)軟件對GLIBC版本的依賴關(guān)系。
由此可見,默認情況下 apptainer 并不能很好的隔離 glibc。但奇怪的是為啥早上的時候能運行?是環(huán)境變量變了還是什么變了?而且一陣好一陣壞的,不得而知!
我的目的就是要隔離宿主機的 gblic,好在 apptainer 提供了 --contailall 選項,它是一個非常有用的安全與隔離選項,它能為 apptainer 容器運行環(huán)境提供一個最高級別的隔離。
| 功能類別 | 具體控制項 | 默認情況 (無--containall) |
使用 --containall 后 |
|---|---|---|---|
| 文件系統(tǒng)掛載 | 自動掛載宿主機的$HOME、/tmp、/proc等目錄 |
自動掛載 | 禁止自動掛載 |
| 環(huán)境變量 | 繼承宿主機的環(huán)境變量 | 全部繼承 | 不繼承 |
| 臨時文件系統(tǒng) | 在容器內(nèi)掛載/tmp和/var/tmp
|
使用宿主機的目錄 | 使用容器內(nèi)隔離的臨時目錄 |
使用 --containall 參數(shù)后可以屏蔽宿主機環(huán)境變量可能帶來的干擾,讓容器的運行環(huán)境更加純凈和一致,完全使用容器內(nèi)的 gblic 而與宿主機無關(guān),這樣才能解決我的問題。不過,使用該參數(shù)后默認不會保持工作目錄,需要手動傳遞工作目錄,此時將 runi16 腳本內(nèi)容改為如下:
#!/bin/bash
apptainer exec --containall --pwd="$(pwd)" --bind /public:/public /public/apptainers/centos7.sif bash -lc "$*"
而且,即使這樣修改后依然無法 mpirun -n 64 runi16 vasp_std 這樣計算,這樣用時雖然不會報 buffer overflow 錯誤了,但是無法正常并行,看到的是四個獨立的 vasp_std 進程在計算,這就是完全隔離所需的代價,系統(tǒng)內(nèi)外的消息傳遞、網(wǎng)絡模式、內(nèi)存共享等等無法保持一致。此時只能在容器內(nèi)部進行 mpi 并行, 即 runi16 mpirun -n 64 vasp_std,這樣帶來的一個限制就是 只能單個節(jié)點并行,不能跨節(jié)點并行! 不過好在單個節(jié)點核數(shù)也不少,通常情況下也夠用了。
由于使用了 --containall 后,宿主機環(huán)境變量無法直接傳入容器中,若沒有在 ~/.bashrc 中設置 module,在腳本中 runi16 之前的設置將不管用。此外,即使 ~/.bashrc 中有設置,但與所需設置沖突也會失效。最保險的辦法是在運行 runi16 時清除原有設置并重新設置,即:
runi16 "
module purge
module load xxx yyy
mpirun -n 64 vasp_std # 或把核數(shù)替換成自動的 $SLURM_NTASKS_PER_NODE
"
上面 runi16 運行的命令可以放在一行并以 ; 分隔,也可以使用 SLURM 的環(huán)境變量,但前提是兩邊得用雙引號 ",這樣在傳給 runi16 時就會替換為實際值;若用單引號 ' 則直接將 $SLURM_NTASKS_PER_NODE 以字面形式傳給 runi16, 而 runi16 的容器內(nèi)部是沒有 slurm 的,這些變量的值就是空的。