新西兰的经济依赖于农业,而农业是一个对气候变化高度敏感的部门。 这使得开发分析能力来评估其影响并研究可能的缓解和适应方案至关重要。 这种分析可以使用诸如农业系统模型之类的工具来完成。 简单来说,它涉及创建一个模型来量化特定作物在特定条件下的行为,然后模拟改变一些变量以观察该行为如何变化。 可用于执行此操作的一些软件包括华盛顿州立大学的 CropSyst 和澳大利亚联邦科学与工业研究组织 (CSIRO) 的农业生产系统模拟器 (APSIM)。
从历史上看,这些模型主要用于小区域(基于点的)模拟,其中所有变量都是已知的。 对于大区域研究(景观尺度,例如,整个区域或国家层面),土壤和气候数据需要按比例放大或缩小到感兴趣的分辨率,这意味着增加不确定性。 这有两个主要原因: 1) 很难创建和/或获取对高分辨率、地理参考、网格化数据集的访问权限; 以及 2) 作物建模软件最常见的安装是在最终用户的台式机或工作站上,这些机器通常运行受支持的 Microsoft Windows 版本之一(系统建模人员倾向于使用这些工具的 GUI 功能来准备和运行模拟,然后这些模拟受到所用硬件的计算能力的限制)。
新西兰有几个皇家研究所,为该国经济许多不同重要领域提供科学研究,包括土地保护研究所、国家水与大气研究所 (NIWA) 以及新西兰植物与食品研究所。 在一个联合项目中,这些组织贡献了与该国土壤、地形、气候和作物模型相关的数据集。 我们希望创建一个分析框架,该框架使用 APSIM 运行足够的模拟,以涵盖气候变化问题(超过 100 年的气候变化数据)在整个新西兰的相关时间尺度内,空间分辨率约为 25 平方公里。 我们谈论的是数百万次模拟,每次模拟在单个 CPU 核心上至少需要 10 分钟才能完成。 如果我们使用标准台式机,那么可能更快的方法就是站在外面看看会发生什么。
进入 HPC
高性能计算 (HPC) 是指使用并行处理来高效、可靠且快速地运行程序。 通常,这意味着利用跨多台主机的批处理,每个单独的进程只处理少量数据,并使用作业调度程序来协调它们。
并行计算可能意味着分布式计算,其中每个处理线程需要在任务之间(特别是中间结果)与其他线程通信,或者它可能是“令人尴尬的并行”,其中不需要这种通信。 当处理后者时,整体性能会随着可用容量的增加而线性增长。
幸运的是,作物建模是一个令人尴尬的并行问题:无论您有多少数据或多少变量,每个变化的变量都意味着需要运行一次完整的模拟。 而且由于模拟彼此独立,因此您可以运行与您拥有的 CPU 一样多的模拟。
解决依赖地狱
APSIM 是一套复杂的软件。 它的代码库由过去三十年中用多种不同编程语言编写并紧密集成的模块组成。 该应用程序通过利用 Mono Project 框架实现了 Windows 和 GNU/Linux 操作系统之间的可移植性,但要在 Linux 环境中运行它,需要大量的外部依赖项和解决方法,这使得实现变得不简单。
构建和安装文档很少,并且确实存在的说明也针对 Ubuntu Desktop 版本。 几个必需的依赖项没有文档记录,并且构建过程有时依赖于 binfmt_misc 内核模块来允许直接执行链接到 Mono 库的 .exe 文件(而不是调用 mono file.exe),但它这样做是不一致的(这自那以后已在上游修复)。 更令人困惑的是,一些 .exe 文件是 Mono 程序集,而另一些是本机 (libc) 二进制文件(这样做是为了避免操作系统平台之间可执行文件名称的差异)。 最后,Linux 构建是由开发人员“内部”按需创建的,但由于外部用户缺乏兴趣,因此没有公开可访问的自动化构建。
所有这些可能在一个组织内部有效,但这使得 APSIM 在其他环境中难以采用。 HPC 集群倾向于标准化为一种 Linux 发行版(例如,Red Hat Enterprise Linux、CentOS、Ubuntu 等)和作业调度程序(例如,PBS、HTCondor、Torque、SGE、Platform LSF、SLURM 等),并且可以实现不同的存储和网络架构、网络配置、用户身份验证和授权策略等。 因此,可用的软件、版本以及它们的集成方式都高度依赖于环境。 像 OpenHPC 这样的项目旨在为这种情况提供一些合理性,但现实情况是,大多数 HPC 集群本质上都是定制的,根据组织的需求量身定制。
解决这些问题的一个简单方法是引入容器化技术。 这不应该令人惊讶(毕竟它在本文的标题中)。 容器允许创建可以运行的独立、自给自足的工件,而无需在任何支持运行它们的环境中进行更改。 但是,从“可重复研究”的角度来看,容器还提供了额外的优势:软件容器可以以可重复的方式创建,一旦创建,生成的容器镜像既是可移植的又是不可变的。
-
可重复性: 一旦编写了遵循最佳实践的容器定义文件(例如,确保显式定义了已安装的软件版本),就可以以确定性的方式创建相同的生成容器镜像。
-
可移植性: 当管理员创建容器镜像时,他们可以编译、安装和配置所有将需要的软件,并包括运行它们所需的任何外部依赖项或库,一直到 Linux 发行版本身。 在此过程中,除了硬件之外,无需针对执行环境进行任何操作。 创建后,容器镜像可以作为独立工件分发。 这干净地将特定软件的构建和安装阶段与执行该软件时的运行时阶段分开。
-
不可变性: 构建完成后,容器镜像就不可变了。 也就是说,如果不创建新镜像,就无法更改其内容并持久保存它们。
这些属性使得可以捕获处理期间使用的软件堆栈的精确状态,并将其与原始数据一起分发,以便在不同的环境中复制分析,即使该环境中使用的 Linux 发行版与容器镜像内部使用的发行版不匹配也是如此。
Docker
虽然操作系统级虚拟化并非新技术,但正是由于 Docker,它才变得越来越流行。 Docker 提供了一种以简单方式开发、部署和运行软件容器的方法。
APSIM 容器镜像的第一个迭代是在 Docker 中实现的,部分复制了开发人员记录的构建环境。 这被用作容器化和运行应用程序可行性的概念验证。 第二次迭代引入了多阶段构建:一种创建容器镜像的方法,该方法允许将构建阶段与安装阶段分开。 这种分离很重要,因为它减小了生成的容器镜像的最终大小,这将不包括仅在构建时才需要的任何依赖项。 Docker 容器并不特别适合多租户 HPC 环境。 有三个主要事项需要考虑
1. 数据所有权
容器镜像通常不存储与企业身份验证目录(例如,Active Directory、LDAP 等)集成所需的配置,因为这会降低可移植性。 相反,用户信息通常直接在镜像中显式硬编码(当没有硬编码时,默认情况下使用 root)。 当容器启动时,包含的进程将使用此硬编码的身份运行(请记住,默认情况下使用 root)。 结果是,容器化进程创建的输出数据由一个可能仅存在于容器镜像内部的用户拥有。 而不是启动容器的用户(还有,我提到默认情况下使用 root 吗?)。
针对此问题的一种可能的解决方法是在容器启动时覆盖运行时用户(使用 docker run -u… 标志)。 但是,这为用户增加了额外的复杂性,用户现在必须了解用户身份 (UID)、POSIX 所有权和权限、docker run 命令的正确语法,以及找到他们 UID、组标识符 (GID) 以及他们可能需要的任何其他组的正确值。 所有这些都是为了那些只想完成一些科学研究的人。
还值得注意的是,此方法并非每次都有效。 并非所有应用程序都乐于以任意用户或系统中数据库中不存在的用户身份运行(例如,/etc/passwd 文件)。 这些是边缘情况,但它们确实存在。
2. 访问持久存储
容器镜像仅包含应用程序运行所需的文件。 它们通常不包含要由应用程序处理的输入或原始数据。 默认情况下,当容器镜像被实例化时(即,当容器启动时),呈现给容器化应用程序的文件系统将仅显示容器镜像中存在的那些文件和目录。 为了访问输入或原始数据,最终用户必须显式地将所需挂载点从主机服务器映射到容器中文件系统内的路径(通常使用绑定挂载)。 对于 Docker,这些“卷挂载”不可能全局预配置,并且映射必须在容器启动时在每个容器的基础上完成。 这不仅增加了运行应用程序所需命令的复杂性,而且还引入了另一个不良影响……
3. 计算主机安全
以任意用户身份启动进程的能力以及将主机服务器中的任意文件或目录映射到正在运行的容器的文件系统的能力是 Docker 提供给操作员的几个强大功能中的两个。 但它们之所以可能,是因为在 Docker 采用的安全模型中,运行容器的守护程序必须以 root 权限在主机上启动。 因此,有权访问 Docker 守护程序的最终用户最终拥有等同于主机 root 访问权限的权限。 这引入了安全问题,因为它违反了 最小权限原则。 恶意行为者可以执行超出其初始授权范围的操作,但最终用户也可能在无恶意意图的情况下无意中损坏或销毁数据。
解决此问题的一种可能方法是实现用户命名空间。 但在实践中,这些命名空间维护起来很麻烦,尤其是在用户身份集中在企业目录中的企业环境中。
Singularity
为了解决这些问题,APSIM 容器的第三次迭代是使用 Singularity 实现的。 Singularity Community 于 2016 年发布,是一个专门为科学和 HPC 环境设计的开源容器平台。 “Singularity 容器内的用户与容器外的用户是同一用户” 是 Singularity 的决定性特征之一。 它允许最终用户以他或她自己的身份在容器镜像内部运行命令。 相反,它不允许在启动容器时冒充其他用户。
Singularity 方法的另一个优点是容器镜像在磁盘上的存储方式。 对于 Docker,容器镜像存储在多个单独的“层”中,Docker 守护程序需要在容器运行时覆盖和展平这些层。 当多个容器镜像重用同一层时,只需要该层的一个副本即可重新创建运行时容器的文件系统。 这提高了存储的利用效率,但当涉及到分发和检查容器镜像时,它确实增加了一些复杂性,因此 Docker 提供了特殊的命令来执行此操作。 对于 Singularity,整个执行环境都包含在一个单独的可执行文件中。 当多个镜像具有相似内容时,这会引入重复,但它使这些镜像的分发变得微不足道,因为它现在可以使用传统的文件传输方法、协议和工具来完成。
Docker 容器配方文件(即 Dockerfile 和相关资产)可用于重新创建为项目构建的容器镜像。 Singularity 允许本机导入和运行 Docker 容器,因此相同的文件可以用于两个引擎。
一天的工作
为了用一个实际示例来说明上述内容,让我们把自己放在计算科学家的位置上。 为了不单独挑出任何人,假设您想使用 ToolA,它处理输入文件并创建包含文件统计信息的输出。 在请求系统管理员帮助您之前,您决定在本地桌面上测试该工具,看看它是否有效。
ToolA 具有简单的语法。 这是一个单独的二进制文件,它将一个或多个文件名作为命令行参数,并接受 -o {json|yaml} 标志来更改结果的格式。 输出存储在与输入文件相同的路径中。 例如
$ ./ToolA file1 file2
$ ls
file1 file1.out file2 file2.out ToolA
您有数千个文件要处理,但即使 ToolA 使用多线程来独立处理文件,您的机器中也没有数千个 CPU 核心。 您必须使用集群的作业调度程序。 在规模上执行此操作的最简单方法是启动与您需要处理的文件一样多的作业,每个作业使用一个 CPU 线程。 您测试新方法
$ export PATH=$(pwd):${PATH}
$ cd ~/input/files/to/process/samples
$ ls -l | wc -l
38
$ # we will set this to the actual qsub command when we run in the cluster
$ qsub=""
$ for myfiles in *; do $qsub ToolA $myfiles; done
...
$ ls -l | wc -l
75
太棒了。 是时候去烦系统管理员并在集群中安装 ToolA 了。
事实证明,ToolA 很容易在 Ubuntu Bionic 中安装,因为它已经在仓库中了,但在我们的 HPC 集群使用的 CentOS 7 中编译却是一场噩梦。 因此,系统管理员决定创建一个 Docker 容器镜像并将其推送到公司的注册表。 他还在恳求您不要行为不端后将您添加到 docker 组。
您查找 Docker 命令的语法,并决定在提交可能失败的数千个作业之前进行一些测试运行。
$ cd ~/input/files/to/process/samples
$ rm -f *.out
$ ls -l | wc -l
38
$ docker run -d registry.example.com/ToolA:latest file1
e61d12292d69556eabe2a44c16cbd27486b2527e2ce4f95438e504afb7b02810
$ ls -l | wc -l
38
$ ls *out
$
啊,当然,您忘记挂载文件了。 让我们再试一次。
$ docker run -d -v $(pwd):/mnt registry.example.com/ToolA:latest /mnt/file1
653e785339099e374b57ae3dac5996a98e5e4f393ee0e4adbb795a3935060acb
$ ls -l | wc -l
38
$ ls *out
$
$ docker logs 653e785339
ToolA: /mnt/file1: Permission denied
您向系统管理员寻求帮助,他告诉您 SELinux 阻止进程访问文件,并且您的 docker run 中缺少一个标志。 您不知道 SELinux 是什么,但您记得在文档的某个地方提到过它,因此您查找它并再次尝试
$ docker run -d -v $(pwd):/mnt:z registry.example.com/ToolA:latest /mnt/file1
8ebfcbcb31bea0696e0a7c38881ae7ea95fa501519c9623e1846d8185972dc3b
$ ls *out
$
$ docker logs 8ebfcbcb31
ToolA: /mnt/file1: Permission denied
您回到系统管理员那里,他告诉您容器默认使用 UID 为 1000 的 myuser,但您的文件仅对您可读,并且您的 UID 不同。 因此,您做了您知道是不良做法的事情,但您已经厌倦了:您在再次尝试之前运行 chmod 777 file1。 您也厌倦了必须复制和粘贴哈希值,因此您在 docker run 中添加了另一个标志
$ docker run -d --name=test -v $(pwd):/mnt:z registry.example.com/ToolA:latest /mnt/file1
0b61185ef4a78dce988bb30d87e86fafd1a7bbfb2d5aea2b6a583d7ffbceca16
$ ls *out
$
$ docker logs test
ToolA: cannot create regular file '/mnt/file1.out': Permission denied
唉,至少这次您得到了不同的错误。 进步! 您的友好系统管理员告诉您,容器中的进程将没有对您的目录的写入权限,因为身份不匹配,并且您需要在命令行中添加更多标志。
$ docker run -d -u $(id -u):$(id -g) --name=test -v $(pwd):/mnt:z registry.example.com/ToolA:latest /mnt/file1
docker: Error response from daemon: Conflict. The container name "/test" is already in use by container "0b61185ef4a78dce988bb30d87e86fafd1a7bbfb2d5aea2b6a583d7ffbceca16". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.
$ docker rm test
$ docker run -d -u $(id -u):$(id -g) --name=test -v $(pwd):/mnt:z registry.example.com/ToolA:latest /mnt/file1
06d5b3d52e1167cde50c2e704d3190ba4b03f6854672cd3ca91043ad23c1fe09
$ ls *out
file1.out
$
成功! 现在我们只需要用作业调度程序使用的命令包装我们的命令,然后再次用我们的 for 循环包装所有这些命令。
$ cd ~/input/files/to/process
$ ls -l | wc -l
934752984
$ for myfiles in *; do qsub -q short_jobs -N "toola_${myfiles}" docker run -d -u $(id -u):$(id -g) --name="toola_${myfiles}" -v $(pwd):/mnt:z registry.example.com/ToolA:latest /mnt/${myfiles}; done
现在这有点笨拙,不是吗? 让我们看看使用 Singularity 如何简化它。
$ cd ~
$ singularity pull --name ToolA.simg docker://registry.example.com/ToolA:latest
$ ls
input ToolA.simg
$ ./ToolA.simg
Usage: ToolA [-o {json|yaml}] <file1> [file2...fileN]
$ cd ~/input/files/to/process
$ for myfiles in *; do qsub -q short_jobs -N "toola_${myfiles}" ~/ToolA.simg ${myfiles}; done
我还需要多说什么吗?
这之所以有效,是因为默认情况下,Singularity 容器以启动它们的用户身份运行。 没有后台守护程序,因此不允许权限提升。 Singularity 还默认绑定挂载一些目录($PWD、$HOME、/tmp、/proc、/sys 和 /dev)。 管理员可以配置其他目录,这些目录也在全局(即主机)基础上默认挂载,并且最终用户(可选)也可以在运行时绑定任意目录。 当然,标准的 Unix 权限适用,因此这仍然不允许无限制地访问主机文件。
但是气候变化呢?
哦! 当然。 回到正题。 我们决定按项目分解我们需要运行的大量模拟。 然后每个项目都可以专注于特定的作物、特定的地理区域或不同的作物管理技术。 在完成特定项目的所有模拟后,它们会被整理到一个 MariaDB 数据库中,并使用 RStudio Shiny Web 应用程序进行可视化。

