详解如何在 docker 容器中捕获信号

我们可能都使用过 docker stop 命令来停止正在运行的容器,有时可能会使用 docker kill 命令强行关闭容器或者把某个信号传递给容器中的进程。这些操作的本质都是通过从主机向容器发送信号实现主机与容器中程序的交互。比如我们可以向容器中的应用发送一个重新加载信号,容器中的应用程序在接到信号后执行相应的处理程序完成重新加载配置文件的任务。本文将介绍在 docker 容器中捕获信号的基本知识。

信号(linux)

信号是一种进程间通信的形式。一个信号就是内核发送给进程的一个消息,告诉进程发生了某种事件。当一个信号被发送给一个进程后,进程会立即中断当前的执行流并开始执行信号的处理程序。如果没有为这个信号指定处理程序,就执行默认的处理程序。

进程需要为自己感兴趣的信号注册处理程序,比如为了能让程序优雅的退出(接到退出的请求后能够对资源进行清理)一般程序都会处理 SIGTERM 信号。与 SIGTERM 信号不同,SIGKILL 信号会粗暴的结束一个进程。因此我们的应用应该实现这样的目录:捕获并处理 SIGTERM 信号,从而优雅的退出程序。如果我们失败了,用户就只能通过 SIGKILL 信号这一终极手段了。除了 SIGTERM 和 SIGKILL ,还有像 SIGUSR1 这样的专门支持用户自定义行为的信号。下面的代码简单的说明在 nodejs 中如何为一个信号注册处理程序:

process.on('SIGTERM',function() {
 console.log('shutting down...');
});

关于信号的更多信息,笔者在《linux kill 命令》一文中有所提及,这里不再赘述。

容器中的信号

Docker 的 stop 和 kill 命令都是用来向容器发送信号的。注意,只有容器中的 1 号进程能够收到信号,这一点非常关键!
stop 命令会首先发送 SIGTERM 信号,并等待应用优雅的结束。如果发现应用没有结束(用户可以指定等待的时间),就再发送一个 SIGKILL 信号强行结束程序。

kill 命令默认发送的是 SIGKILL 信号,当然你可以通过 -s 选项指定任何信号。

下面我们通过一个 nodejs 应用演示信号在容器中的工作过程。创建 app.js 文件,内容如下:

'use strict';

var http = require('http');

var server = http.createServer(function (req,res) {
 res.writeHead(200,{'Content-Type': 'text/plain'});
 res.end('Hello World\n');
}).listen(3000,'0.0.0.0');

console.log('server started');

var signals = {
 'SIGINT': 2,'SIGTERM': 15
};

function shutdown(signal,value) {
 server.close(function () {
  console.log('server stopped by ' + signal);
  process.exit(128 + value);
 });
}

Object.keys(signals).forEach(function (signal) {
 process.on(signal,function () {
  shutdown(signal,signals[signal]);
 });
});

这个应用是一个 http 服务器,监听端口 3000,为 SIGINT 和 SIGTERM 信号注册了处理程序。接下来我们将介绍以不同的方式在容器中运行程序时信号的处理情况。

应用程序作为容器中的 1 号进程

创建 Dockerfile 文件,把上面的应用打包到镜像中:

FROM iojs:onbuild
COPY ./app.js ./app.js
COPY ./package.json ./package.json
EXPOSE 3000
ENTRYPOINT ["node","app"]

请注意 ENTRYPOINT 指令的写法,这种写法会让 node 在容器中以 1 号进程的身份运行。

接下来创建镜像:

$ docker build --no-cache -t signal-app -f Dockerfile .

然后启动容器运行应用程序:

$ docker run -it --rm -p 3000:3000 --name="my-app" signal-app

此时 node 应用在容器中的进程号为 1:

现在我们让程序退出,执行命令:

$ docker container kill --signal="SIGTERM" my-app

此时应用会以我们期望的方式退出:

应用程序不是容器中的 1 号进程

创建一个启动应用程序的脚本文件 app1.sh,内容如下:

#!/usr/bin/env bash
node app 

然后创建 Dockerfile1 文件,内容如下:

FROM iojs:onbuild
COPY ./app.js ./app.js
COPY ./app1.sh ./app1.sh
COPY ./package.json ./package.json
RUN chmod +x ./app1.sh
EXPOSE 3000
ENTRYPOINT ["./app1.sh"]

接下来创建镜像:

$ docker build --no-cache -t signal-app1 -f Dockerfile1 .

然后启动容器运行应用程序:

$ docker run -it --rm -p 3000:3000 --name="my-app1" signal-app1

此时 node 应用在容器中的进程号不再是 1:

现在给 my-app1 发送 SIGTERM 信号试试,已经无法退出程序了!在这个场景中,应用程序由 bash 脚本启动,bash 作为容器中的 1 号进程收到了 SIGTERM  信号,但是它没有做出任何的响应动作。

我们可以通过:

$ docker container stop my-app1
# or
$ docker container kill --signal="SIGKILL" my-app1

退出应用,它们最终都是向容器中的 1 号进程发送了 SIGKILL 信号。很显然这不是我们期望的,我们希望程序能够收到 SIGTERM  信号优雅的退出。

在脚本中捕获信号

创建另外一个启动应用程序的脚本文件 app2.sh,内容如下:

#!/usr/bin/env bash
set -x

pid=0

# SIGUSR1-handler
my_handler() {
 echo "my_handler"
}

# SIGTERM-handler
term_handler() {
 if [ $pid -ne 0 ]; then
  kill -SIGTERM "$pid"
  wait "$pid"
 fi
 exit 143; # 128 + 15 -- SIGTERM
}
# setup handlers
# on callback,kill the last background process,which is `tail -f /dev/null` and execute the specified handler
trap 'kill ${!}; my_handler' SIGUSR1
trap 'kill ${!}; term_handler' SIGTERM

# run application
node app &
pid="$!"

# wait forever
while true
do
 tail -f /dev/null & wait ${!}
done

这个脚本文件在启动应用程序的同时可以捕获发送给它的 SIGTERM 和 SIGUSR1 信号,并为它们添加了处理程序。其中 SIGTERM 信号的处理程序就是向我们的 node 应用程序发送 SIGTERM 信号。

然后创建 Dockerfile2 文件,内容如下:

FROM iojs:onbuild
COPY ./app.js ./app.js
COPY ./app2.sh ./app2.sh
COPY ./package.json ./package.json
RUN chmod +x ./app2.sh
EXPOSE 3000
ENTRYPOINT ["./app2.sh"]

接下来创建镜像:

$ docker build --no-cache -t signal-app2 -f Dockerfile2 .

然后启动容器运行应用程序:

$ docker run -it --rm -p 3000:3000 --name="my-app2" signal-app2

此时 node 应用在容器中的进程号也不是 1,但是它却可以接收到 SIGTERM 信号并优雅的退出了:

结论

容器中的 1 号进程是非常重要的,如果它不能正确的处理相关的信号,那么应用程序退出的方式几乎总是被强制杀死而不是优雅的退出。究竟谁是 1 号进程则主要由 EntryPoint,CMD,RUN 等指令的写法决定,所以这些指令的使用是很有讲究的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


