容器内init进程方案

进程标识符 (PID) 是Linux 内核为每个进程提供的唯一标识符。熟悉docker的同学都知道, 所有的进程 PID都属于某一个PID namespaces, 也就是说容器具有一组自己的 PID,这些 PID 映射到主机系统上的 PID。启动Linux内核时启动的第一个进程具有 PID 1,一般来说该进程就是 init 进程,例如 systemd 或 SysV。同样,在容器中启动的第一个进程也会获得该PID namespaces内的 PID 1。Docker 和 Kubernetes 使用信号与容器内的进程通信,来终止容器的运行, 只能向容器内 PID 1 的进程发送信号。

在容器的环境中,PID 和 Linux 信号会产生两个需要考虑的问题。

问题 1:Linux 内核如何处理信号

对于具有 PID 1 的进程,Linux 内核处理信号的方式与其他进程有所不同。系统不会自动为此进程注册信号处理函数,SIGTERM 或 SIGINT 等信号默认被忽略,必须使用 SIGKILL 来终止进程。使用 SIGKILL 可能会导致应用程序无法平滑退出,例如正在写入的数据出现不一致或正在处理的请求异常结束。

问题 2:经典 init 系统如何处理孤立进程

宿主机上的init进程(如 systemd)也用来回收孤儿进程。孤儿进程(其父级已结束的进程)会重新附加到 PID 1 的进程,PID 1进程会在这些进程结束时回收它们。但在容器中,这一职责由具有 PID 1 的进程承担,如果该进程无法正确处理回收,则可能会出现耗尽内存或一些其他资源的风险。

常见的解决方案

上述问题对于一些应用程序可能无足轻重,并不需要关注,但是对于一些面向用户或者处理数据的应用程序却极为关键。需要严格防止。 对此有以下几种解决方案:

解决方案 1:作为 PID 1 运行并注册信号处理程序

最简单方法是使用 Dockerfile 中的 CMD 或 ENTRYPOINT 指令来启动进程。例如,在以下 Dockerfile 中,nginx 是第一个也是唯一一个要启动的进程。

FROM debian:9

RUN apt-get update && \

apt-get install -y nginx

EXPOSE 80

CMD [ "nginx", "-g", "daemon off;" ]

nginx 进程会注册自己的信号处理程序。如果是我们自己写的程序则需要自己在代码中执行相同操作。

因为我们的进程就是PID 1进程,所以可以保证能够正确的收到并处理信号。 这种方式可以轻松地解决了第一个问题,但是对于第二个问题却无法解决。 如果你的应用程序不会产生多余的子进程,则第二个问题也不存在。 可以直接采用这种相对简单的解决方案。

此处需要注意,有时候我们可能一不小心就让我们的进程不是容器内首进程了,例如如下Dockerfile:

FROM tagedcentos:7

ADD command /usr/bin/command

CMD cd /usr/bin/ && ./command

我们只是想执行启动命令而已,却发现此时首进程变为了shell:

[[email protected] /]# ps -ef

UID PID PPID C STIME TTY TIME CMD

root 1 0 1 07:05 pts/0 00:00:00 /bin/sh -c cd /usr/bin/ && ./command

root 6 1 0 07:05 pts/0 00:00:00 ./command

docker会自动地判断你当前启动命令是否由多个命令组成,如果是多个命令则会用shell来解释。如果是单个命令则就算外面包了一层shell容器内首进程也直接是业务进程。例如如果将dockerfile写成CMD bash -c "/usr/bin/command",容器内首进程还是业务进程,如下:

[[email protected] /]# ps -ef

UID PID PPID C STIME TTY TIME CMD

root 1 0 2 13:09 ? 00:00:00 /usr/bin/command

所以正确地书写Dockerfile也可以让我们避免掉很多问题。

