亚马逊AWS官方博客

基于 Amazon ECS 的并行批处理任务解决方案

很多企业都会有运行批处理任务的需求,而且这些批处理任务可能都是不同类型的工作,比如有的是处理数据的,有的是进行媒体编码的,有的是进行机器学习模型训练的。这些批处理任务通常都是通过消息进行触发,也就是说需要处理数据或者其他任务的时候,程序发起方会发送一个或者多个消息到一个特定的队列里,然后由监控该队列的应用程序发起任务处理。

本文描述了一种基于容器化的,在 Amazon ECS 平台上搭建的、并行批处理任务的、通用的解决方案。

解决方案概述

该方案如下图所示。

在该方案中,我们会启动一个 Amazon ECS 集群,并关联 auto scaling 组。然后在该平台上,我们可以借助一个开源工具:mapbox/watchbot(以下简称 watchbot)来搭建任务处理平台。该平台与 Amazon ECS 无缝集成,结合 Amazon ECR 和 ECS 服务,使得用户只需要关注任务处理程序即可,其他的任务调度,高可用性管理等工作,都可以由 watchbot 来完成。Watchbot 的处理流程如下图所示。

在使用 watchbot 之前,用户把自己的任务处理程序(可以是任何语言编写的程序)封装到 docker 镜像中,并上传到 Amazon ECR 里。然后在部署了watchbot 以后,会在 Amazon ECS 集群里启动一个 ECS 服务,该服务会启动1到多个 watcher 容器(具体几个 watcher 容器可以由用户自己配置)。该 watcher 容器会监控一个预先创建好的 Amazon SQS 队列,一旦该队列里被放入了消息,watcher 容器就会把该消息设置为不可见,然后启动预先配置好的 ECS 任务或者也可以叫做任务容器(也就是上图中的蓝色方块),任务容器就会从 Amazon ECR 里拉取包含了用户业务处理程序的 docker 镜像,并把消息作为参数传给任务容器,然后在任务容器里根据传入的消息参数,进行相应的逻辑处理。任务容器执行完以后,可以有4种退出状态:

  1. 退出代码为0:表示任务容器执行结束并正常退出,对应的消息会被 watcher 容器从队列里删除。
  2. 退出代码为3:表示该消息被任务容器拒绝,对应的消息会被 watcher 容器从队列里删除,同时发送一个通知到 SNS 主题。
  3. 退出代码为4:表示该消息不被任务容器处理,对应的消息会被 watcher 容器转移到对应的 error 队列。
  4. 退出代码为其他值:表示任务容器在处理该消息时发生被捕获的异常并主动退出,对应的消息会被 watcher 容器转移到对应的 error 队列,并发送一个通知到 SNS 主题。

如果任务容器在执行过程中出现未被捕获的故障而导致异常退出,则 watcher 容器会再次启动该任务容器,如果任务容器反复执行失败导致异常退出,则 watcher 容器在尝试一定次数以后,会放弃并把该消息放入对应的 error 队列。

架构部署