原型 Shiny 应用程序屏幕截图显示了全国范围内运行的气候变化对青贮玉米的影响,比较了当前和本世纪末的情景。
该应用程序允许我们比较两种不同的情景(参考与替代),用户可以通过选择与气候(包括当前气候和本世纪中叶和本世纪末的气候变化预测)、土壤和特定管理技术(如灌溉或施肥)相关的变量组合来构建这些情景。 结果以栅格值或差异(每个像素的结果的平均值或变异系数)及其在感兴趣区域的分布显示。
上面的屏幕截图显示了一个原型全国范围内在“可耕地”上运行的示例,其中我们比较了基线(1985-2005 年)与未来气候变化(2085-2100 年)最极端排放情景下的青贮玉米生物量。 在此示例中,我们没有考虑管理技术的任何变化,例如调整播种日期。 我们看到,南半球产量的大部分负面影响发生在北部地区,而最南端则显示出积极的反应。 当然,我们建议(并且您会期望)农民在今年年初开始适应温暖的气温并做出相应的反应(例如,提前播种,这将减少负面影响并增强积极影响)。
下一步
框架到位后,剩下的就是繁重的工作。 运行所有模拟! 当然,说起来容易做起来难。 我们的内部集群是一个共享资源,我们必须与几个其他项目和团队竞争容量。
计划进行更多工作,以进一步概括我们如何在计算资源之间分配作业,以便我们可以利用我们可以获得的任何容量(包括公共云,如果该项目获得足够的额外资金)。 这意味着成为与作业调度程序无关的,并解决数据重力问题。
正在进行的工作还在进一步改进 Web 应用程序的 UI 和 UX 方面,直到我们确信它可以发布给决策者和其他感兴趣的各方。
如果您从科学的角度对我们的工作感兴趣,请联系我,我会让您与项目负责人联系。 对于所有其他咨询,您也可以联系我,我会尽力提供帮助。
Eric Burgueño 将在 1 月 21 日至 25 日在新西兰基督城的 linux.conf.au 会议上介绍 使用容器分析气候变化和土壤对新西兰农作物的影响。
3 条评论