有时,我们可能需要在容器中准备环境,以便进程能够正常运行。在此情况下,一般我们会让容器在启动时执行一个 shell 脚本。此 shell 脚本的任务是准备环境和启动主进程。但是,如果采用此方法,shell脚本将是PID 1 而不是我们的进程。因此必须使用内置的 exec 命令从 shell 脚本启动进程。exec 命令会将脚本替换为我们所需的程序, 这样我们的业务进程将成为 PID 1。

解决方案 2:使用专用 init 进程

正如在传统宿主机所做的那样,还可以使用init进程来处理这些问题。但是, 传统的init进程(例如 systemd 或 SysV)太过复杂而庞大,建议使用专为容器创建的init进程(例如 tini)。

如果使用专用 init 进程,则 init 进程具有 PID 1 并执行以下操作:

注册正确的信号处理程序。init进程会将信号传递给业务进程

回收僵尸进程

可以通过使用 docker run 命令的 --init 选项在 Docker 中使用此解决方案。但是目前kubernetes还不支持直接使用该方案,需要在启动命令前手动指定。

落地的难题

上面两种解决方案看似美好,实则在实施的过程中还是存在很多弊端。

方案一需要严格保证用户进程是首进程并且不能fork出多余的其他进程。 有时候我们在启动的时候需要执行一个shell脚本准备环境, 或者需要运行多个命令,例如‘sleep 10 && cmd‘, 此时容器内首进程便为shell,就会碰到问题一, 无法转发信号。 如果我们限制用户的启动命令不能包含shell语法, 对用户体验也不太好。 并且作为PASS平台,我们需要为用户提供一个简单友好的接入环境,帮用户处理好相关的问题。 从另外一方面考虑, 在容器环境下多进程在所难免,即使我们在启动时确保只运行一个进程,有时候在运行时过程中也会fork出进程。 我们无法确保我们所使用的第三方组件或者开源的方案不会产生子进程, 我们稍不注意就会碰到第二个问题,僵尸进程无法回收的囧境。

方案二中需要在容器中有一个init进程负责完成所有的这些任务, 当前业务普遍的做法是, 在构建镜像的时候里面自带init进程,负责处理上面所有的问题。 这种方案固然可行,但是需要让所有人都使用这种方式似乎有点难以接受。首先对用户镜像有侵入,用户必须修改已有的Dockerfile, 专门增加init进程 或者 只能在包含有该init进程的基础镜像上面进行构建。 其次管理起来比较麻烦,如果init进程升级,意味着全部镜像都得重新build,这似乎无法接受。即使使用docker默认支持的tini,也有一些其他问题,我们后面会谈到。

归根结底, 作为PASS平台,我们想给用户提供一个便捷的接入环境,帮助用户解决这些问题:

用户进程能够收到信号, 进行一些优雅的退出

允许用户产生多进程,并且在多进程的情况下帮助用户回收僵尸进程。

不对用户的运行命令做约束,允许用户填写各种shell格式的命令,都能够解决上述1和2问题

解决方案

如果我们想要对用户无侵入,则最好使用docker或kubernetes原生支持的方案。

上面已经介绍过了docker run --init选项, docker原生提供的init进程实则为tini。tini支持给进程组传递信号, 通过-g参数或者TINI_KILL_PROCESS_GROUP来进行开启该功能。 开启该功能后我们就可以将tini作为首进程,然后让它传递信号给所有的子进程。问题一就可以轻松解决。 例如我们执行 docker run -d --init ubuntu:14.04 bash -c "cd /home/ && sleep 100" 就会发现容器内的进程视图如下:

[email protected]:/# ps -ef

UID PID PPID C STIME TTY TIME CMD

root 1 0 2 14:50 ? 00:00:00 /sbin/docker-init -- bash -c cd /home/ && sleep 100

root 6 1 0 14:50 ? 00:00:00 bash -c cd /home/ && sleep 100

root 7 6 0 14:50 ? 00:00:00 sleep 100

