容器

容器是一种封装环境,包含操作系统、库和软件。例如,如果您的主机运行的是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的后台运行模式,您可以移除killcheck-alivejob-id-regex模块的需求,这会在运行工作流时禁用某些安全检查:

  • 如果启动容器或执行脚本时出现错误,Cromwell可能无法识别该错误并挂起。例如,当容器尝试超出其分配的资源(内存耗尽)时可能会发生这种情况;容器守护进程可能会在不完成脚本的情况下终止容器。
  • 如果您中止工作流(通过尝试关闭Cromwell或发出中止命令),Cromwell将无法获取容器执行的引用,因此无法终止该容器。

这仅在本地环境中是必要的,因为那里没有作业管理器来控制这一点。但如果您的容器技术可以向标准输出发出标识符,那么您就可以移除后台运行标志。

后续步骤

恭喜您提升了工作流的可重复性!您可能会对以下基于云的教程感兴趣,它们可以帮助您在完全不同的环境中测试工作流(并确保结果一致):