在 docker 容器中捕获信号
<p><span style="font-family: Microsoft YaHei; font-size: 15px">我们可能都使用过 docker stop 命令来停止正在运行的容器,有时可能会使用 docker kill 命令强行关闭容器或者把某个信号传递给容器中的进程。这些操作的本质都是通过从主机向容器发送信号实现主机与容器中程序的交互。比如我们可以向容器中的应用发送一个重新加载信号,容器中的应用程序在接到信号后执行相应的处理程序完成重新加载配置文件的任务。本文将介绍在 docker 容器中捕获信号的基本知识。</span></p><h1><span style="font-family: Microsoft YaHei; font-size: 18pt">信号(linux)</span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">信号是一种进程间通信的形式。一个信号就是内核发送给进程的一个消息,告诉进程发生了某种事件。当一个信号被发送给一个进程后,进程会立即中断当前的执行流并开始执行信号的处理程序(这么说不太准确,信号是在特定的时机被处理)。如果没有为这个信号指定处理程序,就执行默认的处理程序。</span><br><span style="font-family: Microsoft YaHei; font-size: 15px">进程需要为自己感兴趣的信号注册处理程序,比如为了能让程序优雅的退出(接到退出的请求后能够对资源进行清理)一般程序都会处理 SIGTERM 信号。与 SIGTERM 信号不同,SIGKILL 信号会粗暴的结束一个进程。因此我们的应用应该实现这样的目录:捕获并处理 SIGTERM 信号,从而优雅的退出程序。如果我们失败了,用户就只能通过 SIGKILL 信号这一终极手段了。除了 SIGTERM 和 SIGKILL ,还有像 SIGUSR1 这样的专门支持用户自定义行为的信号。下面的代码简单的说明在 nodejs 中如何为一个信号注册处理程序:</span></p>
<div class="cnblogs_code">
<pre>process.on('SIGTERM', <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">() {
console.log(</span>'shutting down...'<span style="color: rgba(0, 0, 0, 1)">);
});</span></pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">关于信号的更多信息,笔者在《linux kill 命令》一文中有所提及,这里不再赘述。</span></p>
<h1><span style="font-family: Microsoft YaHei; font-size: 18pt">容器中的信号</span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">Docker 的 stop 和 kill 命令都是用来向容器发送信号的。注意,只有容器中的 1 号进程能够收到信号,这一点非常关键!</span><br><span style="font-family: Microsoft YaHei; font-size: 15px">stop 命令会首先发送 SIGTERM 信号,并等待应用优雅的结束。如果发现应用没有结束(用户可以指定等待的时间),就再发送一个 SIGKILL 信号强行结束程序。</span><br><span style="font-family: Microsoft YaHei; font-size: 15px">kill 命令默认发送的是 SIGKILL 信号,当然你可以通过 -s 选项指定任何信号。</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">下面我们通过一个 nodejs 应用演示信号在容器中的工作过程。创建 app.js 文件,内容如下:</span></p>
<div class="cnblogs_code">
<pre>'use strict'<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">var</span> http = require('http'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">var</span> server = http.createServer(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (req, res) {
res.writeHead(</span>200, {'Content-Type': 'text/plain'<span style="color: rgba(0, 0, 0, 1)">});
res.end(</span>'Hello World\n'<span style="color: rgba(0, 0, 0, 1)">);
}).listen(</span>3000, '0.0.0.0'<span style="color: rgba(0, 0, 0, 1)">);
console.log(</span>'server started'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">var</span> signals =<span style="color: rgba(0, 0, 0, 1)"> {
</span>'SIGINT': 2<span style="color: rgba(0, 0, 0, 1)">,
</span>'SIGTERM': 15<span style="color: rgba(0, 0, 0, 1)">
};
</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> shutdown(signal, value) {
server.close(</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> () {
console.log(</span>'server stopped by ' +<span style="color: rgba(0, 0, 0, 1)"> signal);
process.exit(</span>128 +<span style="color: rgba(0, 0, 0, 1)"> value);
});
}
Object.keys(signals).forEach(</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (signal) {
process.on(signal, </span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> () {
shutdown(signal, signals);
});
});</span></pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">这个应用是一个 http 服务器,监听端口 3000,为 SIGINT 和 SIGTERM 信号注册了处理程序。接下来我们将介绍以不同的方式在容器中运行程序时信号的处理情况。</span></p>
<h1><span style="font-family: Microsoft YaHei; font-size: 18pt">应用程序作为容器中的 1 号进程</span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">创建 Dockerfile 文件,把上面的应用打包到镜像中:</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">FROM iojs:onbuild
COPY .</span>/app.js ./<span style="color: rgba(0, 0, 0, 1)">app.js
COPY .</span>/package.json ./<span style="color: rgba(0, 0, 0, 1)">package.json
EXPOSE </span><span style="color: rgba(128, 0, 128, 1)">3000</span><span style="color: rgba(0, 0, 0, 1)">
ENTRYPOINT [</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">node</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app</span><span style="color: rgba(128, 0, 0, 1)">"</span>]</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">请注意 ENTRYPOINT 指令的写法,这种写法会让 node 在容器中以 1 号进程的身份运行。</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">接下来创建镜像:</span></p>
<div class="cnblogs_code">
<pre>$ docker build --no-cache -t signal-app -f Dockerfile .</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">然后启动容器运行应用程序:</span></p>
<div class="cnblogs_code">
<pre>$ docker run -it --<span style="color: rgba(0, 0, 255, 1)">rm</span> -p <span style="color: rgba(128, 0, 128, 1)">3000</span>:<span style="color: rgba(128, 0, 128, 1)">3000</span> --name=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">my-app</span><span style="color: rgba(128, 0, 0, 1)">"</span> signal-app</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">此时 node 应用在容器中的进程号为 1:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="http://images2017.cnblogs.com/blog/952033/201709/952033-20170926194718887-1789392292.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">现在我们让程序退出,执行命令:</span></p>
<div class="cnblogs_code">
<pre>$ docker container <span style="color: rgba(0, 0, 255, 1)">kill</span> --signal=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">SIGTERM</span><span style="color: rgba(128, 0, 0, 1)">"</span> my-app</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">此时应用会以我们期望的方式退出:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="http://images2017.cnblogs.com/blog/952033/201709/952033-20170926194728528-767382755.png"></span></p>
<h1><span style="font-family: Microsoft YaHei; font-size: 18pt"><span style="font-family: Microsoft YaHei">应用程序不是容器中的 1 号进程</span></span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">创建一个启动应用程序的脚本文件 app1.sh,内容如下:</span></p>
<div class="cnblogs_code">
<pre>#!/usr/bin/<span style="color: rgba(0, 0, 255, 1)">env</span><span style="color: rgba(0, 0, 0, 1)"> bash
node app </span></pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">然后创建 Dockerfile1 文件,内容如下:</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">FROM iojs:onbuild
COPY .</span>/app.js ./<span style="color: rgba(0, 0, 0, 1)">app.js
COPY .</span>/app1.<span style="color: rgba(0, 0, 255, 1)">sh</span> ./app1.<span style="color: rgba(0, 0, 255, 1)">sh</span><span style="color: rgba(0, 0, 0, 1)">
COPY .</span>/package.json ./<span style="color: rgba(0, 0, 0, 1)">package.json
RUN </span><span style="color: rgba(0, 0, 255, 1)">chmod</span> +x ./app1.<span style="color: rgba(0, 0, 255, 1)">sh</span><span style="color: rgba(0, 0, 0, 1)">
EXPOSE </span><span style="color: rgba(128, 0, 128, 1)">3000</span><span style="color: rgba(0, 0, 0, 1)">
ENTRYPOINT [</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">./app1.sh</span><span style="color: rgba(128, 0, 0, 1)">"</span>]</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">接下来创建镜像:</span></p>
<div class="cnblogs_code">
<pre>$ docker build --no-cache -t signal-app1 -f Dockerfile1 .</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">然后启动容器运行应用程序:</span></p>
<div class="cnblogs_code">
<pre>$ docker run -it --<span style="color: rgba(0, 0, 255, 1)">rm</span> -p <span style="color: rgba(128, 0, 128, 1)">3000</span>:<span style="color: rgba(128, 0, 128, 1)">3000</span> --name=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">my-app1</span><span style="color: rgba(128, 0, 0, 1)">"</span> signal-app1</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">此时 node 应用在容器中的进程号不再是 1:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="http://images2017.cnblogs.com/blog/952033/201709/952033-20170926194833153-1647085355.png"></span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">现在给 my-app1 发送 SIGTERM 信号试试,已经无法退出程序了!在这个场景中,应用程序由 bash 脚本启动,bash 作为容器中的 1 号进程收到了 SIGTERM 信号,但是它没有做出任何的响应动作。</span><br><span style="font-family: Microsoft YaHei; font-size: 15px">我们可以通过:</span></p>
<div class="cnblogs_code">
<pre>$ docker container stop my-<span style="color: rgba(0, 0, 0, 1)">app1
# or
$ docker container </span><span style="color: rgba(0, 0, 255, 1)">kill</span> --signal=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">SIGKILL</span><span style="color: rgba(128, 0, 0, 1)">"</span> my-app1</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">退出应用,它们最终都是向容器中的 1 号进程发送了 SIGKILL 信号。很显然这不是我们期望的,我们希望程序能够收到 SIGTERM 信号优雅的退出。</span></p>
<h1><span style="font-family: Microsoft YaHei; font-size: 18pt">在脚本中捕获信号</span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">创建另外一个启动应用程序的脚本文件 app2.sh,内容如下:</span></p>
<div class="cnblogs_code">
<pre>#!/usr/bin/<span style="color: rgba(0, 0, 255, 1)">env</span><span style="color: rgba(0, 0, 0, 1)"> bash
set </span>-<span style="color: rgba(0, 0, 0, 1)">x
pid</span>=<span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">
# SIGUSR1</span>-<span style="color: rgba(0, 0, 0, 1)">handler
my_handler() {
</span><span style="color: rgba(0, 0, 255, 1)">echo</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">my_handler</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
}
# SIGTERM</span>-<span style="color: rgba(0, 0, 0, 1)">handler
term_handler() {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> [ $pid -ne <span style="color: rgba(128, 0, 128, 1)">0</span> ]; <span style="color: rgba(0, 0, 255, 1)">then</span>
<span style="color: rgba(0, 0, 255, 1)">kill</span> -SIGTERM <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">$pid</span><span style="color: rgba(128, 0, 0, 1)">"</span>
<span style="color: rgba(0, 0, 255, 1)">wait</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">$pid</span><span style="color: rgba(128, 0, 0, 1)">"</span>
<span style="color: rgba(0, 0, 255, 1)">fi</span><span style="color: rgba(0, 0, 0, 1)">
exit </span><span style="color: rgba(128, 0, 128, 1)">143</span>; # <span style="color: rgba(128, 0, 128, 1)">128</span> + <span style="color: rgba(128, 0, 128, 1)">15</span> --<span style="color: rgba(0, 0, 0, 1)"> SIGTERM
}
# setup handlers
# on callback, </span><span style="color: rgba(0, 0, 255, 1)">kill</span> the <span style="color: rgba(0, 0, 255, 1)">last</span> background process, <span style="color: rgba(0, 0, 255, 1)">which</span> is `<span style="color: rgba(0, 0, 255, 1)">tail</span> -f /dev/<span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">` and execute the specified handler
trap </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">kill ${!}; my_handler</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)"> SIGUSR1
trap </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">kill ${!}; term_handler</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)"> SIGTERM
# run application
node app </span>&<span style="color: rgba(0, 0, 0, 1)">
pid</span>=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">$!</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
# </span><span style="color: rgba(0, 0, 255, 1)">wait</span><span style="color: rgba(0, 0, 0, 1)"> forever
</span><span style="color: rgba(0, 0, 255, 1)">while</span> <span style="color: rgba(0, 0, 255, 1)">true</span>
<span style="color: rgba(0, 0, 255, 1)">do</span>
<span style="color: rgba(0, 0, 255, 1)">tail</span> -f /dev/<span style="color: rgba(0, 0, 255, 1)">null</span> & <span style="color: rgba(0, 0, 255, 1)">wait</span> ${!<span style="color: rgba(0, 0, 0, 1)">}
</span><span style="color: rgba(0, 0, 255, 1)">done</span></pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">这个脚本文件在启动应用程序的同时可以捕获发送给它的 SIGTERM 和 SIGUSR1 信号,并为它们添加了处理程序。其中 SIGTERM 信号的处理程序就是向我们的 node 应用程序发送 SIGTERM 信号。</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">然后创建 Dockerfile2 文件,内容如下:</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">FROM iojs:onbuild
COPY .</span>/app.js ./<span style="color: rgba(0, 0, 0, 1)">app.js
COPY .</span>/app2.<span style="color: rgba(0, 0, 255, 1)">sh</span> ./app2.<span style="color: rgba(0, 0, 255, 1)">sh</span><span style="color: rgba(0, 0, 0, 1)">
COPY .</span>/package.json ./<span style="color: rgba(0, 0, 0, 1)">package.json
RUN </span><span style="color: rgba(0, 0, 255, 1)">chmod</span> +x ./app2.<span style="color: rgba(0, 0, 255, 1)">sh</span><span style="color: rgba(0, 0, 0, 1)">
EXPOSE </span><span style="color: rgba(128, 0, 128, 1)">3000</span><span style="color: rgba(0, 0, 0, 1)">
ENTRYPOINT [</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">./app2.sh</span><span style="color: rgba(128, 0, 0, 1)">"</span>]</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">接下来创建镜像:</span></p>
<div class="cnblogs_code">
<pre>$ docker build --no-cache -t signal-app2 -f Dockerfile2 .</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">然后启动容器运行应用程序:</span></p>
<div class="cnblogs_code">
<pre>$ docker run -it --<span style="color: rgba(0, 0, 255, 1)">rm</span> -p <span style="color: rgba(128, 0, 128, 1)">3000</span>:<span style="color: rgba(128, 0, 128, 1)">3000</span> --name=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">my-app2</span><span style="color: rgba(128, 0, 0, 1)">"</span> signal-app2</pre>
</div>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">此时 node 应用在容器中的进程号也不是 1,但是它却可以接收到 SIGTERM 信号并优雅的退出了:</span></p>
<p><span style="font-family: Microsoft YaHei; font-size: 15px"><img src="http://images2017.cnblogs.com/blog/952033/201709/952033-20170926194956747-106428789.png"></span></p>
<h1><span style="font-family: Microsoft YaHei; font-size: 18pt">结论</span></h1>
<p><span style="font-family: Microsoft YaHei; font-size: 15px">容器中的 1 号进程是非常重要的,如果它不能正确的处理相关的信号,那么应用程序退出的方式几乎总是被强制杀死而不是优雅的退出。究竟谁是 1 号进程则主要由 EntryPoint, CMD, RUN 等指令的写法决定,所以这些指令的使用是很有讲究的。</span></p>
<p> </p>
</div>
<div id="MySignature" role="contentinfo">
<div>作者:sparkdev</div>
<div>出处:http://www.cnblogs.com/sparkdev/</div>
<div>本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。</div><br><br>
来源:https://www.cnblogs.com/sparkdev/p/7598590.html
頁:
[1]