容器的单进程模型

0X00 Linux 的进程关系

既然想搞清楚容器的单进程模型,那自然需要先复习一下 Linux 下的基本进程关系了。你说你用的是 Windows Container?不懂,不会,打扰了😢

我们知道 Linux 下会有一个 PID=1 的进程来带动其他进程,以前 PID=1 的进程是 init 后来大家都在用 systemd ,这里就不多说了,只来回顾一下「孤儿进程」和「僵尸进程」这两个概念。

这里来模拟一个场景,默认 PID=1 的是 systemd

  1. 打开运行一个新进程
  2. 新进程 fork 了一个子进程出来
  3. 子进程持续运行
  4. 此时父进程终止了
  5. 子进程成为孤儿进程(因为他的父进程挂了)
  6. 该子进程会交由 systemd 接管
  7. 当子进程结束之后 systemd 会替它「收尸」,也就是释放、回收资源之类的

还有另一个场景:某菜鸡程序员(可能是我)写了个程序,现在运行起来了。该进程会 fork 新进程,每隔一会儿就会 fork 一个,但是该进程并没有 wait/waitpid 这种操作(俗称管杀不管埋)。所以当子进程结束后资源并没有被回收,甚至 PID 都还占着。这种已经结束了但是没被回收的进程就叫僵尸进城噢不,叫僵尸进程

需要注意的一点就是,孤儿进程并不是什么大问题,systemd 会解决它的;僵尸进程也不都有问题,任何进程在结束之后和被回收之前,都处于这个状态,真正要注意的是源源不断产生僵尸进程的那个进程,就算只是一直占着 PID 也不是个事儿嘛。

0X01 容器的本质

好了现在已经回忆起僵尸进程和孤儿进程这两个概念了,接下来回忆一下容器的本质。简单来说的话容器(以 Docker for Linux 为例)并非虚拟化,而是在宿主机上运行的一个普通进程而已,只是通过 Linux 自身的一些特性将其与宿主机环境隔离开了而已。当然了,既然不是虚拟化也就没有宿主机这种说法,这里只是图个方便才这么说的。如果你想更多的了解容器技术本身,推荐下面这一系列文章

0X02 单进程模型

既然我们了解了上面两项知识,那么自然也就明白为什么容器里 EntryPoint 的进程是 PID=1 了。接下来我们假设你写了个普通的程序,在程序内部会 fork 一些子进程出来。然后将其搬到了容器里,那么你自己的这个程序运行之后,又 fork 了一堆子孙进程出来,现在你想让 PID=1 的进程负责收尸工作,那么 PID=1 的进程是哪个呢?就是你自己写的那个呀🧐

如果没搞错的话,一般自己写一个多进程的程序是不太会管僵尸进程子进程这些的,一切交给 systemd 就好了。但是现在 PID=1 的是自己写的那个程序,又没有这个收尸能力,所以才会有这么个「单进程模型」啦。

那怎么解决呢?有问题就有答案,你可以选择修改你的程序,让它监控子进程、僵尸进程,并且对其进行合理的回收♻️;也可以让每个容器只运行一个进程,多进程就干脆多容器。然后把相关联的几个容器间的网络、文件、数据都给打通,这样一来每个进程都是 PID=1,当它结束的时候直接容器就停止了,也就不会有回收资源的问题了。可是看起来虽然少了监控回收的工作,但是多了打通网络、打通文件系统、打通进程间数据共享的问题,还多了一大堆其他的隐藏工作。这时候知道的小伙伴就知道了,该 Kubernetes 上场了~

如果你不知道 Kubernetes 的话,可以先简单将其理解成一个容器编排工具,它里面有一个重要的概念叫做 pod。pod 中可以运行一个或多个容器,容器之间共享同一个网络命名空间,也就是说同一个 pod 里的容器 A 可以用 127.0.0.1 访问同处于一个 pod 中的其他容器;pod 中的所有容器也共享一个 Volume,也就是说简单的文件共享也是有的;并且它们之间甚至可以使用 Linux 下标准的进程间通信,例如信号量这些。

0X03 最后

综上所述,我们得出几条结论

  1. 由于将进程隔离起来,导致容器内部并没有 init/systemd 这种负责收尸的老大哥;
  2. 由于 1 的缘故,我们说容器使用的是单进程模型;
  3. 虽然是单进程模型,但是并非真的只能运行一个进程,你想的话可以自己实现一个 systemd 一样的东西在容器里面玩起来(但是并不推荐);
  4. 程序/业务真的复杂到这种程度的话,建议引入容器编排工具,例如 Kubernetes;

参考资料: