当容器导致节点失效:理解僵尸进程和 PID 1

发布: (2025年12月24日 GMT+8 22:42)
6 min read
原文: Dev.to

Source: Dev.to

引子

在我职业生涯的早期,我目睹了一件事,这件事彻底改变了我对容器的看法。我们在 Kubernetes 上使用 Rocky Linux 节点运行 MySQL。一切看似正常,直到节点一个接一个地挂掉。罪魁祸首是什么?僵尸进程——数以百计的僵尸进程悄然累积,最终导致节点不堪重负。

这次事件让我领悟到一个根本真理:容器并不是轻量级的虚拟机。它们只是进程。

在 Linux 中,当一个进程结束执行时,它并不会直接消失。它会进入 僵尸 状态:进程已经完成,但它的条目仍然保留在进程表中。父进程必须使用 wait() 读取子进程的退出状态。只要父进程没有调用 wait(),子进程就会保持为僵尸。

父进程

Parent Process
      |
      |--- fork() ---> Child Process
      |                     |
      |                     | (does work)
      |                     |
      |                     v
      |                 Exits (becomes zombie)
      |                     |
      |<--- wait() ---------+
      |
      v
   Zombie cleaned up

在普通的 Linux 系统中,这并不是一个大问题。如果父进程在未调用 wait() 的情况下死亡,孤儿子进程会被 init 进程(PID 1)收养,后者会定期回收这些僵尸进程。

为什么容器会破坏此模型

当你在容器中运行而没有 init 进程时,你的应用程序会成为 PID 1。没有传统的 init 进程,因此你的应用现在需要负责回收僵尸进程。

FROM mysql:8.0
# MySQL process becomes PID 1
# It was never designed to be an init system

大多数应用程序——包括 MySQL——都不是为成为 init 进程而设计的。它们不会对孤儿子进程调用 wait(),所以当子进程死亡时,它们会变成僵尸进程,没人去清理它们。在节点上,ps aux | grep Z 显示出数百个僵尸 MySQL 辅助进程,每个进程虽然已经死亡,但仍占用进程表中的条目。

每个僵尸进程都包含:

  • 进程表中的一条记录
  • 一个 PID(而 PID 是有限的)

最终,你会耗尽 PID 或填满进程表,导致无法生成新进程。节点变得不稳定,服务会崩溃。

修复方案:Tini

解决方案出乎意料地简单:使用专为容器设计的合适 init 进程。Tini 是专为容器构建的极简 init 系统。它:

  • 以 PID 1 运行
  • 将你的应用程序作为子进程启动
  • 正确转发信号
  • 通过调用 wait() 来回收僵尸进程

实现

选项 1:在 Dockerfile 中安装

FROM mysql:8.0

# 安装 tini
RUN apt-get update && apt-get install -y tini

# 将 tini 设置为 entrypoint
ENTRYPOINT ["/usr/bin/tini", "--"]

# 你的实际命令
CMD ["mysqld"]

选项 2:使用 Docker 内置的 init

docker run --init mysql:8.0

选项 3:Kubernetes

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: mysql
    image: mysql:8.0
    # 对于 Kubernetes,建议将 tini 烘焙进镜像或使用已包含它的基础镜像

在 Kubernetes 中,最安全的做法是将类似 Tini 的 init 程序烘焙进镜像,因为依赖运行时标志在不同环境下并不具备可移植性。

更大的教训

这次事件挑战了我对容器的认知模型。我过去把容器当作“轻量级虚拟机”——一个运行自己小世界的隔离箱子。实际情况并非如此:容器本质上只是带有高级隔离(命名空间、cgroup)的进程。它与宿主共享内核。当该进程出现僵尸进程、消耗内存或耗尽 PID 等异常行为时,宿主系统也会受到影响。

理解这一点会改变你在以下方面的做法:

  • 调试容器问题
  • 设计容器镜像
  • 考虑资源限制与隔离

快速参考

场景会发生什么解决方案
App as PID 1, spawns children僵尸进程累积Use Tini or --init
App crashes without signal handling孤儿子进程变成僵尸进程Proper init + signal forwarding
Too many zombiesPID 用尽,节点不稳定Prevention via init system
  • 僵尸进程是正常的;只有在未被回收时才会成为问题。
  • 容器默认没有传统的 init。
  • 除非专为此设计,否则你的应用不应是 PID 1。
  • Tini(或 dumb-init)是简单的解决方案,应该作为标准做法。
  • 容器是进程,而不是虚拟机——永远不要忘记这一点。
Back to Blog

相关文章

阅读更多 »

翻页(不重置系统)

引言 新的一年并不是重置按钮。经验上没有 git reset --hard —— 没有可以抹去成功、失败或什么…的干净板块。

微型语言模型

Forem 动态 !Forem 标志 https://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.co...