通过脚本来实现自定义容器的自动重启

  • 1. 场景还原
  • 2. 自定义启动脚本
  • 3. 使用自定义脚本来作为容器启动的脚本
  • 4. 制作自定义脚本作为入口点的新镜像
  • 5. 测试新镜像启动是否走自定义启动脚本


1. 场景还原

现在我有一个自定义的Docker镜像,是基于基础镜像来构建的带有多个服务的镜像,镜像里面包含了两个服务,其中需要A服务先启动,然后B服务依赖A服务,需要等A服务启动成功后,才能正常对外提供服务。其中A服务启动成功后,可以通过 5005 端口来判断,A服务的端口就是 5005;

在刚开始使用的时候,每次都是先启动一个这个镜像的容器,然后进入容器内部,手动执行A服务的启动命令,然后等待A服务启动完成,再执行B服务的启动命令。

这种场景,如果在服务器不关机的情况下,服务一直是正常运行的。但是如果服务器关机重启了,或者需要更新镜像版本了,这就得人工来重复操作了。这就显得有点不太智能了,于是想着折腾一番,能不能整一个在容器启动的时候,执行某个脚本,这个启动脚本里面包含A服务、B服务的启动命令,并且还能维护下启动顺序,如果能把启动服务的日志输出到Docker容器的日志里,使用 docker logs 命令能实时的查看服务的日志,这样也是极好的。于是带着这个目的,开始折腾了。

2. 自定义启动脚本

针对上面的场景,先写一个shell脚本,来启动需要启动的服务;
其中 A服务的启动命令是:

rasa run --enable-api

A服务启动成功后,可以对外提供 5005 端口的 web服务;但是 A服务的启动过程有点慢,比如要几十秒或者一分钟,这个时间不确定;

B服务的启动命令是:

python3 /home/rasa_dev/bot_api.py

B服务启动成功后,可以对外提供 5006 端口的 web服务;

自定义脚本 wait_for_port.sh 内容如下:

#!/bin/bash

# 第一步:运行 rasa run --enable-api 命令
nohup rasa run --enable-api &

# 第二步:等待端口5005可用
while ! nc -z localhost 5005; do
  echo "Waiting for port 5005 to become available..."
  sleep 5
done

# 第三步:一旦端口可用,执行 bot_api.py
python3 /home/rasa_dev/bot_api.py &

# 防止容器退出
sleep infinity
[root@VM-0-5-centos aikg-bot_build]#

脚本包含了需要启动的命令,并且有个判断A服务 5005端口的过程,在端口可用之后再启动 B 服务

在这个脚本中,nc(netcat)命令用于检查端口是否可访问。while循环将持续检测端口,直到它变为可访问状态。一旦端口可用,脚本将执行bot_api.py

刚开始我将上面两个启动命令的日志重定向到 /logs 文件夹下、比如 启动 B服务的命令为:

python3 /home/rasa_dev/bot_api.py >> /logs/bot_api.log 2>&1 &

这样在 B服务启动时,启动日志就会追加到 /logs/bot_api.log 文件中了。

然后启动镜像的时候将日志目录挂载出来,这样就可以在宿主机上查看服务的启动日志了,但是我还是感觉麻烦,所以又改成了上面的命令,移除了/logs/bot_api.log 2>&1 这个日志重定向的内容,这样就不会将日志重定向到文件,而是直接输出到stdoutstderr。当容器运行时,所有输出都会被捕获,这样就可以使用docker logs命令来查看它们:

docker logs <container-name-or-id>

如果日志文件很重要,需要留痕或者方便后面追溯的话,可以将日志重定向到文件中,这样方便追溯或者排查原因,不过不考虑日志内容丢失的话,直接输出到stdoutstderr是最简便的方式。

刚开始我的镜像里面是不包含 nc 这个命令的,我先使用当前版本的镜像,启动一个容器,然后进入容器内部安装这个nc命令:

比如我之前的镜像是:test-bot:1.0.0 ,使用下面命令直接启动一个容器:

docker run -itd --name test-bot x-bot:1.0.0 /bin/bash

上面命令已交互式的方式来运行一个容器,容器启动后,自动进入容器内部,接着来安装这个 nc工具;

这里需要检查下基础镜像底层是Centos还是Ubuntu或者其他的基础镜像,这个决定我们接下来要如何安装新的工具。

可以直接运行 yum 命令 或者 apt-get 命令,看下哪个可以正常运行,则接下来用哪个命令来安装 nc 工具;

我这里支持 apt-get 的方式来安装:

