随着Docker技术的不断成熟,越来越多的企业开始考虑使用Docker。其持续集成、版本控制、可移植性、隔离性等,都是我们谈论的优势。而在很多的Docker教程中提到多数是教你如何启动容器,很少讨论如何优雅停止容器化的应用程序。
通常我们在Dockerfile
的编写过程中,需要以CMD
或者ENTRYPOINT
来执行一个shell脚本,当你在容器中执行这个bash脚本时,当前的bash进程将获得PID为1,而实际的应用程序将作为PID1的子进程。这里就会有一个问题: 容器的停止指令信号SIGTERM首先传给PID为1的bash进程,而bash并不会转发到实际的应用程序,然后Docker会因为响应超时10s之后在内核级别杀死该容器。
响应超时时间可以调整,默认是10s的配置置,它的作用是给应用程序更多的时间来正常停止
我们可以通过简单Redis
的Dockerfile
来做演示
FROM debian:buster-slim
LABEL maintainer="JaeGerW2016"
RUN \
apt-get update && \
apt-get install -y --no-install-recommends redis-server procps && \
rm -rf /var/lib/apt/lists/*
COPY start.sh start.sh
RUN chmod +x start.sh
EXPOSE 6379
ENTRYPOINT ["/start.sh"]
以下是start.sh
脚本内容,包含的内容是更改内核相关参数并启动Redis
服务
#!/usr/bin/env bash
# Disable THP Support in kernel
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# TCP backlog setting (defaults to 128)
sysctl -w net.core.somaxconn=16384
#---------------------------------------------------------------
/usr/bin/redis-server
现在我们基于以上的2个文件来构建并运行此容器(由于涉及到宿主机上的内核参数的调整,需要给容器以特权模式--privileged
运行)
docker build -t demo/redis .
docker run -d --privileged --name demo demo/redis
然后我们进容器查看demo
容器正在运行的进程
docker exec demo ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 06:12 ? 00:00:00 bash /start.sh
root 7 1 0 06:12 ? 00:00:00 /usr/bin/redis-server *:6379
可以看出redis-server
进程号是7在运行,这里我们尝试docker stop demo
去正常停止该容器
docker stop demo ## 这里等待10s来做停止操作
demo
docker logs demo
net.core.somaxconn = 16384
7:C 13 Aug 2020 06:12:36.092 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
7:C 13 Aug 2020 06:12:36.092 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=7, just started
7:C 13 Aug 2020 06:12:36.092 # Warning: no config file specified, using the default config. In order to specify a config file use /usr/bin/redis-server /path/to/redis.conf
7:M 13 Aug 2020 06:12:36.093 * Running mode=standalone, port=6379.
7:M 13 Aug 2020 06:12:36.093 # Server initialized
7:M 13 Aug 2020 06:12:36.093 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
7:M 13 Aug 2020 06:12:36.093 * Ready to accept connections
以上的日志内容可以看到最后一行的内容是Ready to accept connections
表示Redis没有收到正常终止推出的信号,Docker 就会绕过容器应用直接向内核发送 SIGKILL
,内核会强行杀死应用,从而终止容器。
exec
命令来解决此问题,exec
代替bash进程而不是创建bash进程的子进程,从而是应用程序获得PID
为1tini
系统,专用于容器的轻量级init
系统正常停止Redis
容器的方法是将start.sh
脚本中最后一行改为exec /usr/bin/redis-server
#!/usr/bin/env bash
# Disable THP Support in kernel
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# TCP backlog setting (defaults to 128)
sysctl -w net.core.somaxconn=16384
#---------------------------------------------------------------
exec /usr/bin/redis-server
重新构建redis
镜像并启动新容器,然后再次检查redis-server
的进程PID
docker exec demo ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 07:56 ? 00:00:00 /usr/bin/redis-server *:6379
如上所见,redis-server
以PID1
的进程运行
接下来我们来测试下docker stop demo
能否可以正常优雅退出
docker stop demo
demo
docker logs demo
net.core.somaxconn = 16384
1:C 13 Aug 2020 07:56:40.133 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 13 Aug 2020 07:56:40.134 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 13 Aug 2020 07:56:40.134 # Warning: no config file specified, using the default config. In order to specify a config file use /usr/bin/redis-server /path/to/redis.conf
1:M 13 Aug 2020 07:56:40.136 * Running mode=standalone, port=6379.
1:M 13 Aug 2020 07:56:40.136 # Server initialized
1:M 13 Aug 2020 07:56:40.136 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
1:M 13 Aug 2020 07:56:40.136 * Ready to accept connections
1:signal-handler (1597305566) Received SIGTERM scheduling shutdown...
1:M 13 Aug 2020 07:59:26.342 # User requested shutdown...
1:M 13 Aug 2020 07:59:26.342 * Saving the final RDB snapshot before exiting.
1:M 13 Aug 2020 07:59:26.343 * DB saved on disk
1:M 13 Aug 2020 07:59:26.343 # Redis is now ready to exit, bye bye...
init
系统Tini
所做的一切都是衍生出一个单独的子进程(Tini
是在一个容器中运行的),等待它退出所有的时候,然后杀死僵尸进程和执行信号转发
Tini
是你能想到的最简单的init
。
github
地址: https://github.com/krallin/tini
之前的redis-server
的Dockerfile
改动如下:
FROM debian:buster-slim
LABEL maintainer="JaeGerW2016"
# Add Tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
RUN \
apt-get update && \
apt-get install -y --no-install-recommends redis-server procps && \
rm -rf /var/lib/apt/lists/*
EXPOSE 6379
ENTRYPOINT ["/tini", "--", "/usr/bin/redis-server"]
重新构建redis
镜像并启动新容器,然后再次检查redis-server
的进程PID
docker exec demo5 ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 09:11 ? 00:00:00 /tini -- /usr/bin/redis-server
root 6 1 0 09:11 ? 00:00:00 /usr/bin/redis-server *:6379
docker stop demo5
demo5
# docker logs demo5
6:C 13 Aug 2020 09:11:15.906 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
6:C 13 Aug 2020 09:11:15.906 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=6, just started
6:C 13 Aug 2020 09:11:15.906 # Warning: no config file specified, using the default config. In order to specify a config file use /usr/bin/redis-server /path/to/redis.conf
6:M 13 Aug 2020 09:11:15.911 * Running mode=standalone, port=6379.
6:M 13 Aug 2020 09:11:15.911 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
6:M 13 Aug 2020 09:11:15.911 # Server initialized
6:M 13 Aug 2020 09:11:15.911 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
6:M 13 Aug 2020 09:11:15.911 * Ready to accept connections
6:signal-handler (1597309905) Received SIGTERM scheduling shutdown...
6:M 13 Aug 2020 09:11:45.277 # User requested shutdown...
6:M 13 Aug 2020 09:11:45.277 * Saving the final RDB snapshot before exiting.
6:M 13 Aug 2020 09:11:45.278 * DB saved on disk
6:M 13 Aug 2020 09:11:45.278 # Redis is now ready to exit, bye bye...
以上的进程显示虽然redis-server
是以PID
为6的进程在容器里运行,然后我们用docker stop demo5
来给tini
发送SIGTERM
终止信号 是会被转发给应用程序redis-server
,日志显示redis-server
在接收到tini
转发的SIGTERM
信号正常优雅退出。
Tini
,即使您没有为其显式安装信号处理程序,SIGTERM
也会正确终止您的过程。Tini
工作的Docker图像将与Tini
无任何变化一起工作。USER <用户名>
USER
指令和 WORKDIR
相似,都是改变环境状态并影响以后的层。WORKDIR
是改变工作目录,USER
则是改变之后层的执行 RUN
, CMD
以及 ENTRYPOINT
这类命令的身份。
示例:
...
RUN groupadd -r -g 999 redis && useradd -r -g redis -u 999 redis
USER redis
RUN [ "redis-server" ]
...
USER <用户名>
指令有其局限性,是改变环境状态并影响以后的层,在执行一些需要root权限的操作时,需要su
或者sudo
重新切换到root权限,这些都比较麻烦,尤其是在TTY缺失的环境i下容易出错
gosu
github
地址:https://github.com/tianon/gosu
示例:(来自redis
的dockerhub
官网文档)
...
RUN groupadd -r -g 999 redis && useradd -r -g redis -u 999 redis
ENV GOSU_VERSION 1.12
RUN set -eux; \
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates dirmngr gnupg wget; \
rm -rf /var/lib/apt/lists/*; \
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
apt-mark auto '.*' > /dev/null; \
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
chmod +x /usr/local/bin/gosu; \
gosu --version; \
gosu nobody true
...
Dockerfile
如下:
FROM debian:buster-slim
LABEL maintainer="JaeGerW2016"
RUN groupadd -r -g 999 redis && useradd -r -g redis -u 999 redis
ENV GOSU_VERSION 1.12
RUN set -eux; \
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates dirmngr gnupg wget; \
rm -rf /var/lib/apt/lists/*; \
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
apt-mark auto '.*' > /dev/null; \
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
chmod +x /usr/local/bin/gosu; \
gosu --version; \
gosu nobody true
RUN \
apt-get update && \
apt-get install -y --no-install-recommends redis-server procps && \
rm -rf /var/lib/apt/lists/*
EXPOSE 6379
CMD ["gosu","redis","redis-server"]
redis-server
以PID
1和 redis
用户运行
docker exec demo11 ps -ef
UID PID PPID C STIME TTY TIME CMD
redis 1 0 0 10:14 ? 00:00:00 redis-server *:6379
root 13 0 0 10:14 ? 00:00:00 ps -ef