前段时间帮别人安装了大气科学相关的一套建模系统——Weather Research and Forecasting Model,以及其预处理工具 WPS(WRF Preprocessing System,不是金山那个 WPS)。其构建过程不可谓不复杂,遂考虑用容器化的方式实现可控的构建环境,并写一篇随笔记录一下成品的使用和遇到的困难。
Note: 此前没怎么写过容器相关的东西,不太明白各种情况下容器化的 Best Practice,欢迎各位指正 & 批判 & 提出改进意见。
需要考虑的问题
受限的生产环境
用户使用 WPS/WRF 的服务器使用的是 AlmaLinux 8.10 系统(RHEL 兼容的系统,大概是 CentOS 的精神后继),并且没有 root 权限。直接构建的过程中遇到了 conan 不好装等问题。同时在这台服务器上安装 Podman/Docker 也较为繁琐,即使安装 Rootless Docker。
但在笔者哼哧哼哧装完 Rootless Docker 后才十分小丑地发现,这台机器已经装了 Podman,并且可以 Rootless 地运行(已经给每个用户配置好了 subgid/subuid)。
构建依赖
WPS 和 WRF 有一堆依赖,除了烂大街的 glibc/gfortran/zlib/curl 之类外,还包括:
- MPICH,最简单的一个,没有 C/Fortran 系统库以外的依赖,但会给 WPS/WRF 产生一个动态链接的依赖,同时后续的构建也需要用 MPICH 提供的 mpif90 编译器(或许应该称为某种编译器前端)
- HDF5,依赖 zlib/szip 还算简单,问题是 Conan 版的 Recipe 没有构建 Fortran 库,因此需要自己用 CMake 构建
- NetCDF 4:需要用到 HDF5 和 zlib/szip,conan 上的版本有点旧,并且没有 Fortran 版,因此自己构建。C 和 Fortran 的库是分开的,先构建 netcdf-c 再在其基础上构建 netcdf-fortran
- jasper/libpng/libjpeg:图片压缩库,从 conan 拉就好了
其中 zlib/curl/jasper/libpng/libjpeg 之类的没有修改构建配置,用的人又比较多的库,可以直接用 conan 安装,但 HDF5、NetCDF 4、MPICH 这三者必须手动编译。
成品使用教程
构建脚本已经全部传了 GitHub :https://github.com/cqjjjzr/WRF-WPS-Docker-Containerized 。使用分为两种情况:在容器里面运行成品,和需要把成品二进制取出来在外面跑。同时提供了两个版本,区别在基础镜像为 Ubuntu 24.04 或 Alma Linux 8.10。
Usage 1: 在容器里运行
- 克隆代码库:
git clone https://github.com/cqjjjzr/WRF-WPS-Docker-Containerized.git
并 cd 进去 - Podman 用户请运行
podman build . -f Containerfile --build-arg-file=argfile.conf --tag=wrf_wps:latest
。Docker 用户请运行docker build -f Containerfile --tag=wrf_wps:latest .
(不要漏掉最后的点)。如果想要构建以 Alma Linux 为基础镜像的容器,把 Containerfile 换成 Containerfile_almalinux 即可 - 上一命令执行完后会输出一个镜像 ID。直接用这个镜像 ID 或
wrf_wps:latest
进行podman run
即可,WRF 和 WPS 的命令分别在/build/WRF/run
和/build/WPS
下面。你可能需要挂载你的数据目录到容器里面,并podman run bash
再从里面工作。你可能还需要阅读一下官方的 WRF_DOCKER 项目的教程。
如果想要修改 WRF 和 WPS 在构建过程中的配置,请修改 argfile.conf
(仅 Podman 可用)或通过 build-arg
参数传入。具体的配置项有:
wrf_config_1
、wrf_config_2
:运行 WRF 的configure
时分别传入的两个数字wrf_type
:运行 WRF 的compile
时跟的参数wps_config
:运行 WPS 的configure
时传入的数字
这里一个问题是,WRF 和 WPS 会读取其构建目录内的文件(namelist.wps
等),而我们是可能修改这些文件的(调参等),因此即用即丢的容器会造成这些配置修改的丢失。一个解决方案是先创建一个容器,把 WRF、WPS 目录 podman cp
拷出来,再在后续的 podman run
时,把两个目录挂载进去。
Usage 2: 将二进制文件复制出来用
尝试构建了容器后用着哪哪都别扭,又是挂载数据目录又是要把 WRF、WPS 的目录复制出来的。因此考虑用一个不那么合乎周礼的方法:先用一个和生产环境一样的系统版本作为基础镜像构建项目,再把构建好的文件取出来。
- 克隆代码库:
git clone https://github.com/cqjjjzr/WRF-WPS-Docker-Containerized.git
并 cd 进去 - Podman 用户请运行
podman build . -f Containerfile --build-arg-file=argfile.conf --tag=wrf_wps:latest
。Docker 用户请运行docker build -f Containerfile --tag=wrf_wps:latest .
(不要漏掉最后的点)。如果想要构建以 Alma Linux 为基础镜像的容器,把 Containerfile 换成 Containerfile_almalinux 即可。编译配置的修改同上。 - 复制出来二进制文件,在一个合适的地方(用于存放结果文件的目录):
podman create wrf_wps:latest # 先建一个容器方便取文件,记下返回的ID,替换到所有下列命令的<cont-id>里面
podman cp <cont-id>:/build/WRF ./out/WRF
podman cp <cont-id>:/build/WPS ./out/WPS
podman cp <cont-id>:/install_dir/mpich ./out/mpich
podman rm <cont-id>
- 此时
./out/
中已经有了可以用的二进制文件。但直接执行会报找不到 MPICH 的动态库,因此还需要设置LD_LIBRARY_PATH
追加上到./out/mpich/lib
的绝对路径。推荐把这一设置加到 bash/zsh/fish 的 Profile 里面。未来改进应该把这个写进 rpath 里面!
关于版本更新
虽然笔者有支持升级版本的想法(比如各种依赖版本是写在 conanfile 和 argfile.conf 里面可以改的,而非硬编码),但由于项目里用到了一堆 Patch,因此这并不一定是一个简单的工作。基本的升级思路就是:先修改 argfile 和 conanfile 升级依赖版本与 checkout 时的 tag,再尝试构建。如果出现失败(特别是 Patch 应用失败),则修改各个 Patch 直到成功。
另外 HDF5 和 NetCDF-C 可以确定最新分支与目前使用的版本代码不匹配、无法应用 Patch。hdf5-cmake-d507b7c.patch
与 netcdf-c-cmake-0ad7164.patch
给出了最新分支上(截至写稿时)可以用的 Patch,仅作为参考。
编写过程中遇到的问题
项目目录里一堆 Patch 就已经很能说明问题了。
Conan 和 Build Type
conan 2 改进了 CMake 构建脚本的生成方式,从原先的手动修改 prefix 和 module 路径指向 conan 安装路径的方式改进到了 conan 吐一组 CMakePresets 和 CMakeUserPresets.json 文件的方式。这种情况下只需要在配置的时候用 cmake .. --preset conan-release
就带上各种 conan 的配置。
然而,conan 生成的 CMake 配置文件的问题在于,它内部通过 Generator Expression 的方式指定了只有当当前构建的 build type 与 conan install 时的设置相同(这里是 Release)时,才会正确设置各种 Target 的值,否则只会提供空值。一开始笔者没有发现这一点,同时也没有发现 NetCDF 会偷偷修改 build type,因此十分痛苦地调了很久,明明已经通过 target_link_libraries 传入了库,却没有进入最终的 Ninja 配置里面。期间怀疑过:
- 间接依赖并没有被正确处理——添加为直接依赖也没有解决
- 依赖定序问题,即先写被依赖的再写依赖的会造成丢失——再写一遍也没有解决
- target 的依赖被整个覆盖——在 configure 阶段最后打印一遍依赖,发现没有被覆盖
最后是不知道在哪个阶段,笔者发现链接库里面有一大串 Generator Expression 指定条件为 Build Type = Release,才怀疑起 build type 不匹配,最后通过全文搜索发现 NetCDF 的 CMake 代码里面有覆盖了 build type 的地方,Patch 掉才解决。
各种依赖写漏
CMake 构建时依赖是通过 target_link_libraries 一个个传入的,而不是通过设置 LDFLAGS
和 LIBS
漫灌,因此在写漏、写错依赖名的时候,就是不会 Link。这里没有容错的空间,因此 NetCDF 需要大量 Patch 补齐写漏、写错的依赖。几个典型的包括 szip 的大小写写错、HDF5::HDF5 应该换成 hdf5-static 等等,再补上一堆没有写出来的 find_package
。
WRF/WPS 补齐依赖的过程还要 Cursed 一些。因为这两个程序的构建既不是 autoconf 又不是 CMake(事实上是给了一份 CMake 的,但似乎不是特别完善),因此需要手动 Patch 其文件来提供参数。同时漏的一些库中,有一部分是来自 conan 的,笔者采取的办法是通过 conan 生成 JSON 格式的配置(conan install . --build missing --format json > conaninfo.json
),再通过 jq 提取其中库的打包安装路径(export JASPER=$(cat conaninfo.json | jq -r '.graph.nodes | [.[] | select(.name == "jasper")] | first | .package_folder')
)。同时为了保险,又让 conan 生成了一份 autoconf 版本的配置(事实上就是生成一个会设置 LDFLAGS 和 LIBS 的脚本),获得一组包含所有依赖的 LDFLAGS 与 LIBS 环境变量配置进去。实在是太罪恶了。
这两者的 configure 事实上是生成了一个 configure.wps
文件,其中包括各种参数,再在 Makefile 内去 include 这个配置文件。笔者的做法是修改了生成这个配置文件的脚本和模板,而非去修改生成后的配置文件,意图在使其能适应更多的构建环境,以及确保一定程度上的向上兼容。
P. S.
积攒起来还没写的东西有点多(NAS 和宿舍网络配置的文章已经拖了一年多了,都快拖到我毕业了……),缓更……
Comments | 2 条评论
博主 1640390791
博主很棒,想咨询您如果我在超算账户中没有root权限想安装rootless docker,其中如何配置subuid和subgid,以及在解压二进制包后,使用curl命令没有root权限,提示:-bash: /usr/bin/sudo: Permission denied
这应当如何解决啊?
博主 茶栗
@1640390791 subuid和subgid恐怕需要管理员或有sudo权限的帮助。总之您可以先看看 /etc/subuid 里面有没有自己的用户名、subgid 里面有没有自己所在的组。如果需要配置请参考 https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md#etcsubuid-and-etcsubgid-configuration 。
解压二进制包、curl 是具体在哪一步完成的?使用了上述哪种方案?