前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >为什么说容器是单进程模型

为什么说容器是单进程模型

作者头像
大彬
发布于 2019-09-25 07:59:06
发布于 2019-09-25 07:59:06
1.3K00
代码可运行
举报
文章被收录于专栏:一起学Golang一起学Golang
运行总次数:0
代码可运行

Go 语言现在的一个主要应用领域就是云原生技术,包括容器(以 Docker 为代表)、KubernetesPrometheus 等。后面将写一系列文章来介绍一下云原生技术栈中的关键技术。

过去两年很多大公司的一个主要技术方向就是将应用上云,在这个过程中的一个典型错误用法就是将容器当成虚拟机来使用,将一堆进程启动在一个容器内。但是容器和虚拟机对进程的管理能力是有着巨大差异的。不管在容器中还是虚拟机中都有一个一号进程,虚拟机中是 systemd 进程,容器中是 entrypoint 启动进程,然后所有的其他线程都是一号进程的子进程,或者子进程的子进程,递归下去。这里的主要差异就体现在 systemd 进程对僵尸进程回收的能力。

0. 僵尸进程

说到僵尸进程,这里简单介绍一下 Linux 系统中的进程状态,我们可以通过 ps 或者 top 等命令查看系统中的进程,比如通过 ps aux 在我的 ecs 虚拟机上面得到如下的输出。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[root@emr-header-1 ~]# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  0.0 190992  3568 ?        Ss   Mar16 289:04 /usr/lib/systemd/systemd --switched-root --system --de
root         2  0.0  0.0      0     0 ?        S    Mar16   0:05 [kthreadd]
root         3  0.0  0.0      0     0 ?        S    Mar16  13:01 [ksoftirqd/0]
root         5  0.0  0.0      0     0 ?        S<   Mar16   0:00 [kworker/0:0H]
root         7  0.0  0.0      0     0 ?        S    Mar16  14:41 [migration/0]
root         8  0.0  0.0      0     0 ?        S    Mar16   0:00 [rcu_bh]
root         9  0.0  0.0      0     0 ?        S    Mar16 243:19 [rcu_sched]
root        10  0.0  0.0      0     0 ?        S    Mar16   0:50 [watchdog/0]
root        11  0.0  0.0      0     0 ?        S    Mar16   0:39 [watchdog/1]
root        12  0.0  0.0      0     0 ?        S    Mar16  23:51 [migration/1]
root        13  0.0  0.0      0     0 ?        S    Mar16  15:44 [ksoftirqd/1]
root        15  0.0  0.0      0     0 ?        S<   Mar16   0:00 [kworker/1:0H]

我们可以看到排在第一位的就是前面说到的 1 号进程 systemd。其中的 STAT 那一列就是进程状态,这里的状态都是和 S 有关的,但是正常还有 R、D、Z 等状态。各个状态的含义简单描述如下:

  • S: Interruptible Sleep,中文可以叫做可中断的睡眠状态,表示进程因为等待某个资源或者事件就绪而被系统暂时挂起。当资源或者事件 Ready 的时候,进程轮转到 R 状态。
  • R: 也就是 Running,有时候也可以指代 Runnable,表示进程正在运行或者等待运行。
  • Z: Zombie,也就是僵尸进程。我们知道每个进程都是会占用一定的资源的,比如 pid 等,如果进程结束,资源没有被回收就会变成僵尸进程。
  • D: Disk Sleep,也就是 Uninterruptible Sleep,不可中断的睡眠状态,一般是进程在等待 IO 等资源,并且不可中断。D 状态相信很多人在实践中第一次接触就是 ps 卡住。D 状态一般在 IO 等资源就绪之后就会轮转到 R 状态,如果进程处于 D 状态比较久,这个时候往往是 IO 出现问题,解决办法大部分情况是重启机器。
  • I: Idle,也就是空闲状态,不可中断的睡眠的内核线程。和 D 状态进程的主要区别是可能实际上不会造成负载升高。

关于僵尸进程,这里继续讨论一下。对于正常的使用情况,子进程的创建一般需要父进程通过系统调用 wait() 或者 waitpid() 来等待子进程结束,从而回收子进程的资源。除了这种方式外,还可以通过异步的方式来进行回收,这种方式的基础是子进程结束之后会向父进程发送 SIGCHLD 信号,基于此父进程注册一个 SIGCHLD 信号的处理函数来进行子进程的资源回收就可以了。记住这两种方式,后面还会涉及到。

