皇冠体育寻求亚洲战略合作伙伴,皇冠代理招募中,皇冠平台开放会员注册、充值、提现、电脑版下载、APP下载。

首页科技正文

大众论坛日照社区:容器内init进程方案

admin2023-03-11371

背景

进程标识符 (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平台,我们想给用户提供一个便捷的接入环境,帮助用户解决这些问题:

  1. 用户进程能够收到信号, 进行一些优雅的退出
  2. 允许用户产生多进程,并且在多进程的情况下帮助用户回收僵尸进程。
  3. 不对用户的运行命令做约束,允许用户填写各种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就只能改代码了,笔者的集群中就是通过改代码来实现的。为了解决用户的痛点,我们有能力也有义务为合理的需求改代码,况且这个改动足够小,非常简单。

后记

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

,

申博Sunbet

申博Sunbet www.9cx.net是Sunbet娱乐的官方网站。申博用20多年的时间,诠释了高品质、高效率、高信誉。开放的Sunbet、Sunbet等业务备受申博用户的追捧。

网友评论

6条评论
  • 2021-01-10 00:04:04

    Allbet欧博官网欢迎进入allbet欧博官网。allbet欧博官网开放ALLBET欧博真人客户端、Allbet代理网页版、Allbet会员网页版、Allbet会员注册、Allbet代理开户、Allbet电脑客户端下载、Allbet手机版下载等业务。太有心意了

  • 2021-02-18 00:06:11

    Allbet欢迎进入allbet欧博官网。allbet欧博官网开放ALLBET欧博真人客户端、Allbet代理网页版、Allbet会员网页版、Allbet会员注册、Allbet代理开户、Allbet电脑客户端下载、Allbet手机版下载等业务。我不淡定了

  • 2021-10-06 00:02:18

    菜宝钱包(caibao.it)是使用TRC-20协议的Usdt第三方支付平台,Usdt收款平台、Usdt自动充提平台、usdt跑分平台。免费提供入金通道、Usdt钱包支付接口、Usdt自动充值接口、Usdt无需实名寄售回收。菜宝Usdt钱包一键生成Usdt钱包、一键调用API接口、一键无实名出售Usdt。我都看十遍了

  • 2023-03-02 00:12:29

    农历新年,古称元旦、元正、岁旦、新岁等,又俗称过年、过大年,辛亥革命以后称春节,其起源蕴含着深邃的文化内涵,在传承发展中承载了丰厚的历史文化底蕴,是中国传统节日中最隆重的节日。人们在辞旧迎新之际总结过往,企盼新程,其中所寄寓的除旧立新、为善行孝、尊礼重道等良好教益,是中华民族几千年来经久传延的美德。时至今日,诸如贴春联、包饺子、压岁钱、守岁拜年等习俗已经融进了我们的生活和血液,在一年一度的贺岁中强化着彼此的文化认同。紫禁城作为明清两代的皇宫,在“过年”这一良辰令节,把深宫与民间的礼俗进一步融合强化,萃取升华,蕴含着对美好生活的憧憬向往,充满着尊老敬贤的人文情怀,追求着崇德向善的民族精神,这正是我们要延续的文化血脉。实名羡慕

热门标签