如何在docker-compose服务中更新celery工作者,但保持长时间运行的任务处于活动状态,直到任务完成

时间:2020-04-21 07:56:36

标签: docker deployment docker-compose celery

我有一个flask应用程序,该应用程序允许用户通过celery作业队列启动长时间运行的任务(有时> 1d)。 Flask应用程序及其所有依赖项(包括celery worker)都通过docker进行了容器化,并以docker-compose文件开头。

我的问题是,当我使用新版本的应用程序软件更新容器映像时,需要使用以下命令重新启动容器:

docker-compose down
docker-compose up -d

这将取消所有长时间运行的作业,因为docker-compose中每个默认值只有一个短超时值。按照docker-compose and graceful Celery shutdown中的建议,通过docker-compose设置较长的超时值以进行平稳停止对我不起作用,因为无法预测作业将花费多长时间,并且更新可能需要很长时间才能完成所有任务完成。

我的想法是以某种方式将运行中的容器与docker-compose控件分离,然后在分离的容器内正常关闭芹菜,然后允许作业完成,但不接受新作业。然后,我可以通过docker-compose up -d启动普通容器堆栈。

我想这样做:

  • 从docker compose中删除/重命名celery容器
  • 向容器中的芹菜任务发出信号以使其正常停止并让作业完成但不接受新作业
  • 然后启动将接受新作业的新容器

我尝试使用docker rename重命名由docker-compose启动的容器,但它们仍然对docker-compose down做出反应。

我的问题是这种方法是否是处理此问题的正确方法,甚至可以通过docker-compose实现?在docker-compose环境中处理长时间运行的芹菜工人的优雅更新的最佳实践是什么?

我发现的其他相关问题,但不能完全解决问题:

docker-compose and graceful Celery shutdown:答案显示了如何优雅地停止容器,但我想立即开始新的芹菜工人,以确保没有停机时间。

How do I restart celery workers gracefully?:这适用于本地安装,但是我必须重新启动容器才能获取新的应用程序代码。

编辑:解决方案的新提示:

在此问题中,我发现了类似的情况。这里的docker-compose --scale用于复制一项服务,然后可以从旧服务和新服务中找到ID。新服务启动后,应该能够告诉celery关闭并完成旧容器中的执行任务。如果这是解决方案,我稍后会添加为答案。

https://github.com/docker/compose/issues/1786#

编辑:更多地考虑具有缩放比例的变体。在这里,我又遇到了长时间运行的任务的问题。观察即将死去的容器将很麻烦,直到我可以还原到1个实例为止。在链接的示例中,仅重要的是在停止旧服务之前检查新服务是否确实已启动,以便脚本可以立即扩展到单个实例。我宁愿复制该服务,但从docker-compose的控制中删除新服务,以便在我缩放回1个容器时不会被杀死。这必须通过删除正在运行的容器的docker-compose标签来实现:

"Labels": {
                "com.docker.compose.config-hash": "44e0bbd2a10e28bcad071a42315e65ed4d89f2d815a08aed4f3133b05b9d9f71",
                "com.docker.compose.container-number": "1",
                "com.docker.compose.oneoff": "False",
                "com.docker.compose.project": "karmada_docker_upgreat",
                "com.docker.compose.project.config_files": "docker-compose_test.yml",
                "com.docker.compose.project.working_dir": "/home/USERNAME/git/karmada_docker_upgreat",
                "com.docker.compose.service": "karmada_celery_kalibrate_worker",
                "com.docker.compose.version": "1.25.0"
            }

这是错误的曲目吗?重命名服务与docker-compose无关。

**编辑**无法更改正在运行的容器的标签:https://github.com/moby/moby/issues/15496 我考虑得越多,我将不得不使用普通的docker命令来运行celery容器。使用docker命令和shell脚本,很容易实现我需要做的事情。我仍然希望在docker-compose中看到一个解决方案。

1 个答案:

答案 0 :(得分:1)

经过更多研究,我找到了解决此问题的方法。但是我不得不放弃使用docker-compose的约束。