root@aikg-bot-build:/home/rasa_dev# yum
bash: yum: command not found
root@aikg-bot-build:/home/rasa_dev# 
root@aikg-bot-build:/home/rasa_dev# apt-get
apt 1.8.2.3 (amd64)
Usage: apt-get [options] command
       apt-get [options] install|remove pkg1 [pkg2 ...]
       apt-get [options] source pkg1 [pkg2 ...]

apt-get is a command line interface for retrieval of packages
and information about them from authenticated sources and
for installation, upgrade and removal of packages together
with their dependencies.

如果是支持 yum 命令的话,执行 yum 命令,应该是:

[root@VM-0-5-centos ~]# yum
  File "/usr/bin/yum", line 30
    except KeyboardInterrupt, e:
                            ^
SyntaxError: invalid syntax
[root@VM-0-5-centos ~]# 
[root@VM-0-5-centos ~]# apt-get
-bash: apt-get: command not found
[root@VM-0-5-centos ~]#

安装 nc 工具的话,直接在容器内执行:apt-get update && apt-get install -y netcat

在执行 apt-get install 之前,通常建议先执行 apt-get update,主要有以下几个原因:

  • 更新软件包列表:
    apt-get update 命令会从配置的源服务器下载最新的软件包列表。这些列表包含了每个软件包的最新版本和依赖关系信息。通过更新这些列表,你的系统就能够获取到当前最新的软件包信息。
  • 确保安装最新版本的软件:
    如果不执行 apt-get update,你的系统可能仍然使用旧的软件包列表,这意味着你可能无法安装到最新版本的软件包。在这种情况下,执行 apt-get install 可能会安装旧版本的软件包,甚至可能会导致安装失败或出现依赖性问题。
  • 解决依赖关系:
    软件包之间可能有复杂的依赖关系,新的软件包版本可能会改变这些依赖关系。通过执行 apt-get update,你能确保所有依赖关系都基于最新的信息,从而避免在安装过程中遇到问题。
  • 安全性:
    最新的软件包列表通常包括了最新的安全修复。如果你不更新列表,可能会错过一些重要的安全更新,导致系统存在潜在的安全风险。
  • 系统稳定性:
    最新的软件包列表还包含了各种错误修复和改进,通过保持列表的更新,可以提高系统的稳定性和性能。

一个典型的软件包安装流程如下:

sudo apt-get update          # 更新软件包列表
sudo apt-get upgrade         # 升级已安装的软件包(可选)
sudo apt-get install <package-name>  # 安装新的软件包

这样做可以确保你安装的是最新版本的软件包,并且系统依赖关系是最新的,减少安装过程中遇到问题的可能性。

总的来说,执行 apt-get update 是一个确保安装过程顺利并且系统安全和稳定的好习惯。

上面工具安装完成,可以先测试下,是否能正常执行

root@aikg-bot-build:/home/rasa_dev# nc -zv localhost 80
localhost [127.0.0.1] 80 (http) : Connection refused
root@aikg-bot-build:/home/rasa_dev# 
root@aikg-bot-build:/home/rasa_dev# nc -zv localhost 5005
localhost [127.0.0.1] 5005 (?) open
root@aikg-bot-build:/home/rasa_dev#

可以看到当运行 nc -zv localhost 80,得到了 “Connection refused” 的消息。这意味着端口80上没有服务正在监听。

当运行 nc -zv localhost 5005,得到了 “open” 的消息,这表示端口5005上有一个服务正在监听。这通常意味着有一个服务(可能是某个应用或某个后台服务)正在使用这个端口。

nc-v 选项提供了详细的输出,这有助于诊断网络连接问题。当它说 “open” 或者 “Connection refused”,它实际上是在告诉你 nc 尝试与指定端口建立连接的结果。如果端口是开放的,nc 能够成功建立连接;如果端口被拒绝连接,那通常意味着没有服务在那里监听,或者防火墙阻止了连接。

这里还有一个小 tips 就是脚本的最后一行:sleep infinity,刚开始没有加这个得时候,我发现容器启动完成后,就自动 Down 掉了,并不能一直停留在启动成功的状态。

解决这个问题呢,有两种方式,其目的就是让启动脚本在启动服务完成后,进入一个无限循环的状态,一直来维持住现状。

  • 方式一:使用tail -f /dev/nulltail -f /dev/null命令会让脚本进入一个无限循环,读取/dev/null文件的末尾,实际上什么也不做,但是这会保持一个前台进程运行,从而防止容器因缺乏前台进程而自动退出。
  • 方式二:使用 sleep infinitysleep infinity表示让脚本进入无限期睡眠,这样即使所有其他命令都已完成,容器也不会退出;

3. 使用自定义脚本来作为容器启动的脚本

由于这里要修改镜像的启动命令,就得需要重新构建一个新的镜像了,先整个 Dockerfile,内容如下:

[root@VM-0-5-centos ~]# cat Dockerfile 
FROM test-bot:1.0.0

LABEL MAINTAINER="linmm"

# 设置工作目录
WORKDIR /home/rasa_dev

# 定义环境启动时执行的脚本
ADD wait_for_port.sh /wait_for_port.sh
RUN chmod +x /wait_for_port.sh

# 设置 ENTRYPOINT 和 CMD
ENTRYPOINT ["/wait_for_port.sh"]

[root@VM-0-5-centos aikg-bot_build]#

这里我们自定义的脚本 wait_for_port.sh 其实内容很简单,只包含几个固定的命令,没有涉及到动态的参数什么的。

如果复杂一点,脚本里面需要动态参数,比如要监听的端口 5005 是动态的,这样就可以在 Dockerfile 文件的最后,来使用 CMD 命令来给脚本传入参数;

比如在文件的最后一行添加内容:CMD ["5005"]

CMD指令提供了容器启动时ENTRYPOINT所指定的命令需要的参数。这里,CMD指定了参数5005,这通常会被传递给ENTRYPOINT指定的脚本作为参数使用。

如果wait_for_port.sh不需要额外参数,那么CMD指令可以省略,或者可以设置成CMD []表示没有参数。

4. 制作自定义脚本作为入口点的新镜像

现在自定义脚本也有了,Dockerfile 也有了,那就开整新镜像吧。

[root@VM-0-5-centos test-bot_build]# ll
-rw-r--r-- 1 root root      286 Jun 20 19:40 Dockerfile
-rwxr--r-- 1 root root      360 Jun 19 17:50 wait_for_port.sh
[root@VM-0-5-centos aikg-bot_build]#

直接在当前目录下执行:

[root@VM-0-5-centos aikg-bot_build]# docker build -t test-bot:1.0.1 .
Sending build context to Docker daemon  61.07MB
Step 1/6 : FROM test-bot:1.0.3_r3_1
 ---> f6d10507d2f2
Step 2/6 : LABEL MAINTAINER="linmm"
 ---> Running in 5a9275d08c2e
Removing intermediate container 5a9275d08c2e
 ---> 91ee1346c000
Step 3/6 : WORKDIR /home/rasa_dev
 ---> Running in 25a410ccf9f9
Removing intermediate container 25a410ccf9f9
 ---> 18b42a453605
Step 4/6 : ADD wait_for_port.sh /wait_for_port.sh
 ---> cf6136d5fa86
Step 5/6 : RUN chmod +x /wait_for_port.sh
 ---> Running in 12a6ba7675b3
Removing intermediate container 12a6ba7675b3
 ---> f6e9e1cbd2d7
Step 6/6 : ENTRYPOINT ["/wait_for_port.sh"]
 ---> Running in 18ea0cd27bc4
Removing intermediate container 18ea0cd27bc4
 ---> 0c8e946b6409
Successfully built 0c8e946b6409
Successfully tagged test-bot:1.0.1

新镜像顺利构建完成。

5. 测试新镜像启动是否走自定义启动脚本

直接运行

docker run -itd --name test-bot x-bot:1.0.1

接着使用 docker logs 命令来查看启动日志:

[root@VM-0-5-centos ~]# docker logs -f --tail 20  test-bot
Waiting for port 5005 to become available...
/usr/local/lib/python3.10/site-packages/rasa/core/tracker_store.py:1042: MovedIn20Warning: Deprecated API features detected! 
  Base: DeclarativeMeta = declarative_base()
/usr/local/lib/python3.10/site-packages/rasa/shared/utils/validation.py:134: DeprecationWarning: pkg_resources is deprecated
  import pkg_resources
2024-06-20 12:31:53 INFO     root  - Starting Rasa server on http://0.0.0.0:5005
2024-06-20 12:31:54 INFO     rasa.core.processor  - Loading model models/nlu-20240620-122934-future-sharp.tar.gz...
Waiting for port 5005 to become available...
Waiting for port 5005 to become available...
Waiting for port 5005 to become available...
Waiting for port 5005 to become available...
Waiting for port 5005 to become available...
Waiting for port 5005 to become available...
Waiting for port 5005 to become available...
Waiting for port 5005 to become available...
  rasa.shared.utils.io.raise_warning(
2024-06-20 12:32:32 INFO     root  - Rasa server is up and running.
 * Serving Flask app 'bot_api'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5006
 * Running on http://172.27.100.24:5006
Press CTRL+C to quit

可以看到容器启动后,限制行了启动A服务,在 A服务启动完成前,通过监听 5005 端口来循环阻塞,接着再启动 B服务;并且容器不会自动停止。