pid namespaces销毁触发内核crash

Time: 八月 4, 2015
Category: docker & lxc, namespaces

最近在测试pid namespaces的过程中发现一个问题:就是当机器OOM的时候,杀掉了一个有pid namespace的进程,这个进程在回收的过程中,触发了内核crash

内核crash的地方是在回收进程pid的时候踩了空指针,内核版本是2.6.32

所以在低版本内核中,如果pid namespace使用不正确,可能会带来致命的稳定性问题

pid namespace是实现容器的基础技术之一,docker和lxc中都使用了pid namespace来为容器提供独立隔离的进程体系,实现容器之间的PID隔离,但是我看了一下docker和lxc的实现,实现方式很简单,在低版本内核上是极有可能触发这个bug的,我们来了解一下docker和lxc的进程隔离实现方案

关于pid namespace的实现原理,可以参考: Namespaces in operation, part 3: PID namespaces

1. PID1:init进程

容器在创建pid namespace之后,负责创建pid namespace的进程会成为新pid namespace下的首进程,也就是俗称init进程,也叫PID1进程

从用户态的角度来看,init进程是所有进程的父进程,所一个进程体系里的根

从内核态来看,init进程有更多的特殊含义:

  1. the owner of new namespace
  2. 进程组管理,如果一个进程daemonize了,它会脱离原理的进程组体系,init进程会成为他的父进程
  3. 子进程回收,当子进程退出后,负责回收工作,并且如果PID1进程挂掉,则同一个pid namespace内的进程都会被内核干掉
  4. 等等

所以,如果使用pid namespace来实现容器之间的进程隔离,就需要非常妥善的处理PID1进程,需要考虑这个进程的实现方式、职能等其他因素。我在测试的时候发现的这个bug,其实就和PID1进程有关,详细来说就是:

当容器发生OOM的时候,内核会根据oom算分策略选择一个进程来杀掉,但是很有可能是会选到PID1进程,但PID1进程被干掉时,内核会先把其他进程杀掉,然后再销毁namespace,然后这个过程中,内核踩空指针直接crash掉了

所以原则上,当一个容器退出时,我们应该确保容器的init进程最后退出

2. 开源的进程隔离方案

了解到这个问题之后,再来看看我们现在开源系统里的pid namespace的实现方案,看看init进程的退出机制是什么样的?

在开源的容器虚拟化方案实现里,容器的进程隔离主要有两种实现方式:

  1. docker: 由进入容器的第一个进程负责创建pid namespace, 当该进程退出时,容器销毁
  2. lxc: 容器启动时,在容器内启动一个特殊的init进程,由init进程来负责创建pid namespace,容器内的其他进程通过共享的方式使用同一个pid namespace

2.1. docker

在docker的实现了,容器有一个entry point的定义,所谓entry point是指container的入口,类似与程序中的main()函数

docker将entry point作为容器内的第一个进程,这个进程在创建的时候,内核为其初始化pid namespace以及其他虚拟化设置,entry point程序在容器内的进程PID是1,也就是我们前面说的init进程

docker这种方式的问题在于:

  1. 容器的生命周期不可控,生命周期完全依赖于进入容器的第一个进程,这个通常是业务实例的首进程,换句话说,如果业务代码不够健壮,首进程在运行期间异常crash掉,会对容器造成致命性破坏,容器内的其他进程随之会被内核杀掉
  2. 如果触发了内核OOM,由于这种方式实现的pid namespace缺陷性,会导致内核在销毁其他进程和namespace时触发内核crash,正如前面我们提到的这种情况

不过,从另外一个角度来看,这个可能是定位的问题,docker不是面向容器的,它是面向实例的,以及docker的一整套生态系统其实都是以实例为基础的,所以这种方案对docker来说是非常简单而且容易理解的

2.2. lxc

和docker不同的是,在lxc的实现里,lxc使用一个特殊的init进程来完成pid namespace的管理,默认情况下,lxc使用/sbin/init作为容器内的第一个进程,/sbin/init负责维护pid namespace并负责容器内子进程的所有回收工作。相比docker的实现,lxc的init进程才更像真正意义上的PID1进程

lxc这种实现方案的好处在于:

  1. init进程非常健壮,容器生命周期可控、稳定

但是这个设计仍有没有没有解决内核OOM时可能触发内核crash的问题,其根本原因还是在于,不管是lxc的实现还是docker的实现,PID1进程总是会在cgroup子系统里的,所以内核OOM的时候仍不可避免的会被杀掉

Leave a Comment