容器内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:
[root@425523c23893 /]# 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"
,容器内首进程还是业务进程,如下:
[root@c380600ce1c4 /]# 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"
就会发现容器内的进程视图如下:
root@24cc26039c4d:/# 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就只能改代码了,笔者的集群中就是通过改代码来实现的。为了解决用户的痛点,我们有能力也有义务为合理的需求改代码,况且这个改动足够小,非常简单。
后记
在容器落地的过程中会碰到各种实际的问题,开源的方案可能无法覆盖到我们所有的需求,需要我们在精通社区的实现基础上进行轻微的变形即可完美适应企业内部的场景。
容器内init进程方案的更多相关文章
- Docker容器内Mysql大小写敏感方案解决
Docker容器内Mysql大小写敏感方案解决 一.(lower_case_table_names)参数说明 二.Docker 部署 MySql 并修改为大小写不敏感 2.1直接在Docker启动的时 ...
- Docker/K8s 解决容器内时区不一致方案
转载自:https://cloud.tencent.com/developer/article/1433215 1.背景介绍 我们知道,使用 docker 容器启动服务后,如果使用默认 Centos ...
- jenkins容器内安装python3
前言 很多小伙伴可能在考虑 jenkins 拉取了 github 上的代码后,发现还越少 python3 环境,那能怎么办呢? 咨询了一位运维朋友给我的答案是,将 python3 挂载到容器工作目录上 ...
- Docker学习笔记 - 在运行中的容器内启动新进程
docker psdoker top dc1 # 容器情况# 在运行中的容器内启动新进程docker exec [-d] [-i] [-t] 容器名 [command] [args]docker ex ...
- 在docker宿主机上查找指定容器内运行的所有进程的PID
转载 https://www.cnblogs.com/keithtt/p/7591097.html 找到指定容器的所有进程的PID可以更方便的对容器进程进行管理,特别是在某些容器卡住无法连接的场景. ...
- Flume+Kafka收集Docker容器内分布式日志应用实践
1 背景和问题 随着云计算.PaaS平台的普及,虚拟化.容器化等技术的应用,例如Docker等技术,越来越多的服务会部署在云端.通常,我们需要需要获取日志,来进行监控.分析.预测.统计等工作,但是云端 ...
- 如何规避容器内做Java堆dump导致容器崩溃的问题
写在前边 最近公司生产环境的容器云上出了个性能问题,为了做性能分析,使用 JDK 自带的 jmap 收集堆dump,出现了内存溢出导致了容器崩溃. 本篇文章将带你探究,如何规避容器内做堆 dump 导 ...
- init进程解析rc文件的相关函数分析
init进程的源码文件位于system/core/init,其中解析rc文件语法的代码放在五个函数中, init_parse_config_file (init_parser.c), read_fil ...
- init进程 && 解析Android启动脚本init.rc && 修改它使不启动android && init.rc中启动一个sh文件
Android启动后,系统执行的第一个进程是一个名称为init 的可执行程序.提供了以下的功能:设备管理.解析启动脚本.执行基本的功能.启动各种服务.代码的路径:system/core/init,编译 ...
随机推荐
- day46-守护线程
#1.守护线程要注意的坑:下面代码只能打印出子线程开始,无法打印出子线程执行完毕,因为主线程在t.start()以后就结束了, #而子线程要睡眠1秒,所以子线程守护线程随着主线程的结束而结束了. fr ...
- mysql 子查询不支持limit
mysql执行如下语句 : SELECT * FROM `xxx_base_info` where yn =1 and xxx_keeper_id in (SELECT id FROM `user`w ...
- Xcode查看iOS崩溃与崩溃日志分析
一.造成崩溃的原因 1.代码中存在bug 2.Watchdog 超时机制 3.用户强制退出 4.低内存终止 5.其他违法系统规则的操作,大部分是内存问题 二.崩溃的类型 1.信号错误类 (1)EXC_ ...
- 《C 程序设计语言》练习1-4
#include<stdio.h> /*当celsius=0,1,...,100时,打印摄氏温度与华氏温度对照表; 浮点数版本*/ main () { float fahr,celsius ...
- Springmvc多视图
Springmvc多视图 多视图是一个方法可以返回json/xml等格式的数据 第一步:导入xml格式支持的jar包 spring-oxm-3.2.0.RC2.jar 第二步:配置支持多视图 < ...
- 树形dp(最小支配集)
http://poj.org/problem?id=3659 #include<iostream> #include<cstring> #include<algorith ...
- centos5.5 下面 lnmp环境遇到的小问题
A)nginx 启动:/www/nginx/sbin/nginx -c /www/nginx/conf/nginx.conf 查看: ps -ef | grep nginx 停止:强制停止所有Ngin ...
- 为什么前两年大热的VR创业突然冷了?
不得不说,如果不是<头号玩家>在国内的热映,人们似乎都要忘记VR这个行业了.<头号玩家>中那些带有极强真实色彩的游戏,其实就是VR进化的目标,甚至是巅峰!而里面的角色佩戴的设备 ...
- react全家桶从0搭建一个完整的react项目(react-router4、redux、redux-saga)
react全家桶从0到1(最新) 本文从零开始,逐步讲解如何用react全家桶搭建一个完整的react项目.文中针对react.webpack.babel.react-route.redux.redu ...
- 整数拆分-dp问题
Integer Partition In number theory and combinatorics, a partition of a positive integer n, also call ...