僵尸进程的最大危害是对资源的一种永久性占用,比如进程号,系统会有一个最大的进程数 n 的限制,也就意味一旦 1 到 n 进程号都被占用,系统将不能创建任何进程和线程(进程和线程对于 OS 而言,使用同一种数据结构来表示,task_struct)。这个时候对于用户的一个直观感受就是 shell 无法执行任何命令,这个原因是 shell 执行命令的本质是 fork。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[root@emr-header-1 ~]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 63471
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 131070
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 63471
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

1. 孤儿进程

前面说到如果子进程先于父进程退出,并且父进程没有对子进程残留的资源进行回收的话将会产生僵尸进程。这里引申另外一种情况,父进程先于子进程退出的话,那么子进程的资源谁来回收呢?

父进程先于子进程退出,这个时候我们一般将还在运行的子进程称为孤儿进程,但是实际上孤儿进程并没有一个明确的定义,他的状态还是处于上面讨论的几种进程状态中。那么孤儿进程的资源谁来回收呢?类 Unix 系统针对这种情况会将这些孤儿进程的父进程置为 1 号进程也就是 systemd 进程,然后由 systemd 来对孤儿进程的资源进行回收。

2. 单进程模型的本质

看完上面两节大家应该知道了虚拟机或者一个完整的 OS 是如何避免僵尸进程的。但是,在容器中,1 号进程一般是 entry point 进程,针对上面这种 将孤儿进程的父进程置为 1 号进程进而避免僵尸进程 处理方式,容器是处理不了的。进而就会导致容器中在孤儿进程这种异常场景下僵尸进程无法彻底处理的窘境。

所以说,容器的单进程模型的本质其实是容器中的 1 号进程并不具有管理多进程、多线程等复杂场景下的能力。如果一定在容器中处理这些复杂情况的,那么需要开发者对 entry point 进程赋予这种能力。这无疑是加重了开发者的心智负担,这是任何一项大众技术或者平台框架都不愿看到的尴尬之地。

3. 如何避免

除了第二节讨论的开发者自己赋予 entrypoint 进程管理多进程的能力,这里我更推荐借助 Kubernetes (下面简称 k8s)来做这件事情。我想现在应该也没有人对容器进行人工管理了,大部分人应该都转向了容器编排和调度工具 k8s 阵营了(对于那些还在使用 swarm 的一小波人,我劝你们早日弃暗投明 :))。

k8s 中可以将多个容器编排到一个 pod 里面,共享同一个 Linux NameSpace。这项技术的本质是使用 k8s 提供一个 pause 镜像,展开来说就是先用 pause 镜像实例化出 NameSpace,然后其他容器加入这个 NameSpace 从而实现 NameSpace 共享。突然意识到这块需要有容器和 NameSpace 的技术背景,限于篇幅,希望你可以自行搜索这种技术背景。或者我下一篇文章讨论一下容器技术的本质。

言归正传,我们来介绍一下 pause。pause 是 k8s 在 1.16 版本引入的技术,要使用 pause,我们只需要在 pod 创建的 yaml 中指定 shareProcessNamespace 参数为 true,如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  shareProcessNamespace: true
  containers:
  - name: nginx
    image: nginx
  - name: shell
    image: busybox
    securityContext:
      capabilities:
        add:
        - SYS_PTRACE
    stdin: true
    tty: true

创建 pod。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
kubectl apply -fshare-process-namespace.yaml

attach 到 pod 中,ps 查看进程列表。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/ # ps ax
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    8 root      0:00 nginx: master process nginx -g daemon off;
   14 101       0:00 nginx: worker process
   15 root      0:00 sh
   21 root      0:00 ps ax

我们可以看到 pod 中的 1 号进程变成了 /pause,其他容器的 entrypoint 进程都变成了 1 号进程的子进程。这个时候开始逐渐逼近事情的本质了:/pause 进程是如何处理 将孤儿进程的父进程置为 1 号进程进而避免僵尸进程 的呢?我们看一下源码,git repo: pause.c

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#define STRINGIFY(x) #x
#define VERSION_STRING(x) STRINGIFY(x)

#ifndef VERSION
#define VERSION HEAD
#endif

static void sigdown(int signo) {
  psignal(signo, "Shutting down, got signal");
  exit(0);
}

static void sigreap(int signo) {
  while (waitpid(-1, NULL, WNOHANG) > 0)
    ;
}