我们以 us-east-1 region 为例,一步一步的说明如何部署整个并行批处理任务解决方案。注意在部署之前,需要在你的电脑上配置 AWS credentials,并具有管理 Amazon ECR 和 Amazon ECS 的权限。或者也可以在一台 EC2 实例上完成部署工作,那么可以在该 EC2 实例上关联一个角色,并为角色赋予管理 Amazon ECR 和 ECS 的权限。

  1. 创建 Amazon ECS 集群,具体步骤参考:
    https://docs.aws.amazon.com/AmazonECS/latest/developerguide/create_cluster.html
    该 ECS 集群可以与Auto scaling 组结合实现根据业务负载自动伸缩。有关 Auto scaling 的内容请参考官方文档:
    https://docs.aws.amazon.com/autoscaling/plans/userguide/what-is-aws-auto-scaling.html
    然后根据官方文档创建 ECR 仓库:
    https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-create.html
    假设该仓库的名字叫做 ecs-watchbot,则该 ECR 的 URI 为:123456789012.dkr.ecr.us-east-1.amazonaws.com/ecs-watchbot,这里的 123456789012 为用户的 AWS 账号。
  2. 从 github下载 watchbot 代码:git clone https://github.com/mapbox/ecs-watchbot.git
  3. 进入 ecs-watchbot 子目录,创建 watchbot 容器镜像,注意下面命令中别忘了最后一个“.”。在本地构建 ecs-watchbot 容器镜像以后,上传到第一步中创建的 ECR 仓库里。编辑 Dockerfile,内容如下:
    FROM ubuntu:16.04
    # Setup
    
    RUN apt-get update -qq && apt-get install -y curl
    
    # Install node.js
    
    RUN curl -s https://s3.amazonaws.com/mapbox/apps/install-node/v2.0.0/run | NV=4.4.2 NP=linux-x64 OD=/usr/local sh
    
    RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
    
    RUN apt-get install -y nodejs
    
    # Setup application directory
    
    RUN mkdir -p /usr/local/src/watchbot
    
    WORKDIR /usr/local/src/watchbot
    
    # npm installation
    
    COPY ./package.json ./
    
    RUN npm install --production
    
    # Copy files into the container
    
    COPY ./index.js ./
    
    COPY ./lib ./lib
    
    COPY ./bin ./bin
    
    # Logging onto the host EC2
    
    VOLUME /mnt/log
    
    # Run the watcher
    
    CMD ["/bin/sh", "-c", "npm start"]

    然后构建容器镜像:

    sudo docker build -t ecs-watchbot:v3.5.1.

    登录 ECR:

    eval "sudo $(aws ecr get-login --region us-east-1 --no-include-email )"

    上传容器镜像到 ECR:

    sudo docker tag ecs-watchbot:v3.5.1 123456789012.dkr.ecr.us-east-1.amazonaws.com/ecs-watchbot:v3.5.1
    
    sudo docker push 123456789012.dkr.dkr.ecr.us-east-1.amazonaws.com/ecs-watchbot:v3.5.1
  4. 在部署基于 watchbot 的 ECS 服务的时候,mapbox 在 github 上提供了一个样例 mapbox/ecs-telephone:https://github.com/mapbox/ecs-telephone。我们可以基于该样例进行修改,使其满足实际的需求。在安装该样例之前,先在当前主机上安装 js 以及其他组件:
    sudo yum -y erase npmcurl --silent --location https://rpm.nodesource.com/setup_8.x | sudo bash -sudo yum -y install nodejs
    
    sudo npm install -g @mapbox/cfn-config
    
    sudo npm install -g @mapbox/cloudfriend
    
    sudo npm install -g @mapbox/watchbot
  5. 该方案会使用 KMS 对 Amazon Cloudformation 的相关资源进行加密,比如环境变量等,因此需要先部署 KMS。从 github 下载 cloudformation-kms 的代码:
    git clone https://github.com/mapbox/cloudformation-kms.git
    进入 cloudformation-kms 目录,执行下面的命令生成 Amazon Cloudformation 模板:

    export NODE_PATH=/usr/lib/node_modules/@mapboxvalidate-template cloudformation-kms.template.jsbuild-template cloudformation-kms.template.js >> cloudformation-kms.template

    然后使用下面的命令创建 Amazon Cloudformation 堆栈:

    aws cloudformation create-stack \
    
    --stack-name kms-production \
    
    --capabilities CAPABILITY_IAM \
    
    --template-body file://cloudformation-kms.template \
    
    --region us-east-1
    

    堆栈创建完毕以后,会有一个输出,其 export name 为 cloudformation-kms-production,这对应到生成的 KMS key 的 ARN,这个 ARN 会在 ecs-telephone 模板中被使用到。

  6. 从 github 下载 ecs-telephone 代码: git clone https://github.com/mapbox/ecs-telephone.git进入 ecs-telephone 子目录,可以看到一个 Dockerfile 文件,使用该 Dockerfile 构建出的容器镜像里就包含了用户的业务逻辑。用户可以使用任何编程语言编写相应的业务逻辑。比如下面的例子使用 python 写了一段很简单的逻辑,该逻辑把传入当前容器的环境变量:message 的内容输出到 Amazon Cloudwatch log 里,保存该代码名为 domsg.py
    import boto3
    
    import os
    
    message = os.environ["Message"]
    
    print (message)
    

    然后编写一个 Dockerfile,如下所示:

     FROM ubuntu:16.04
    
    WORKDIR /usr/local/src/ecs-telephone
    
    RUN apt-get update -qq
    
    RUN apt-get install -y curl
    
    RUN apt-get install -y python-pip python-dev build-essential
    
    RUN pip install boto3
    
    COPY domsg.py ./
    
    CMD ["python","domsg.py"]
    

    运行下面的命令构建用户的任务容器镜像,tag 为123:

     sudo docker build -t ecs-telephone:123 .

    创建 ECR 仓库,假设名为 ecs-telephone,对应的 URI 为 012345678901.dkr.ecr.us-east-1.amazonaws.com/ecs- telephone。然后把用户的任务容器镜像上传到 ECR 里:

     
    eval "sudo $(aws ecr get-login --region us-east-1 --no-include-email )"
    
    sudo docker tag ecs-telephone:123 012345678901.dkr.ecr.us-east-1.amazonaws.com/ecs-telephone:123
    
    sudo docker push 012345678901.dkr.ecr.us-east-1.amazonaws.com/ecs-telephone:123
    
  7. 进入 ecs-telephone/cloudformation 子目录,可以看到里面有一个 javascript 文件:ecs-telephone.template.js,可以利用该文件生成 Amazon Cloudformation 模板。打开该文件,其内容如下所示:
    const watchbot = require('@mapbox/watchbot');const cf = require('@mapbox/cloudfriend');const Parameters = {
    
    GitSha: { Type: 'String' },
    
    Cluster: { Type: 'String' },
    
    Family: { Type: 'String' }
    
    };
    
    const watcher = watchbot.template({
    
    cluster: cf.ref('Cluster'),
    
    service: 'ecs-telephone',
    
    family: cf.ref('Family'),
    
    serviceVersion: cf.ref('GitSha'),
    
    workers: 1,
    
    reservation: { cpu: 256, memory: 128 },
    
    env: { StackRegion: cf.region },
    
    notificationEmail: 'devnull@mapbox.com'
    
    });
    

    这里有三个输入参数:

    • GitSha:表示包含用户任务的 docker 镜像的 tag 值。
    • Cluster:表示整个任务平台所在的 Amazon ECS 集群的 ARN,注意这里需要输入 ARN 而不是集群的名称。该 ARN 可以使用 awscli 命令来获得:aws ecs list-clusters
    • Family:表示该批处理任务平台的名称,可以根据需要输入一个字符串,其内容没有特定要求。

    还可以在这里根据实际需要修改相应的参数,包括:

    • service:表示使用该模板创建的 ECS 服务的名字。
    • workers:表示 watcher 在发现队列里有消息的时候,启动的包含用户的业务逻辑的容器。缺省为1个。
    • reservation:任务容器在运行时所需要的 CPU 和内存资源。
    • notificationEmail:各种通知产生以后,会发送到该邮箱里。

    参数都修改完毕并保存以后,使用下面的命令创建 Amazon Cloudformation 模板:

    export NODE_PATH=/usr/lib/node_modules
    
    validate-template ecs-telephone.template.js
    
    build-template ecs-telephone.template.js >> ecs-telephone.template

    打开该模板,找到包含 Watchbot-worker-ecs-telephone 字样的部分,可以看到如果使用该模板部署 ECS 服务,则会去找名为ecs-telephone 的 Amazon ECR 仓库,用户可以根据实际情况修改该 ECR 仓库名。

    如果要在中国区运行该模板的话,需要手工进行修改。具体修改的地方包括:

    • 找到 EcrRegion 部分,在其中添加 "cn-north-1": { "Region": "cn-north-1" }
    • 搜索 amazonaws.com,除了 ecs-tasks.amazonaws.com 以外,其他都要添加 .cn,也就是改为 amazonaws.com.cn。
    • 找到下面这一段,将其删除。
  8. 对该 Cloudformation 模板修改完毕以后,使用下面的命令运行模板从而创建 Amazon ECS 服务:
    aws cloudformation create-stack \--stack-name ecs-telephone \--capabilities CAPABILITY_IAM \
    
    --template-body file://ecs-telephone.template \
    
    --parameters \
    
    ParameterKey=GitSha,ParameterValue=123 \
    
    ParameterKey=Cluster,ParameterValue=arn:aws:ecs:us-east-1:123456789012:cluster/production-cluster-ECSCluster\
    
    ParameterKey=Family,ParameterValue=ecs-telephone \
    
    --region us-east-1
    
    
  9. 当 Amazon Cloudformation 堆栈部署完毕以后,进入 ECS 集群,可以看到创建了一个 ECS 服务,点击该服务,进入服务配置以后,有一个始终保持运行状态的 ECS 任务,该任务就是 watcher 容器。还可以通过控制台进入 Amazon SQS 界面,找到三个队列,其后缀名分别为 WatchbotDeadLetterQueueWatchbotTaskEventQueue 以及 WatchbotQueue。触发任务执行的消息需要发送到后缀为 WatchbotQueue 的队列,如果任务执行失败,则消息会放入后缀为 WatchbotDeadLetterQueue 的队列。在向 WatchbotQueue 发送消息的时候,必须遵循下面的格式:
    {
    
    "Subject": "Your subject",
    
    "Message": "Your random message"
    
    }

    该消息中的 Message 内容:“Your random message” 就会被上面第六步中的 domsg.py 输出到 Amazon CloudWatch 的 log group 里。我们可以进入 Amazon CloudWatch 控制台界面上,点击右边的 Logs,然后会看到包含 watchbot 的 log group,点击进去以后找到最新的 log stream 并点击进入,会看到该消息,说明整个流程正常运行起来了:

结论

本文详细描述了如何基于 Amazon ECS 构建并行批处理任务平台。在该方案中,用户只需要关心业务逻辑的代码即可,其他启动,停止,重试任务容器以及自动扩展等操作都交由平台本身完成。另外,我们在部署任务容器时,任务处理的输出以及状态等信息应该保存到 Amazon S3 或者其他存储服务里(比如 RDS),确保任务容器在异常终止的时候,数据不会丢失。

本篇作者

韩思捷

亚马逊 AWS 解决方案架构师,曾负责大企业客户在 AWS 上的售后技术支持工作,目前负责基于 AWS 的云计算方案架构咨询和设计。在加入 AWS 之前,在中国医药集团,Oracle 以及 EMC 研发中心工作,有多年开发和运维经验,并对各种数据库以及存储应用的高可用架构,方案及性能调优有深入研究。