使用 supervisord 管理 docker 容器的最佳方式
Best way to manage docker containers with supervisord
我必须在 相同的 服务器上设置 "dockerized" 环境(集成、质量保证和生产)(客户要求)。每个环境将组成如下:
- rabbitmq
- 芹菜
- 花
- python 基于 3 的应用程序称为 "A"(每个特定分支
环境)
在它们之上,jenkins 将根据 CI 处理部署。
每个环境使用一组容器听起来是最好的方法。
但现在我需要流程经理运行并监督他们:
- 3 个兔子容器,
- 3 celery/flower 个容器,
- 3 "A" 个容器,
- 1 个詹金斯容器。
Supervisord 似乎是最佳选择,但在我的测试中,我无法"properly" 重新启动容器。这是 supervisord.conf
的片段
[program:docker-rabbit]
command=/usr/bin/docker run -p 5672:5672 -p 15672:15672 tutum/rabbitmq
startsecs=20
autorestart=unexpected
exitcodes=0,1
stopsignal=KILL
所以我想知道什么是最好的方式来分离每个环境并能够管理和监督每个服务(一个容器)。
[编辑我受 Thomas 回应启发的解决方案]
每个容器都是 运行 由看起来像
的 .sh 脚本
兔子-integration.py
#!/bin/bash
#set -x
SERVICE="rabbitmq"
SH_S = "/path/to_shs"
export MY_ENV="integration"
. $SH_S/env_.sh
. $SH_S/utils.sh
SERVICE_ENV=$SERVICE-$MY_ENV
ID_FILE=/tmp/$SERVICE_ENV.name # pid file
trap stop SIGHUP SIGINT SIGTERM # trap signal for calling the stop function
run_rabbitmq
$SH_S/env_.sh 看起来像:
# set env variable
...
case $MONARCH_ENV in
$INTEGRATION)
AMQP_PORT="5672"
AMQP_IP="172.17.42.1"
...
;;
$PREPRODUCTION)
AMQP_PORT="5673"
AMQP_IP="172.17.42.1"
...
;;
$PRODUCTION)
AMQP_PORT="5674"
REDIS_IP="172.17.42.1"
...
esac
$SH_S/utils.sh 看起来像:
#!/bin/bash
function random_name(){
echo "$SERVICE_ENV-$(cat /proc/sys/kernel/random/uuid)"
}
function stop (){
echo "stopping docker container..."
/usr/bin/docker stop `cat $ID_FILE`
}
function run_rabbitmq (){
# do no daemonize and use stdout
NAME="$(random_name)"
echo $NAME > $ID_FILE
/usr/bin/docker run -i --name "$NAME" -p $AMQP_IP:$AMQP_PORT:5672 -p $AMQP_ADMIN_PORT:15672 -e RABBITMQ_PASS="$AMQP_PASSWORD" myimage-rabbitmq &
PID=$!
wait $PID
}
至少 myconfig.intergration.conf 看起来像:
[program:rabbit-integration]
command=/path/sh_s/rabbit-integration.sh
startsecs=20
priority=90
autorestart=unexpected
exitcodes=0,1
stopsignal=TERM
在我想使用相同容器的情况下,启动函数如下所示:
function _run_my_container () {
NAME="my_container"
/usr/bin/docker start -i $NAME &
PID=$!
wait $PID
rc=$?
if [[ $rc != 0 ]]; then
_run_my_container
fi
}
其中
function _run_my_container (){
/usr/bin/docker run -p{} -v{} --name "$NAME" myimage &
PID=$!
wait $PID
}
Supervisor 要求其管理的进程不进行守护进程,根据其 documentation:
Programs meant to be run under supervisor should not daemonize
themselves. Instead, they should run in the foreground. They should
not detach from the terminal from which they are started.
这在很大程度上与 Docker 不兼容,其中容器是 Docker 进程 本身的子进程(即因此不是 Supervisor 的子进程) .
为了能够将 Docker 与 Supervisor 一起使用,您可以编写与 Docker 一起使用的 pidproxy
program 的等价物。
但实际上,这两种工具的设计并不是为了一起工作,所以您应该考虑改变其中之一:
- 考虑将 Supervisor 替换为 Docker Compose(设计用于 Docker)
- 考虑将 Docker 替换为 Rocket(没有 "master" 进程)
您需要确保在您的主管配置中使用 stopsignal=INT,然后正常执行 docker run
。
[program:foo]
stopsignal=INT
command=docker -rm run whatever
至少这似乎适用于 docker 版本 1.9.1。
如果您在 shell 脚本中 运行 docker 表单,在 docker 前面加上 exec
非常重要 运行 命令,以便 docker run
替换 shell 进程,从而直接从 supervisord 接收 SIGINT。
您可以 Docker 只是不分离,然后一切正常。我们通过主管以这种方式管理我们的 Docker 个容器。 Docker compose 很棒,但如果您已经在使用 Supervisor 来管理非 docker 事物,那么继续使用它可以将所有管理集中在一个地方。我们将把我们的 docker 运行 包装在一个 bash 脚本中,如下所示,并让主管跟踪它,一切正常:
#!/bin/bash¬
TO_STOP=docker ps | grep $SERVICE_NAME | awk '{ print }'¬
if [$TO_STOP != '']; then¬
docker stop $SERVICE_NAME¬
fi¬
TO_REMOVE=docker ps -a | grep $SERVICE_NAME | awk '{ print }'¬
if [$TO_REMOVE != '']; then¬
docker rm $SERVICE_NAME¬
fi¬
¬
docker run -a stdout -a stderr --name="$SERVICE_NAME" \
--rm $DOCKER_IMAGE:$DOCKER_TAG
我发现通过 supervisor 执行 docker run
实际上工作得很好,有一些预防措施。需要避免的主要事情是允许 supervisord 将 SIGKILL
发送到 docker run
进程,这将终止该进程而不是容器本身。
在大多数情况下,这可以按照 Why Your Dockerized Application Isn’t Receiving Signals 中的说明进行处理。简而言之,需要:
- 使用
CMD ["/path/to/myapp"]
形式(ENTRYPOINT
相同)而不是 shell 形式(CMD /path/to/myapp
)。
- 将
--init
传递给 docker run
。
- 如果使用
ENTRYPOINT
,确保其最后一行调用 exec
,以避免产生新进程。
- 如果上述方法仍然无效,请将
STOPSIGNAL
添加到您的 Dockerfile
。
此外,您需要确保您在 supervisor 中的 stopwaitsecs
设置大于您的进程在收到 SIGTERM
(例如,graceful_timeout
如果使用 gunicorn)。
这是 运行 gunicorn 容器的示例配置:
[program:gunicorn]
command=/usr/bin/docker run --init --rm -i -p 8000:8000 gunicorn
redirect_stderr=true
stopwaitsecs=31
我必须在 相同的 服务器上设置 "dockerized" 环境(集成、质量保证和生产)(客户要求)。每个环境将组成如下:
- rabbitmq
- 芹菜
- 花
- python 基于 3 的应用程序称为 "A"(每个特定分支 环境)
在它们之上,jenkins 将根据 CI 处理部署。
每个环境使用一组容器听起来是最好的方法。
但现在我需要流程经理运行并监督他们:
- 3 个兔子容器,
- 3 celery/flower 个容器,
- 3 "A" 个容器,
- 1 个詹金斯容器。
Supervisord 似乎是最佳选择,但在我的测试中,我无法"properly" 重新启动容器。这是 supervisord.conf
的片段[program:docker-rabbit]
command=/usr/bin/docker run -p 5672:5672 -p 15672:15672 tutum/rabbitmq
startsecs=20
autorestart=unexpected
exitcodes=0,1
stopsignal=KILL
所以我想知道什么是最好的方式来分离每个环境并能够管理和监督每个服务(一个容器)。
[编辑我受 Thomas 回应启发的解决方案]
每个容器都是 运行 由看起来像
的 .sh 脚本兔子-integration.py
#!/bin/bash
#set -x
SERVICE="rabbitmq"
SH_S = "/path/to_shs"
export MY_ENV="integration"
. $SH_S/env_.sh
. $SH_S/utils.sh
SERVICE_ENV=$SERVICE-$MY_ENV
ID_FILE=/tmp/$SERVICE_ENV.name # pid file
trap stop SIGHUP SIGINT SIGTERM # trap signal for calling the stop function
run_rabbitmq
$SH_S/env_.sh 看起来像:
# set env variable
...
case $MONARCH_ENV in
$INTEGRATION)
AMQP_PORT="5672"
AMQP_IP="172.17.42.1"
...
;;
$PREPRODUCTION)
AMQP_PORT="5673"
AMQP_IP="172.17.42.1"
...
;;
$PRODUCTION)
AMQP_PORT="5674"
REDIS_IP="172.17.42.1"
...
esac
$SH_S/utils.sh 看起来像:
#!/bin/bash
function random_name(){
echo "$SERVICE_ENV-$(cat /proc/sys/kernel/random/uuid)"
}
function stop (){
echo "stopping docker container..."
/usr/bin/docker stop `cat $ID_FILE`
}
function run_rabbitmq (){
# do no daemonize and use stdout
NAME="$(random_name)"
echo $NAME > $ID_FILE
/usr/bin/docker run -i --name "$NAME" -p $AMQP_IP:$AMQP_PORT:5672 -p $AMQP_ADMIN_PORT:15672 -e RABBITMQ_PASS="$AMQP_PASSWORD" myimage-rabbitmq &
PID=$!
wait $PID
}
至少 myconfig.intergration.conf 看起来像:
[program:rabbit-integration]
command=/path/sh_s/rabbit-integration.sh
startsecs=20
priority=90
autorestart=unexpected
exitcodes=0,1
stopsignal=TERM
在我想使用相同容器的情况下,启动函数如下所示:
function _run_my_container () {
NAME="my_container"
/usr/bin/docker start -i $NAME &
PID=$!
wait $PID
rc=$?
if [[ $rc != 0 ]]; then
_run_my_container
fi
}
其中
function _run_my_container (){
/usr/bin/docker run -p{} -v{} --name "$NAME" myimage &
PID=$!
wait $PID
}
Supervisor 要求其管理的进程不进行守护进程,根据其 documentation:
Programs meant to be run under supervisor should not daemonize themselves. Instead, they should run in the foreground. They should not detach from the terminal from which they are started.
这在很大程度上与 Docker 不兼容,其中容器是 Docker 进程 本身的子进程(即因此不是 Supervisor 的子进程) .
为了能够将 Docker 与 Supervisor 一起使用,您可以编写与 Docker 一起使用的 pidproxy
program 的等价物。
但实际上,这两种工具的设计并不是为了一起工作,所以您应该考虑改变其中之一:
- 考虑将 Supervisor 替换为 Docker Compose(设计用于 Docker)
- 考虑将 Docker 替换为 Rocket(没有 "master" 进程)
您需要确保在您的主管配置中使用 stopsignal=INT,然后正常执行 docker run
。
[program:foo]
stopsignal=INT
command=docker -rm run whatever
至少这似乎适用于 docker 版本 1.9.1。
如果您在 shell 脚本中 运行 docker 表单,在 docker 前面加上 exec
非常重要 运行 命令,以便 docker run
替换 shell 进程,从而直接从 supervisord 接收 SIGINT。
您可以 Docker 只是不分离,然后一切正常。我们通过主管以这种方式管理我们的 Docker 个容器。 Docker compose 很棒,但如果您已经在使用 Supervisor 来管理非 docker 事物,那么继续使用它可以将所有管理集中在一个地方。我们将把我们的 docker 运行 包装在一个 bash 脚本中,如下所示,并让主管跟踪它,一切正常:
#!/bin/bash¬
TO_STOP=docker ps | grep $SERVICE_NAME | awk '{ print }'¬
if [$TO_STOP != '']; then¬
docker stop $SERVICE_NAME¬
fi¬
TO_REMOVE=docker ps -a | grep $SERVICE_NAME | awk '{ print }'¬
if [$TO_REMOVE != '']; then¬
docker rm $SERVICE_NAME¬
fi¬
¬
docker run -a stdout -a stderr --name="$SERVICE_NAME" \
--rm $DOCKER_IMAGE:$DOCKER_TAG
我发现通过 supervisor 执行 docker run
实际上工作得很好,有一些预防措施。需要避免的主要事情是允许 supervisord 将 SIGKILL
发送到 docker run
进程,这将终止该进程而不是容器本身。
在大多数情况下,这可以按照 Why Your Dockerized Application Isn’t Receiving Signals 中的说明进行处理。简而言之,需要:
- 使用
CMD ["/path/to/myapp"]
形式(ENTRYPOINT
相同)而不是 shell 形式(CMD /path/to/myapp
)。 - 将
--init
传递给docker run
。 - 如果使用
ENTRYPOINT
,确保其最后一行调用exec
,以避免产生新进程。 - 如果上述方法仍然无效,请将
STOPSIGNAL
添加到您的Dockerfile
。
此外,您需要确保您在 supervisor 中的 stopwaitsecs
设置大于您的进程在收到 SIGTERM
(例如,graceful_timeout
如果使用 gunicorn)。
这是 运行 gunicorn 容器的示例配置:
[program:gunicorn]
command=/usr/bin/docker run --init --rm -i -p 8000:8000 gunicorn
redirect_stderr=true
stopwaitsecs=31