当前,我认为我无法用docker-compose做任何事情,因为一旦容器以在线docker-compose开始,它将始终受docker-compose命令的控制。原因是无法在运行的容器上更改标签,docker-compose通过标签找到了它控制的容器(有关详细信息,请参阅问题)。

所以尽管可以使用:

docker-compose up -d --no-deps --scale $SERVICE_NAME=2 --no-recreate $SERVICE_NAME

要启动更新的容器,请保留当前容器的运行状态,如此处建议:

https://github.com/docker/compose/issues/1786#

长期运行的工作结束后,我无法缩减服务规模。因为作业可能会运行很长时间(> 1d),所以我可能要完成多个容器。因此,我将不得不实施大量的开销来计算当前正在完成的容器,并在完成其中一个容器后将其缩放到适当的数量。总是有偶然的docker-compose down会使它们全部掉落的危险。

但是https://github.com/docker/compose/issues/1786#末尾的shell脚本促使我放弃了docker-compose约束,并使用普通的docker命令控制所有芹菜容器。有了这个,很容易管理我想做的事情。我想出了以下shell脚本:

startup () {
  SERVICE_NAME=${1?"Usage: docker_update <SERVICE_NAME> <COMMAND>"}
  COMMAND=${2?"Usage: docker_update <SERVICE_NAME> <COMMAND>"}
  docker run \
         -d \
         --name $SERVICE_NAME \
         SOME_DOCKER_IMAGE \
         $COMMAND
}

update () {
  SERVICE_NAME=${1?"Usage: docker_update <SERVICE_NAME> <COMMAND>"}
  COMMAND=${2?"Usage: docker_update <SERVICE_NAME> <COMMAND>"}
  echo "[INFO] Updating docker service $SERVICE_NAME"
  OLD_CONTAINER_ID=$(docker ps --format "table {{.ID}}  {{.Names}}  {{.CreatedAt}}" | grep $SERVICE_NAME | tail -n 1 | awk -F  "  " '{print $1}')
  OLD_CONTAINER_NAME=$(docker ps --format "table {{.ID}}  {{.Names}}  {{.CreatedAt}}" | grep $SERVICE_NAME | tail -n 1 | awk -F  "  " '{print $2}')

  TEMP_UUID=`uuidgen`
  TEMP_CONTAINER_NAME="celery_worker_${TEMP_UUID}"

  echo "[INFO] rename $OLD_CONTAINER_NAME to $TEMP_CONTAINER_NAME"
  docker rename $OLD_CONTAINER_NAME $TEMP_CONTAINER_NAME

  echo "[INFO] start new/updated celery queue"
  startup $SERVICE_NAME $COMMAND

  echo "[INFO] send SIGTERM to $TEMP_CONTAINER_NAME for warm shutdown"
  docker kill --signal=SIGTERM $TEMP_CONTAINER_NAME

#  Optional waiting for the container to finish
  echo "[INIT] waiting for old docker container to finish"
  docker wait $TEMP_CONTAINER_NAME
}

SERVICE_NAME=${1?"Usage: docker_update <SERVICE_NAME>"}
COMMAND=${2?"Usage: docker_update <SERVICE_NAME> <COMMAND>"}
echo "[INFO] checking if this service already runs"
docker ps --format "table {{.ID}}  {{.Names}}  {{.CreatedAt}}" | grep $SERVICE_NAME

if [ $? -eq 0 ]
then
  echo "[INFO] CONTAINER with name $SERVICE_NAME is online -> update"
  update $SERVICE_NAME $COMMAND
else
  echo "[INFO] CONTAINER with name $SERVICE_NAME is **not** online -> starting"
  startup $SERVICE_NAME $COMMAND
fi

该脚本检查具有给定名称的服务是否正在运行。如果不是,它将启动它。如果它正在运行,它将重命名当前正在运行的容器,然后启动一个新的(可能已更新)容器,然后将SIGTERM发送到旧的容器。对于芹菜,这是执行warm shutdown的信号,这意味着它不再接受新任务,而是完成当前正在执行的任务,然后退出。如果没有任务在运行,它将立即退出。新芹菜工人接管所有新任务。