此时1号docker-init进程,也就是tini进程, 负责转发信号到所有的子进程,并且回收僵尸进程, tini的子进程为6号bash进程, 它负责执行shell命令,可以执行多个命令。这里有一个问题就是: tini进程只会监听他的直接子进程,如果直接子进程退出则整个容器就视为退出了, 也就是本例中的6号bash进程。 如果我们往容器中发送SIGTERM,可能用户进程注册了信号处理函数, 收到信号后处理需要一定的时间完成,但是由于bash没有注册SIGTERM信号处理函数,会直接退出,进而导致tini退出,整个容器退出。用户进程的信号处理函数还没有执行完毕就被强制退出了。我们需要想办法让bash忽略掉这个信号,同事提到bash在交互模式下不会处理SIGTERM信号, 可以一试。 在启动命令前面加上bash -ci即可。发现使用bash交互模式启动用户进程就可以使bash忽略掉SIGTERM,然后等待业务的信号处理函数执行完毕整个容器再退出。

如此便完美解决了上述相关问题。 同时还收获了另外一个微不足道的好处:容器退出时更加快速。我们知道kubernetes中容器退出的逻辑和docker一样,先发送SIGTEMR 然后再发送SIGKILL, 对于大部分用户来说,都不会处理SIGTERM信号,容器内1号进程收到该信号后默认的行为是忽略该信号, 于是SIGTERM信号白白地被浪费掉,需要等待terminationGracePeriodSeconds之后才被删除。既然用户不处理SIGTERM,为什么不直接在收到SIGTERM之后就退出呐? 在当前我们的解决方案下如果用户有注册该信号处理函数,则能正常处理。 如果没有注册则容器在收到SIGTERM之后就马上退出,可以加快退出速度。

目前由于kubernetes中CRI并没有直接提供可以设置docker tini的方法,所以要想在kubernetes中使用tini就只能改代码了,笔者的集群中就是通过改代码来实现的。为了解决用户的痛点,我们有能力也有义务为合理的需求改代码,况且这个改动足够小,非常简单。

后记

在容器落地的过程中会碰到各种实际的问题,开源的方案可能无法覆盖到我们所有的需求,需要我们在精通社区的实现基础上进行轻微的变形即可完美适应企业内部的场景。

日记本
相关推荐
这次疫情教会我的道理
阅读 21596
学习Python可以做哪些副业,你是不是感觉自己错过了一个亿?
阅读 1263
吐槽一下我12岁的小叔子,真让我大开眼界啊!
阅读 940
武汉封城第四十三天:是谁模糊了我们的眼睛
阅读 1688
怀孕了
阅读 7095

原文地址:https://blog.51cto.com/14735789/2476432

时间: 2025-01-22 23:16:19

容器内init进程方案的相关文章

容器内应用日志收集方案

容器化应用日志收集挑战 应用日志的收集.分析和监控是日常运维工作重要的部分,妥善地处理应用日志收集往往是应用容器化重要的一个课题. Docker处理日志的方法是通过docker engine捕捉每一个容器进程的STDOUT和STDERR,通过为contrainer制定不同log driver 来实现容器日志的收集,缺省json-file log driver是将容器的STDOUT/STDERR 输出保存在磁盘上,然后用户就能使用docker logs <container>来进行查询. 在部署

LINUX PID 1和SYSTEMD PID 0 是内核的一部分,主要用于内进换页,内核初始化的最后一步就是启动 init 进程。这个进程是系统的第一个进程,PID 为 1,又叫超级进程

