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

How can I update a celery worker in a docker-compose service but keep long running task active until they finish

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

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

docker-compose down
docker-compose up -d

这将取消所有长 运行ning 作业,因为在 docker-compose 中默认只有一个很短的超时值。按照 docker-compose and graceful Celery shutdown 中的建议,通过 docker-compose 为正常停止设置更长的超时值对我不起作用,因为无法预测作业需要多长时间,更新可能需要很长时间直到所有任务完成。

我的想法是以某种方式从 docker-compose 控件中分离 运行ning 容器,然后在分离的容器内正常关闭 celery,然后允许作业完成,但是不接受新工作。然后我可以通过 docker-compose up -d.

启动普通容器堆栈

因此我想这样做:

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

我的问题是这种方法是否是处理此问题的正确方法,使用 docker-compose 是否可行?在 docker-compose 环境中处理具有长 运行ning 任务的 celery worker 的优雅更新的最佳实践是什么?

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

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#

编辑:更多地考虑带有缩放的变体。我又遇到了 运行ning 长任务的问题。在我可以缩减到 1 个实例之前,观察垂死的容器会很麻烦。在 link 中的示例中,唯一重要的是在停止旧服务之前检查新服务是否真的启动,以便脚本可以立即缩减为单个实例。我宁愿复制该服务,但从 docker-compose 的控制中删除新服务,这样当我缩减到 1 个容器时它就不会被杀死。这必须通过删除 运行ning 容器的 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.

没有影响

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

经过更多的研究,我找到了解决这个问题的方法。但我不得不放弃使用 docker-compose 的限制。

目前,我认为使用 docker-compose 无法完成我需要做的事情,因为一旦使用 docker-compose 启动的容器将始终由 docker-compose 命令控制,只要它在线。原因是无法在 运行ning 容器上更改标签,并且 docker-compose 通过标签找到它控制的容器(有关详细信息,请参见问题)。

所以尽管可以使用:

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

启动更新的容器并离开当前容器 运行ning,如这里所建议:

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

在漫长的 运行ning 工作完成后,我无法缩减服务规模。因为作业可能 运行 很长(> 1 天),所以我可以完成多个容器。因此,我将不得不实施大量开销来计算当前正在完成的容器,并在其中一个完成时重新缩放回适当的数量。总是有意外的危险 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 }')
  OLD_CONTAINER_NAME=$(docker ps --format "table {{.ID}}  {{.Names}}  {{.CreatedAt}}" | grep $SERVICE_NAME | tail -n 1 | awk -F  "  " '{print }')

  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

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