int main(int argc, char **argv) {
  int i;
  for (i = 1; i < argc; ++i) {
    if (!strcasecmp(argv[i], "-v")) {
      printf("pause.c %s\n", VERSION_STRING(VERSION));
      return 0;
    }
  }

  if (getpid() != 1)
    /* Not an error because pause sees use outside of infra containers. */
    fprintf(stderr, "Warning: pause should be the first process\n");

  if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 1;
  if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 2;
  if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                             .sa_flags = SA_NOCLDSTOP},
                NULL) < 0)
    return 3;

  for (;;)
    pause();
  fprintf(stderr, "Error: infinite loop terminated\n");
  return 42;
}

重点关注一下 35 行和 13 行,这个不就是我们上面说的

除了这种方式外,还可以通过异步的方式来进行回收,这种方式的基础是子进程结束之后会向父进程发送 SIGCHLD 信号,基于此父进程注册一个 SIGCHLD 信号的处理函数来进行子进程的资源回收就可以了。

SIGCHLD 信号的处理函数核心就是这一行 while (waitpid(-1, NULL, WNOHANG) > 0) ,其中 WNOHANG 参数是为了让父进程直接返回不阻塞。(注:处理函数调用wait家族函数回收资源)

4. 总结

容器化改造的路非常漫长,对于很多业务同学在改造的过程中由于一些思维的惯性就想把容器当成一个虚拟机来使用,这个可能会导致非常多的问题。或许我们可以探究一些容器的设计模式,以便进行更好的实践。

最近加了很多有趣的人,很多人表示 “原来你就是 xxx,你的博客怎么最近两年没怎么写了” ,“我之前看了很多你的文章,可惜后来不怎么更新了” 。

过去一段时间,我一度认为写博客的投入产出比太低而提不起笔,看来我是错的。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-09-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一起学Golang 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Kubernetes Pod 网络精髓:pause 容器详解
当检查你的 Kubernetes 集群的节点时,在节点上执行 docker ps 命令,你可能会注意到一些被称为“暂停”(pause)的容器,例如:
米开朗基杨
2020/02/14
9.6K0
Kubernetes Pod 网络精髓:pause 容器详解
【Linux】:进程信号(再谈信号保存和信号捕捉)
🌈 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止
IsLand1314
2024/11/26
3280
【Linux】:进程信号(再谈信号保存和信号捕捉)
kubernetes pod为什么需要pause容器?
当我们检查 kubernetes 集群的 node 节点时,我们使用 docker ps 查看时会发现一些名为 pause 的容器在节点上运行。
YP小站
2021/01/04
3.1K0
kubernetes pod为什么需要pause容器?
面试官问:孤儿进程和僵尸进程,你造吗~
那时刚写公众号,当时记录的学习笔记,现在看来,之前记录的有一个错误的地方,当时也没察觉到。
Java小咖秀
2020/07/30
9060
面试官问:孤儿进程和僵尸进程,你造吗~
Linux孤儿进程和僵尸进程详解(wait和watipid)
       当一个进程使用了fork函数会创建一个新的子进程,那么就会存在两个问题,一个是子进程没有结束但是父进程结束了,另一个是子进程结束了但是父进程没有回收子进程的资源。这两种情况就产生了孤儿进程和僵尸进程。下面会通过实际进程运行的示例来进行说明。首先先来明确一个知识点,在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。