要说清 Systemd,得先从 Linux 操作系统的启动说起.Linux 操作系统的启动首先从 BIOS 开始,然后由 Boot Loader 载入内核,并初始化内核.内核初始化的最后一步就是启动 init 进程.这个进程是系统的第一个进程,PID 为 1,又叫超级进程,也叫根进程.它负责产生其他所有用户进程.所有的进程都会被挂在这个进程下,如果这个进程退出了,那么所有的进程都被 kill .如果一个子进程的父进程退了,那么这个子进程会被挂到 PID 1 下面.(注:PID 0 是内核的一部分

docker容器内应用检测失败总结

docker容器内应用检测失败故障总结 各位网友,各位同行大家: 今天在云平台中遇到了一个这样一个问题,在云平台上面docker容器中的应用,在监控客户端 中,显示应用检测失败的问题:以下是经常遇到的几个应用检测失败的常见解决办法如下所示: 问题描述:rds产品  docker容器中的应用检测失败:(备注:宿主机运行正常) 解决思路:1.一般情况下如果应用服务检测失败的话,首先查看一下这个应用的进程是否还在 可以使用命令ps -ef | grep +服务名称:查看一下服务的运行状态.查看一下服务

android init进程分析

android的init进程用来启动zygote进程,用来启动android世界.init进程的源码在顶层目录的/system/core/init使用 find -name Android.mk -exec grep -l "init" {} \;来查找源码,接下来的android服务程序也是使用这个指令来查找源码. /system/core/init/init.c 整个init进程的入口函数669 int main(int argc, char **argv) init_parse_

centos7 docker容器(二)运行和移除容器内应用详解

安装.运行和移除docker中的应用 运行和保存Docker容器 1.运行并保存基于Ubuntu Docker容器的nginx服务器.安装Nginx守护进程到Ubuntu启动容器: # docker run ubuntu bash -c "apt-get -y install nginx" 2.其次,在安装完Nginx包后,发出命令 docker ps -l 得到运行容器的ID或名称.运行以下命令: # docker ps -l 运行以下命令获得更改 # docker commit 5

阿里云专有云平台docker容器内应用故障总结

阿里云专有云平台docker容器内应用检测失败故障总结 各位网友,各位同行,大家好! 今天在阿里云专有云平台中,遇到了一些关于docker容器内应用检测失败的问题,现把今天的解 觉问题的心得和解决思路,分享给大家: 问题描述:在docker容器中rhs服务应用显示检测失败:(这里我只说一个例子就好了,然后解 决的方法和思路说一下: 解决方案和思路:1.首先先查看一下检测失败的报警系统: 2.进入服务器的后端,使用ps -ef 检查服务的进程,是否已经存在,如果不存在, 希,可以将其服务的应用程序

WinForm容器内控件批量效验是否允许为空?设置是否只读?设置是否可用等方法分享

WinForm容器内控件批量效验是否允许为空?设置是否只读?设置是否可用等方法分享 在WinForm程序中,我们有时需要对某容器内的所有控件做批量操作.如批量判断是否允许为空?批量设置为只读.批量设置为可用或不可用等常用操作,本文分享这几种方法,起抛砖引玉的作用,欢迎讨论! 1.  清除容器控件内里面指定控件的值的方法 /// <summary> /// 清除容器里面指定控件的值(通过控件的AccessibleName属性设置为"EmptyValue") /// </

android init进程分析 基本流程

(懒人最近想起我还有csdn好久没打理了,这个android init躺在我的草稿箱中快5年了,稍微改改发出来吧) android设备上电,引导程序引导进入boot(通常是uboot),加载initramfs.kernel镜像,启动kernel后,进入用户态程序.第一个用户空间程序是init, PID固定是1.在android系统上,init的代码位于/system/core/init下,基本功能有: 管理设备 解析并处理启动脚本init.rc 实时维护这个init.rc中的服务 init进程的

Linux下1号进程的前世(kernel_init)今生(init进程)----Linux进程的管理与调度(六)

日期 内核版本 架构 作者 GitHub CSDN 2016-05-29 Linux-4.5 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度-之-进程的创建 前言 Linux下有3个特殊的进程,idle进程(PID=0), init进程(PID=1)和kthreadd(PID=2) * idle进程由系统自动创建, 运行在内核态 idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产