阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

聊聊容器中的一号进程

88次阅读
没有评论

共计 3076 个字符,预计需要花费 8 分钟才能阅读完成。

导读 linux 进程在树中排序。每个进程都可以产生子进程,并且除了最顶层的进程之外,每个进程都有一个父进程。

一旦我们启动了多个进程,那么容器里就会出现一个 pid 1,也就是我们常说的 1 号进程或者 init 进程,然后由这个进程创建出其他的子进程。接下来,我带你梳理一下 init 进程是怎么来的。

一个 Linux 操作系统,在系统打开电源,执行 BIOS/boot-loader 之后,就会由 boot-loader 负责加载 Linux 内核。Linux 内核执行文件一般会放在 /boot 目录下,文件名类似 vmlinuz*。在内核完成了操作系统的各种初始化之后,这个程序需要执行的第一个用户态程就是 init 进程。

内核代码启动 1 号进程的时候,在没有外面参数指定程序路径的情况下,一般会从几个缺省路径尝试执行 1 号进程的代码。这几个路径都是 Unix 常用的可执行代码路径。

系统启动的时候先是执行内核态的代码,然后在内核中调用 1 号进程的代码,从内核态切换到用户态。

目前主流的 Linux 发行版,无论是 RedHat 系的还是 Debian 系的,都会把 /sbin/init 作为符号链接指向 Systemd。Systemd 是目前最流行的 Linux init 进程,在它之前还有 SysVinit、UpStart 等 Linux init 进程。

docker 中的 init

在 Linux 上有了容器的概念之后,一旦容器建立了自己的 Pid Namespace(进程命名空间),这个 Namespace 里的进程号也是从 1 开始标记的。所以,容器的 init 进程也被称为 1 号进程。你只需要记住:1 号进程是第一个用户态的进程,由它直接或者间接创建了 Namespace 中的其他进程。

每个 Docker 容器都是一个 PID 命名空间,这意味着容器中的进程与主机上的其他进程是隔离的。PID 命名空间是一棵树,从 PID 1 开始,通常称为 init。

注意:当你运行一个 Docker 容器时,镜像的 ENTRYPOINT 就是你的根进程,即 PID 1(如果你没有 ENTRYPOINT,那么 CMD 就会作为根进程,你可能配置了一个 shell 脚本,或其他的可执行程序,容器的根进程具体是什么,完全取决于你的配置)。

PID 1 在处理 kill 信号的特别之处

与其他进程不同的是:

PID 1 它会忽略具有默认操作的任何信号。因此除非经过编码,否则应用没有监听 SIGTERM 信号,或者应用中没有实现处理 SIGTERM 信号的逻辑,应用就不会停止。比如默认的 Bash 与 C 语言的程序,是没有注册 SIGTERM 信号的 handler;

PID 1 永远不会响应 SIGKILL 和 SIGSTOP 这两个特权信号;

对于其他的信号,如果用户自己注册了 handler,1 号进程可以响应。

把 Bash 当作 PID 1 呢?

每个基础镜像都有这个是 Bash。Bash 正确地收割了采用的子进程。Bash 可以运行任何东西。所以在你的 Dockerfile 中,你肯定会用这个:

CMD ["/bin/bash", "-c", "/path-to-your-app"]

Bash 默认不会处理 SIGTERM 信号,因此这将会导致如下的问题:第一个问题是:如果将 Bash 作为 PID 1 运行,那么发送到 Docker 容器 docker stop 的信号,最终都是将 SIGTERM 信号发送到 Bash,但是 Bash 默认不会处理 SIGTERM 信号,也不会将它们转发到任何地方(除非您自己编写代码实现)。docker stop 命令执行后,容器会有一个关闭的时限,默认为 10 秒,超过十秒则用 kill 强制关闭。换句话说,给 Bash 发送 SIGTERM 信号终止时,会等待十秒钟,然后被内核强制终止包含所有进程的整个容器。这些进程通过 SIGKILL 信号不正常地终止。SIGKILL 是特权信号,无法被捕获,因此进程无法干净地终止。假设服务正在运行的应用程序正忙于写入文件; 如果应用程序在写入过程中不干净地终止,文件可能会损坏。不干净的终止是不好的。这几乎就像从服务器上拔下电源插头一样。

第二个问题是:一旦进程退出,Bash 也会继续退出。如果程序出了 bug 退出了,Bash 会退出,退出代码为 0,而进程实际上崩溃了(但 0 表示“一切正常”; 这将导致 Docker 或者 k8s 上重启策略不符合预期)。因为真正想要的可能是 Bash 返回与的进程相同的退出代码。

请注意,我们对 bash 进行修改,编写一个 EXIT 处理程序,它只是向子进程发送信号:

#!/bin/bash
function cleanup()
{
    local pids=`jobs -p`
    if [["$pids" != ""]]; then
        kill $pids >/dev/null 2>/dev/null
    fi
}

trap cleanup EXIT
/path-to-your-app

不幸的是,这并不能解决问题。向子进程发送信号是不够的:init 进程还必须等待子进程终止,然后才能终止自己。如果 init 进程过早终止,那么所有子进程都会被内核不干净地终止。

很明显,需要一个更复杂的解决方案,但是像 Upstart、Systemd 和 SysV init 这样的完整 init 系统对于轻量级 Docker 容器来说太过分了。幸运的是,我们有很多在容器中使用的 init 程序。我们这里推荐使用简单的 tini。

tini 当作 PID 1

我们在容器中启动一个 init 系统有很多种,这里推荐使用 tini,它是专用于容器的轻量级 init 系统,使用方法也很简单:

FROM openjdk8:8u201-jdk-alpine3.9
RUN apk add --no-cache tini wget \
    && mkdir -p /opt/arthas \
    && cd /opt/arthas \
    && wget https://arthas.aliyun.com/arthas-boot.jar
ENTRYPOINT ["/sbin/tini", "--"]

请注意,Tini 中还有一些额外的功能,在 Bash 或 Java 中很难实现(例如,Tini 可以注册为“子收割者”,因此它实际上不需要作为 PID 1 运行来完成“僵尸进程”收割工作),但是这些功能对于一些高级应用场景来说非常有用。

为什么 docker 中会有僵尸进程?

使用容器的理想境界是一个容器只启动一个进程,但这在现实应用中有时是做不到的。

比如说,在一个容器中除了主进程之外,我们可能还会启动辅助进程,做监控或者 rotate logs; 再比如说,我们需要把原来运行在虚拟机 (VM) 的程序移到容器里,这些原来跑在虚拟机上的程序本身就是多进程的。

一旦我们启动了多个进程,那么容器里就会出现一个 pid 1,也就是我们常说的 1 号进程或者 init 进程,然后由这个进程创建出其他的子进程。比如我们在部署 java 服务的时候,我们需要部署一个 Arthas(阿尔萨斯),来做为 java 程序的诊断工具。

总结

第一个概念是 Linux 1 号进程。它是第一个用户态的进程。它直接或者间接创建了 Namespace 中的其他进程。第二个概念是容器里 1 号进程对信号处理的三个要点:

PID 1 没有默认的信号处理程序。如果应用没有监听 SIGTERM 信号,或者应用中没有实现处理 SIGTERM 信号的逻辑,应用就不会停止,容器也不会终止。

在容器中,1 号进程永远不会响应 SIGKILL 和 SIGSTOP 这两个特权信号;

对于其他的信号,如果用户自己注册了 handler,1 号进程可以响应。

第三个概念是 tini 作为 1 号进程可以给子进程传递 SIGTERM 信号和收割僵尸进程。

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-25发表,共计3076字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中