首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

实现真正优雅的容器应用

进程的优雅退出(Gracefully Exiting) 看似是个不足为奇的小事,一般情况下只要捕获 SIGTERM 等退出信号,执行完必要的工作再退出进程就好了,但是放到容器环境里,会有些意想不到的问题。本文简单探讨在容器内实现优雅退出会碰到的一系列连环坑。

首先声明一点,这里说的优雅可不是什么 elegant,作为一个小码农,不敢妄自评判什么是优雅,翻译成平稳可能更合适,但我们还是使用惯常翻译。

什么是优雅退出

先来介绍一下优雅退出的定义以及简单的实现,对这部分比较熟悉的同学可以跳过。

在服务器上运行的程序难免遇到需要退出的情况,比如发布新版本需要退出旧版本进程,机器资源不够需要迁移到另外一台主机上运行。在收到退出信号那一刻难免会有没处理完的任务,为了避免造成数据丢失,或是客户端的请求意外终止造成不好的体验,就需要把手头上剩下的事情处理完再退出。这个过程被称为优雅退出(Graceful exiting)。

以一个普通的 Nodejs 程序为例,要实现优雅退出很简单,node 等程序通常有个默认的退出信号 handler,我们在代码里替换这个 handler,手动捕获退出的信号(通常是Docker 或其他进程管理程序发出的 SIGTERM,或是在 terminal 里按下ctrl + c 发出的 SIGINT)。

实际测试的时候需要让这段代码一直运行,否则还等不到退出信号,程序自己就执行完退出了。比如在生产环境上通常有个 web 服务持续运行,为了测试简单,我们加一个循环任务。

上面这段代码在命令行里执行:node app.js然后按下 ctrl + c 会观察到过了 3 秒之后程序退出,输出:

容器内进程的生命周期

为了把上面这个实现了优雅退出的程序放到容器里运行,我们先了解一下容器的运作机制。

在一个容器启动的时候,CMD 或者 ENTRYPOINT 里定义的命令会作为容器的主进程(main process)启动,pid 为 1,一旦这个主进程退出了,容器也会被销毁,容器内其他进程会被 kernel 直接 kill。

到这里应该能想到,如果应用进程是被某个其他进程启动的,可能等不到执行完 doCleanUpWork 里面的任务,容器的主进程退出了就会被强行中止。

UNIX 系统有个规定,父进程应该等待子进程结束并收集其退出的状态码。大部分程序也是遵守这个约定的。所以理想情况下我们用 npm 或者其他程序来启动的程序,在 npm 收到退出命令时,会转发信号给子进程并等待其退出,然后自己才会退出,从而终止容器的运行。

容器中的 NPM 程序

我们看看实际情况,还是以一个 Nodejs 应用举例,一个常见的做法是把启动命令写到 npm script 里,然后这个 npm script 作为 Docker 镜像的 COMMAND,容器启动会把 npm 作为 pid=1 的进程启动,然后 npm script 其实是启动一个 shell 进程,执行 script 里定义的命令。这样进程树会是这样:

当 docker stop 被执行,首先 npm 进程会收到 SIGTERM 信号,然后 把 SIGTERM 转发给 sh 进程并等待其退出。然后 sh 进程的行为有些出乎意料了,它没有转发信号给 node 进程,然后自己直接退出了!

奇怪的 shell

sh 作为操作系统一个重要组件,为什么会不遵守 UNIX 进程的规范呢,其实不止 sh 这个 shell 程序,所有其他的 shell 程序,bash,zsh 都是这样设计的。

一个原因是我们经常需要用 shell 来启动程序,有时候会是后台进程,如果 shell 退出会导致子进程全部退出,应该会是个大麻烦。

实际上 shell 程序除了不转发 signals,还有个更可气的特性是不响应退出信号。这在日常使用中不是问题,因为 kernel 会为每个进程加上默认的 signal handler,例外的是 pid=1 的进程,被 kernel 当作一个 init 角色,不会给他加上默认的 handler,可如果在容器中启动 shell,占据了 pid=1 的位置,这个容器就无法正常退出了,只能等 Docker 引擎在超时后强行杀死进程。

所以我们平常碰到 docker stop 一个容器很慢,很有可能是因为这个容器的启动程序是一个 shell 脚本,或者定义 Dockerfile 的启动命令不是 json 数组的 exec 形式 CMD ["executable","param1","param2"] ,而是 CMD command param1 param2 这种 shell form。

关于 shell 的这两个费解行为有一段 bash 源码中的注释作为参考:

可是既然 shell 不转发退出信号,我们平常在命令行终端中执行程序之后,按下 ctrl + c 就能退出程序又是什么原理呢。这就需要具体到 session 和控制终端(terminal)的概念了。

终端系统简介(shell + terminal + tty)

为了更好的解释 shell 的行为,这里简单介绍一下 shell 以及整个终端系统的结构,对这部分比较熟悉的也可以跳过。

shell 和终端(terminal)是两个经常一起使用并且很容易被混淆的概念。我们日常使用的 shell 工具,比如 bash 或者 zsh,其实是一个脚本解释器,而连接键盘(输入)和显示器(输出)设备的是终端(terminal),比如耳熟能详的 putty,iTerm,都是远程终端工具。而终端通过 tty 系统与进程打交道。

在 Linux 系统中,当一个 shell 被启动的时候,同时也创建了一个 session,这个 session 下的所有进程共用这个终端(terminal),同时 tty 系统会为这个终端创建一个虚拟 tty 设备用于将终端命令转发给 tty 系统。实际上 ctrl +c是控制 tty 的特殊信号,收到这个控制字符串,tty 会向这个终端的所有前台进程发送 SIGINT 中断信号,而 shell 本身不响应信号,所以按下 ctrl + c 只会退出在 shell 中启动的前台进程。

通过 ps 命令查看进程的 session 和 绑定的 tty 设备:

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190207B06KO700?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券
http://www.vxiaotou.com