文章浏览阅读8.8k次,点赞2次,收藏7次。本文介绍Docker Compose的网络的配置。_docker compose 网络配置
文章浏览阅读1.5w次,点赞7次,收藏76次。原网提供的教程需要先刷系统到U盘,再把U盘的系统转移到emmc,但是我下面提供的镜像不需要此操作,注意区分。双头USB线一根(买或者自己做,网上有教程);电脑一台;镊子或者别针(或者其他导电的东西,用来短接主板);螺丝刀,电吹风(加热背胶更好撕);U盘一个(刷机和扩展玩客云硬盘容量都用得上);下面这些链接里面的资源下载备用。#镜像地址:https://www.aliyundrive.com/s/NXjbaKC3Hyq提取码: 5i7a。_玩客云 armbian
文章浏览阅读940次,点赞20次,收藏20次。通过 docker run 命令创建一个新的容器。
文章浏览阅读1k次,点赞20次,收藏20次。Podman 是一个开源的容器运行时项目,可在大多数 Linux平台上使用。Podman提供与Docker 非常相似的功能。Podman 提供了一个与 Docker 兼容的 CLI 工具(命令行界面),可以这样说,会使用 docker 基本就会使用 podman。_podman 替代
文章浏览阅读2.1k次。请注意,这些命令需要在 Docker 主机上执行,并且需要有相应的权限才能访问容器的日志文件。确保你有足够的权限来执行这些命令,或者在以管理员身份运行命令。此外,还要注意这些命令可能会清空所有容器的日志文件,包括正在运行的和已经停止的容器。如果只想清空特定容器的日志文件,可以根据需要修改命令。删除容器中netcore控制台存储到docker日志记录。_docker清空日志命令
文章浏览阅读1.1k次,点赞37次,收藏40次。nacos搭建集群连接mysql实现nginx负载均衡实现讲解。_niginx nacos 集群实现负载均衡
文章浏览阅读3.5k次,点赞35次,收藏36次。Docker 是一种容器引擎,可以在容器内运行一段代码。Docker 镜像是在任何地方运行您的应用程序而无需担心应用程序依赖性的方式。要构建镜像,docker 使用一个名为 Dockerfile 的文件。Dockerfile 是一个包含许多指令(RUN、COPY、EXPOSE 等)的文件。成功执行这些命令后,docker 将创建一个镜像供我们在任何地方使用。
文章浏览阅读2.6k次。2.即使在Dockerfile中换源,但在bulid过程中,依然可以注意到连接的是bebian官方源,因为debian 12改了,默认不使用/etc/apt/sources.list文件,这个文件初始是空的,真正的仓库配置在 /etc/apt/sources.list.d/ 目录下,故,修改源需要用RUN rm -rf /etc/apt/sources.list.d/*给每个pip install后面都加上-i https://pypi.tuna.tsinghua.edu.cn/simple。_error: failed to solve: process "/bin/sh -c yum makecache" did not complete
文章浏览阅读9.7k次,点赞2次,收藏4次。嗨,各位猫头虎博主的小伙伴们!。本文将详细分析该问题的根本原因、解决方法以及如何避免类似的状况。让我们一起来解决这个Bug吧!在云原生应用开发中,Docker是不可或缺的工具,但在使用过程中会遇到各种问题,OCI runtime create失败就是其中之一。本文通过深入分析问题的原因,提供了解决方法和避免类似问题的建议。希望本文能帮助您更好地理解和应对这个常见的Bug。_error response from daemon: oci runtime create failed: container_linux.go:34
文章浏览阅读2.2k次,点赞67次,收藏44次。[Docker实战] 旭日X3派上Docker Openwrt +Samba 实现局域网NAS && 开启AP模式
文章浏览阅读1k次,点赞52次,收藏38次。Docker的运行,依赖linux的环境,官方提供了Docker Desktop for Windows,但是它需要安装Hyper-V,Hyper-V是微软开发的虚拟机,类似于 VMWare 或 VirtualBox,仅适用于 Windows 10。安装过程如果出现超时,不要灰心,多试几次,总会成功的。执行完毕后会重启,在重启的过程中进行安装。此命令省略了镜像版本和运行参数,docker使用latest作为版本,即最新版本。从hello world的例子中,也可以体验到,docker实例的运行是非常快的。
文章浏览阅读3.6k次,点赞77次,收藏74次。【Docker】Docker的使用案例以及未来发展、Docker Hub 服务、环境安全的详细讲解
文章浏览阅读1w次。要删除已存在的 Docker 镜像,您可以使用docker rmi命令。以下是完整的流程。_docker 删除镜像
文章浏览阅读3.3k次。当安装好docker-compose并添加执行权限后,执行命令docker-compose 相关命令时出现 -bash: /usr/local/bin/docker-compose: 无法执行二进制文件。应该是安装包有问题,网上找了几种重新安装方法途径,还是会出现这个问题,最终找到一种可靠重新安装的解决方法,原文。2.从Docker官方网站下载Docker Compose最新版本的二进制文件(下载稍慢)3.授予Docker Compose二进制文件执行权限。1.先卸载Docker Compose的旧版本。_-bash: /usr/local/bin/docker-compose: cannot execute binary file
文章浏览阅读1.1k次。备注:Mysql5.7+ password字段 已改成 authentication_string字段。#备注:Mysql8.0修改密码方式已有变化(此处是个坑,需要注意)#设置完密码策略后重新输入修改命令,更改后的密码为123456。java默认安装路径/usr/lib/jvm/;#进入/etc/profile 配置文件。#查看正在使用的MySQL repo。#验证开启的8080端口是否生效。#验证开启的8080端口是否生效。#执行命令来开启8080端口。#先把root的旧密码置空。_yum install -y java 安装在什么目录
文章浏览阅读1.9k次。Windows11下清理Docker Desktop与wsl的C盘空间占用_wsl清理缓存
文章浏览阅读8.5k次,点赞2次,收藏20次。本机想要启用gpu加速计算,需要由一张多余的nVidia显卡。需要提前禁用nouveau:lsmod | grep nouveau没有输出即禁用了需要安装1、显卡驱动、2、cuda库(安装cuda会自动安装显卡驱动)3、cudnn(深度神经网络的GPU加速库,需要神经网络则安否则可以不安)安装完成后,可以运行nvidia-smi查看GPU设备的状态。_docker gpu
文章浏览阅读6k次,点赞14次,收藏39次。1.Dockerfile命令初识,CMD...;2.idea配置docker,图形化界面;3.编写Dockerfile把jar包制作成镜像,并用idea一键生成和启动容器;4.在Linux中测试,在宿主机用swagger进行测试;_springboot dockerfile
文章浏览阅读1k次,点赞2次,收藏2次。截止目前,Redis 的最新稳定版本是 6.2.6。这个版本在可读性、性能和稳定性方面进行了改进,并增加了一些新的命令和功能。_docker redis配置文件
文章浏览阅读1.5k次,点赞34次,收藏35次。使用上面的Cpolar https公网地址,在任意设备的浏览器进行访问,即可成功看到我们火狐浏览器界面,这样一个公网地址且可以远程访问就创建好了,使用了cpolar的公网域名,无需自己购买云服务器,即可发布到公网进行远程访问!上面在本地成功部署了FireFox 火狐浏览器,并局域网访问成功,下面我们在Linux安装Cpolar内网穿透工具,通过cpolar 转发本地端口映射的http公网地址,我们可以很容易实现远程访问,而无需自己注册域名购买云服务器.下面是安装cpolar步骤。