Ch_Zaqdt
2020/03/04
3.6K0
Linux孤儿进程和僵尸进程详解(wait和watipid)
Linux系统-僵尸&孤儿进程
作者介绍:简历上没有一个精通的运维工程师。请点击上方的蓝色《运维小路》关注我,下面的思维导图也是预计更新的内容和当前进度(不定时更新)。
运维小路
2024/11/01
2000
Linux系统-僵尸&孤儿进程
1.并发编程~先导篇(上)
并发 :一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
逸鹏
2018/08/14
1.5K0
1.并发编程~先导篇(上)
一次 Docker 容器内大量僵尸进程排查分析
前段时间线上的一个使用 Google Puppeteer 生成图片的服务炸了,每个 docker 容器内都有几千个孤儿僵死进程没有回收,如下图所示。
挖坑的张师傅
2022/05/13
2K0
一次 Docker 容器内大量僵尸进程排查分析
Linux进程管理命令及状态详解
查看进程树。 linux中,每一个进程都是由其父进程创建的。此命令以可视化方式显示进程,通过显示进程的树状图来展示进程间关系。如果指定了pid了,那么树的根是该pid,不然将会是init(pid: 1)。
bboy枫亭
2020/09/22
2K0
Linux进程管理命令及状态详解
Kubernetes系列学习文章 - Pod的深入理解(四)
| 导语 从这篇文章开始,我们将详细介绍K8S的各个组件。学习一项技术,理论先行,只有充分的了解了内在原理才能在日后的维护和调优方面有所思路。
宝哥@上云专家
2019/06/10
13.5K3
Kubernetes系列学习文章 - Pod的深入理解(四)
小说python中的孤儿进程
然而,在实际应用中,孤儿进程虽然不会给系统造成直接性的危害,但更多时候会对业务造成一些影响,如当子进程为一个基于tcp的socket服务时,会造成主进程再次启动时无法启动,端口被占用。主进程退出了,子进程会因为无法获得某些资源,而变成业务上的"僵尸进程",这实际也是资源浪费。对于一些有进程监控的服务来说,可能会造成业务主服务无法重启,或是进程不可控。
用户2196567
2018/09/20
1.8K0
小说python中的孤儿进程
Kubernetes中的Pause容器到底是干嘛的
k8s.gcr.io 这个地址是需要连外网才可以拉取到,导致 pause 镜像拉不下来,Pod无法启动。以前都没关注过 pause 这个容器,它是啥,做什么用的,怎么在 Pod 里没看到过他,本文将带你了解 pause 容器。
用户1107783
2023/09/22
4.6K0
Kubernetes中的Pause容器到底是干嘛的
如何查看并杀死僵尸进程
In UNIX System terminology, a process that has terminated,but whose parent has not yet waited for it, is called a zombie. 在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他, 那么他将变成一个僵尸进程. 在fork()/execve()过程中,假设子进程结束时父进程仍存在,而父进程fork()之前既没安装SIGCHLD信号处理函数调用 wai
小小科
2018/05/02
4K0
【Linux】从零开始认识进程 — 中篇
我们可以使用fopen ("log.txt",“w”)来进行使用,该函数会在路径下创建一个新文件log.txt。即可验证进程所处路径:
叫我龙翔
2024/03/21
1180
【Linux】从零开始认识进程 — 中篇
清理linux中的僵尸进程
什么是僵尸进程 Linux 中的僵尸进程有时也称为失效或死进程。它们是已完成执行的进程,但它们的条目并未从进程表中删除。 进程状态 Linux 维护着所有正在运行的进程及其状态的进程表。让我们简要概述各种进程状态: 正在运行 (R):这些进程当前正在运行或可运行。 等待 (S/D):这些是等待事件或资源的进程。等待可以是可中断睡眠 (S) 或不可中断睡眠 (D)。 停止(T):我们可以通过发送适当的信号来停止Linux 进程。 僵尸(Z):当一个进程完成它的任务时,它会释放它正在使用的系统资源并清理它的内存
入门笔记
2022/06/02
3.8K0
docker的reap问题
在使用docker容器的时候,应该了解“PID1僵尸进程reap”问题。如果使用的时候不加注意,可能会导致出现一些意想不到的问题。
大蟒传奇
2018/07/31
1.2K0
docker的reap问题
Linux进程——Linux进程与进程优先级
前言:在上一篇了解完一部分常见的进程状态后,我们先来把剩下的进程状态了解一下,再来进入进程优先级的学习!
Eternity._
2024/06/14
8620
Linux进程——Linux进程与进程优先级
孤儿进程和僵尸进程的概念及进程回收(wait函数及waitpid函数)
一.孤儿进程 孤儿进程可以理解为一个子进程的父进程英年早逝(父进程先于子进程退出),就将这样的一个进程称为孤儿进程,在linux操作系统上。孤儿进程被init进程收养,此时孤儿进程的ppid==1,即init进程的pid == 1。也就是说init进程变成孤儿进程的父进程(干爹)。
lexingsen
2022/02/24
2.3K0
孤儿进程和僵尸进程的概念及进程回收(wait函数及waitpid函数)
探索Linux下进程状态 | 僵尸进程 | 孤儿进程
重复查看代码运行状态:while :; do ps ajx | head -1 && ps ajx | grep testStatus | grep -v grep; sleep 1; done
南桥
2024/04/02
2960
探索Linux下进程状态 | 僵尸进程 | 孤儿进程
Python Web学习笔记之并发编程的孤儿进程与僵尸进程
1、前言   之前在看《unix环境高级编程》第八章进程时候,提到孤儿进程和僵尸进程,一直对这两个概念比较模糊。今天被人问到什么是孤儿进程和僵尸进程,会带来什么问题,怎么解决,我只停留在概念上面,没有深入,倍感惭愧。晚上回来google了一下,再次参考APUE,认真总结一下,加深理解。 2、基本概念 我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作
Jetpropelledsnake21
2018/06/20
6650
相关推荐
Kubernetes Pod 网络精髓:pause 容器详解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验