容器
容器是一种封装环境,包含操作系统、库和软件。例如,如果您的主机运行的是Centos系统,您可以在其中运行一个隔离的Ubuntu 18.04容器。从高层次来看,将容器视为一个程序或二进制文件是很有帮助的。
为了提升可重复性和可移植性,最佳实践是为WDL任务定义运行容器——这能确保在不同系统上运行同一任务时使用完全相同的软件环境。
Docker镜像是目前最常见的容器格式,但对于某些系统来说直接运行Docker本身并不合适,因此Cromwell可配置为支持多种替代方案。
先决条件
本教程页面需要先完成以下前置教程:
目标
在本教程结束时,您将熟悉容器技术以及如何配置Cromwell以独立使用这些技术,或与作业调度程序一起使用。
在工作流中指定容器
容器是在每个任务级别上指定的,在WDL中可以通过在runtime部分指定docker标签来实现。例如,以下脚本应该在ubuntu:latest容器中运行:
task hello_world {
String name = "World"
command {
echo 'Hello, ${name}'
}
output {
File out = stdout()
}
runtime {
docker: 'ubuntu:latest'
}
}
workflow hello {
call hello_world
}
Docker
Docker 是一种流行的容器技术,Cromwell 和 WDL 原生支持该技术。
本地后端上的Docker
在单台机器(笔记本电脑或服务器)上,只要安装了Docker,无需额外配置即可运行docker。
您可以从Docker Hub安装适用于Linux、Mac或Windows的Docker
云端Docker
强烈建议您为将在云后端运行的任务提供Docker镜像,事实上大多数云服务提供商都要求这样做。
虽然可以使用其他容器引擎替代,但如果支持Docker则不建议这样做。
HPC上的Docker
Docker允许运行用户获得超级用户权限,这被称为Docker守护进程攻击面。在高性能计算(HPC)和多用户环境中,Docker建议"只应允许受信任的用户控制您的Docker守护进程"。
出于这个原因,本教程还将探讨其他支持使用Docker容器运行工作流的可重现性和简便性的技术:Singularity和udocker。
Singularity
Singularity是一种专为HPC系统设计的容器技术,同时确保Docker无法提供的适当安全级别。
安装
在HPC系统上配置Cromwell之前,您需要先安装Singularity,相关文档请参阅此处。
为了获得Singularity的全部功能特性,强烈建议由root用户安装Singularity,并启用setuid位(如此文档所述)。
这意味着您可能需要请系统管理员代为安装。
由于singularity最好需要setuid权限,管理员可能对授予Singularity此特权有所顾虑。
如果遇到这种情况,您可以考虑将这封信转发给管理员。
如果您无法以这些权限安装Singularity,可以尝试用户安装。 如果是这种情况,您需要修改Cromwell配置以在"沙盒"模式下运行,文档的这一部分对此进行了说明。
为Cromwell配置Singularity
安装Singularity后,您需要修改Cromwell配置文件中backend.providers内的config块。特别需要注意的是,该配置块包含一个名为submit-docker的键,其中包含当作业需要使用Docker镜像运行时执行的脚本。如果作业未指定Docker镜像,则将使用常规的submit配置块。
由于配置需要更多关于执行环境的信息,请参阅下面的本地和作业调度器部分以获取示例配置。
本地环境
在本地后端环境中,您需要配置Cromwell使用一个不同的submit-docker脚本来启动Singularity而非docker。Singularity要求docker镜像必须添加docker://前缀。
使用容器可以隔离脚本允许交互的文件系统,因此我们将当前工作目录绑定为${docker_cwd},并使用容器特定的脚本路径${docker_script}。
一个用于Singularity的示例提交脚本如下:
singularity exec --containall --bind ${cwd}:${docker_cwd} docker://${docker} ${job_shell} ${docker_script}
由于Singularity exec命令不会生成任务ID,我们除了docker-submit脚本外,还必须在提供者部分包含run-in-background标签。由于Cromwell会监控rc文件的存在,run-in-background选项有个注意事项:我们需要确保Singularity容器成功完成,否则工作流可能会无限期挂起。
为确保容器内的可重现性和隔离环境,--containall是一个重要功能。默认情况下,Singularity会挂载用户的主目录并导入用户环境变量以及其他一些使Singularity在交互式shell中更易用的配置。遗憾的是,主目录中的设置和用户环境变量可能会影响所用工具的运行结果,这意味着不同用户可能得到不同结果。因此,为确保使用Singularity时的结果可重现性,应当使用--containall标志。这将确保环境被清理且不会挂载HOME目录。
综合以上内容,我们为本地环境准备了一个基础配置示例:
include required(classpath("application"))
backend {
default: singularity
providers: {
singularity {
# The backend custom configuration.
actor-factory = "cromwell.backend.impl.sfs.config.ConfigBackendLifecycleActorFactory"
config {
run-in-background = true
runtime-attributes = """
String? docker
"""
submit-docker = """
singularity exec --containall --bind ${cwd}:${docker_cwd} docker://${docker} ${job_shell} ${docker_script}
"""
}
}
}
}
作业调度器
要在作业调度器上运行Singularity,需要将singularity命令作为封装命令传递给调度器。
例如,在SLURM中,我们可以按照SLURM文档中说明使用常规SLURM配置,但会添加一个submit-docker代码块,当任务被标记为docker容器时执行。
构建此模块时需要注意以下几点:
- 确保已加载Singularity(并在PATH中)。例如如果安装了module,可以调用module load Singularity。如果集群管理员提供了Singularity模块。或者可以直接修改PATH变量,亦或在配置中直接使用/path/to/singularity。
- 我们应将工作节点视为无法稳定访问互联网或构建环境,因此需要在任务提交到集群前拉取容器。
- 建议使用Singularity缓存,这样相同镜像只需拉取一次。确保将SINGULARITY_CACHEDIR环境变量设置为工作节点可访问的文件系统位置!
- 如果使用缓存,需要确保由Cromwell启动的提交进程不会同时向同一缓存拉取,这可能导致缓存损坏。可以通过flock实现文件锁,并在作业提交前拉取镜像来避免。flock和pull命令需要放在提交命令之前,这样所有pull命令都在同一节点执行,这是文件锁正常工作的必要条件。
- 如上所述,--containall标志对于可重现性非常重要。
submit-docker = """
# Make sure the SINGULARITY_CACHEDIR variable is set. If not use a default
# based on the users home.
if [ -z $SINGULARITY_CACHEDIR ];
then CACHE_DIR=$HOME/.singularity/cache
else CACHE_DIR=$SINGULARITY_CACHEDIR
fi
# Make sure cache dir exists so lock file can be created by flock
mkdir -p $CACHE_DIR
LOCK_FILE=$CACHE_DIR/singularity_pull_flock
# Create an exclusive filelock with flock. --verbose is useful for
# for debugging, as is the echo command. These show up in `stdout.submit`.
flock --verbose --exclusive --timeout 900 $LOCK_FILE \
singularity exec --containall docker://${docker} \
echo "successfully pulled ${docker}!"
# Submit the script to SLURM
sbatch \
[...]
--wrap "singularity exec --containall --bind ${cwd}:${docker_cwd} $IMAGE ${job_shell} ${docker_script}"
"""
综上所述,一个完整的SLURM + Singularity配置可能如下所示:
backend {
default = slurm
providers {
slurm {
actor-factory = "cromwell.backend.impl.sfs.config.ConfigBackendLifecycleActorFactory"
config {
runtime-attributes = """
Int runtime_minutes = 600
Int cpus = 2
Int requested_memory_mb_per_core = 8000
String? docker
"""
submit = """
sbatch \
--wait \
-J ${job_name} \
-D ${cwd} \
-o ${out} \
-e ${err} \
-t ${runtime_minutes} \
${"-c " + cpus} \
--mem-per-cpu=${requested_memory_mb_per_core} \
--wrap "/bin/bash ${script}"
"""
submit-docker = """
# Make sure the SINGULARITY_CACHEDIR variable is set. If not use a default
# based on the users home.
if [ -z $SINGULARITY_CACHEDIR ];
then CACHE_DIR=$HOME/.singularity/cache
else CACHE_DIR=$SINGULARITY_CACHEDIR
fi
# Make sure cache dir exists so lock file can be created by flock
mkdir -p $CACHE_DIR
LOCK_FILE=$CACHE_DIR/singularity_pull_flock
# Create an exclusive filelock with flock. --verbose is useful for
# for debugging, as is the echo command. These show up in `stdout.submit`.
flock --verbose --exclusive --timeout 900 $LOCK_FILE \
singularity exec --containall docker://${docker} \
echo "successfully pulled ${docker}!"
# Submit the script to SLURM
sbatch \
--wait \
-J ${job_name} \
-D ${cwd} \
-o ${cwd}/execution/stdout \
-e ${cwd}/execution/stderr \
-t ${runtime_minutes} \
${"-c " + cpus} \
--mem-per-cpu=${requested_memory_mb_per_core} \
--wrap "singularity exec --containall --bind ${cwd}:${docker_cwd} $IMAGE ${job_shell} ${docker_script}"
"""
kill = "scancel ${job_id}"
check-alive = "squeue -j ${job_id}"
job-id-regex = "Submitted batch job (\\d+).*"
}
}
}
}
未设置Setuid
此外,如果您或您的系统管理员无法为singularity授予setuid权限,您需要进一步修改配置以确保使用沙盒镜像:
submit-docker = """
[...]
# Build the Docker image into a singularity image
# We don't add the .sif file extension because sandbox images are directories, not files
DOCKER_NAME=$(sed -e 's/[^A-Za-z0-9._-]/_/g' <<< ${docker})
IMAGE=${cwd}/$DOCKER_NAME
singularity build --sandbox $IMAGE docker://${docker}
# Now submit the job
# Note the use of --userns here
sbatch \
[...]
--wrap "singularity exec --userns --bind ${cwd}:${docker_cwd} $IMAGE ${job_shell} ${docker_script}"
"""
Singularity 缓存
默认情况下,Singularity会将拉取的Docker镜像缓存到您主目录的~/.singularity中。
但是,如果您需要与其他用户共享Docker镜像,或者用户目录空间有限,可以通过在.bashrc文件中或在submit-docker代码块开头导出SINGULARITY_CACHEDIR变量来重定向缓存位置。
export SINGULARITY_CACHEDIR=/path/to/shared/cache
有关Singularity缓存的更多信息,请参阅Singularity 2缓存文档(该文档尚未针对Singularity 3进行更新)。
udocker
udocker 是一款旨在"在用户空间无需root权限即可运行简单docker容器"的工具。
本质上,udocker提供了一个模拟docker的命令行界面,并通过四种不同的容器后端实现这些命令:
- PRoot
- Fakechroot
- runC
- Singularity
安装
udocker可以在无需任何root权限的情况下安装。更多信息请参考udocker的安装文档此处。
配置
(截至2019-02-18) udocker暂不支持通过摘要值查找docker容器,因此您需要确保禁用hash-lookup功能。更多细节请参阅此章节。
要在本地环境中配置udocker,您必须将提供者的配置标记为run-in-background并更新submit-docker以使用udocker:
run-in-background = true
submit-docker = """
udocker run -v ${cwd}:${docker_cwd} ${docker} ${job_shell} ${docker_script}
"""
使用像SLURM这样的作业队列,你只需要像我们处理Singularity那样,将这个脚本封装在sbatch提交中:
submit-docker = """
# Pull the image using the head node, in case our workers don't have network access
udocker pull ${docker}
sbatch \
-J ${job_name} \
-D ${cwd} \
-o ${cwd}/execution/stdout \
-e ${cwd}/execution/stderr \
-t ${runtime_minutes} \
${"-c " + cpus} \
--mem-per-cpu=${requested_memory_mb_per_core} \
--wrap "udocker run -v ${cwd}:${docker_cwd} ${docker} ${job_shell} ${docker_script}"
"""
缓存
udocker 将镜像缓存于单一目录中,默认路径为 ~/.udocker,这意味着缓存是按用户隔离的。
不过与 Singularity 类似,若需与项目组成员共享缓存,可通过以下方式覆盖默认缓存目录位置:
* 使用此处所述的配置文件,添加类似 topdir = "/path/to/cache" 的配置项
* 设置环境变量 $UDOCKER_DIR
详细配置
Cromwell 使用容器时的行为可以通过其他几个选项进行修改。
强制执行容器要求
您可以通过不在提供者部分包含submit块来强制使用容器。
但请注意,这两个代码块中一些插值变量(${stdout}、${stderr})有所不同。
Docker摘要
每个Docker仓库都包含多个标签(tags),用于指向特定类型的最新镜像。
例如,当您使用docker run image运行普通Docker镜像时,实际上运行的是image:latest,即该镜像的latest标签。
然而,默认情况下Cromwell会请求并使用镜像的sha哈希值来运行,而非标签。
这种策略实际上更可取,因为它能确保每次任务或工作流的执行都使用完全相同的镜像版本,但某些引擎如udocker不支持此功能。
如果您正在使用udocker或希望禁用基于哈希的镜像引用,可以设置以下配置选项:
docker.hash-lookup.enabled = false
注意:禁用哈希查找后,使用浮动标签的任何容器将无法使用调用缓存功能。
Docker根目录
如果您想更改容器内的根目录(任务放置输入和输出文件的位置),可以编辑以下选项:
backend {
providers {
LocalExample {
actor-factory = "cromwell.backend.impl.sfs.config.ConfigBackendLifecycleActorFactory"
config {
# Root directory where Cromwell writes job results in the container. This value
# can be used to specify where the execution folder is mounted in the container.
# it is used for the construction of the docker_cwd string in the submit-docker
# value above.
dockerRoot = "/cromwell-executions"
}
}
}
}
Docker配置块
以下是可放入配置文件的进一步Docker配置选项。 如需最新参数列表,请参考示例配置文件 以及特定后端提供程序示例。
docker {
hash-lookup {
# Set this to match your available quota against the Google Container Engine API
#gcr-api-queries-per-100-seconds = 1000
# Time in minutes before an entry expires from the docker hashes cache and needs to be fetched again
#cache-entry-ttl = "20 minutes"
# Maximum number of elements to be kept in the cache. If the limit is reached, old elements will be removed from the cache
#cache-size = 200
# How should docker hashes be looked up. Possible values are "local" and "remote"
# "local": Lookup hashes on the local docker daemon using the cli
# "remote": Lookup hashes on docker hub, gcr, gar, quay
#method = "remote"
}
}
最佳实践
镜像版本
为流水线阶段选择镜像版本时,强烈建议使用哈希值而非标签以确保可复现性。例如,在WDL中您可以这样操作:
runtime {
docker: 'ubuntu:latest'
}
但你应该这样做:
runtime {
docker: 'ubuntu@sha256:7a47ccc3bbe8a451b500d2b53104868b46d60ee8f5b35a24b41a86077c650210'
}
你可以使用docker images --digests命令查找镜像的sha256值
笔记
Cromwell如何知道作业或容器何时完成?
Cromwell通过检测rc(返回码)文件的存在来判断任务是否成功或失败。该rc文件是在执行目录中作为script的一部分生成的,脚本在运行时会被组装。这一点很重要,因为如果脚本执行成功但容器没有终止,Cromwell将继续执行工作流,而容器会持续占用系统资源。
在上述配置中:
- singularity: exec模式不会在后台运行容器
Cromwell: 后台运行
通过启用Cromwell的后台运行模式,您可以移除kill、check-alive和job-id-regex模块的需求,这会在运行工作流时禁用某些安全检查:
- 如果启动容器或执行脚本时出现错误,Cromwell可能无法识别该错误并挂起。例如,当容器尝试超出其分配的资源(内存耗尽)时可能会发生这种情况;容器守护进程可能会在不完成脚本的情况下终止容器。
- 如果您中止工作流(通过尝试关闭Cromwell或发出中止命令),Cromwell将无法获取容器执行的引用,因此无法终止该容器。
这仅在本地环境中是必要的,因为那里没有作业管理器来控制这一点。但如果您的容器技术可以向标准输出发出标识符,那么您就可以移除后台运行标志。
后续步骤
恭喜您提升了工作流的可重复性!您可能会对以下基于云的教程感兴趣,它们可以帮助您在完全不同的环境中测试工作流(并确保结果一致):