在kubernetes的架构设计中,同一个pod内有多个服务的情况下,每个服务作为一个容器是最佳实践,各容器通过pause容器共享网络命令空间。如果违背这一原则,在一个容器内运行了多个进程,则可能引发一些意料之外的问题。

僵尸进行无法回收

示例情景如下:

  1. 一个pod内只有一个容器
  2. 容器内运行两个进程(一个后台,一个前台)

该pod容器镜像的entrypoint脚本如下:

#!/bin/bash

/usr/sbin/sshd &

exec "$@"

传入的command参数为:

args:
  - '/usr/bin/gobgpd'
  - '-f'
  - '/etc/gobgpd/gobgpd.conf'

容器启动后,其中会有两个进程:

  1. sshd: 后台运行(启动会detach当前的父进程,并fork一个新的进程,其父进程为init进程)
  2. gobgpd: 前台运行(由于前面有exec,所以会替换当前entrypoint脚本的进程)

进入容器查看进程:

root@rs0-57c568fd64-6b8cs:/$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 14:17 ?        00:00:00 /usr/bin/gobgpd -f /etc/gobgpd/gobgpd.conf
root         7     1  0 14:17 ?        00:00:00 [sshd] <defunct>
root        12     1  0 14:17 ?        00:00:00 /usr/sbin/sshd
sshd        19     1  0 14:17 ?        00:00:00 [sshd] <defunct>
sshd        70     1  0 14:17 ?        00:00:00 [sshd] <defunct>
root        73     0  0 14:17 pts/0    00:00:00 bash
sshd        88     1  0 14:17 ?        00:00:00 [sshd] <defunct>
root        89    73  0 14:17 pts/0    00:00:00 ps -ef

可见gobgpd的进程号为1, 表明其成为了容器的init进程, sshd出现了多个进程,其中还有若干显示为defunct的僵尸进程,由于gobgpd为init进程,因此是其他所有进程的父进程。 sshd进程结束时,本应由其父进程进行回收处理,但其父进程(gobgpd)并不具备真正的init进程的功能,因此无法对僵尸进程进行回收。这样就会不断的产生新的僵尸进程,导致进程空间占满,直接影响pod所在的node。

最佳实践

最佳实践一定是将上述pod分为两个独立的容器, 一个运行gobgpd, 另一个运行sshd。每个容器中都只运行一个单一的前台的进程。

如果一定要在一个容器中运行多个进程,且这些进程之间逻辑上没有父子进程关系。那么需要避免这些进程成为init进程(因为后台进程会成为init进程的子进程)。方法有以下几种:

使用pause容器并共享进程空间

一个pod内有多个容器时,kubernetes会启动一个pause基础容器,用于共享各容器的网络空间,但各容器的进程空间仍是隔离的,可在yaml中开启共享进程空间:

shareProcessNamespace: true

开启后,在pod内的任何容器内,都可以看到init进程总是为/pause

root@rs0-78b495fddf-mt57t:/$ ps -ef 
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 14:35 ?        00:00:00 /pause
root         6     0  0 14:35 ?        00:00:00 /usr/bin/gobgpd -f /etc/gobgpd/gobgpd.conf
root        12     6  0 14:35 ?        00:00:00 [sshd] <defunct>
root        17     1  0 14:35 ?        00:00:00 /usr/sbin/sshd
root        92     0  0 14:35 pts/0    00:00:00 bash
root       106    92  0 14:35 pts/0    00:00:00 ps -ef

如上,pause成为了容器内的init进程(因为共享了进程空间,所有容器的init都是这个pause进程), 可以看到, sshd后台进程被init进程接管(其中还有一个defunct的sshd进程的原因是:sshd后台进程是从entrypoint脚本中启动的, 后台进程启动时,会先在init进程下fork一个sshd进程,然后终止entrypoint脚本进程下的sshd, 但由于启动脚本最后的exec命令将entrypoint进程替换为了gobgpd进程,成为了sshd的父进程,而gobgpd不是一个shell进程,没有回收子进程的能力,因此这个sshd进程成为了僵尸进程),虽然还有一个sshd僵尸进程,但却不会持续产生新的僵尸进程了。

这种方法的前提是pod内必须有多个容器,如果只有一个容器的话,kubernetes将不会创建pause容器,也就没有pause进程。

启动脚本中不用exec

通常,在entrypoint脚本中完成所有预备工作后,会使用exec将entrypoint进程替换为用户传入的命令行参数指定的服务进程,以便在服务退出后直接退出容器, 也方便在容器内部通过ps命令查看容器内实际运行的命令。

如果不采用第一种方法(使用pause容器),可以通过不使用exec命令来避免服务进程成为init进程,entrypoint脚本修改如下:

#!/bin/bash

/usr/sbin/sshd &

$@

如此一来, gobgpd进程便不会替代entrypiont脚本进程成为主进程, gobgpd和sshd都会成为entrypoint脚本进程的子进程, entrypoint是一个shell脚本,作为主进程,其具有回收终止的子进程的能力,因此不会有僵尸进程存留。

root@rs0-57c568fd64-bx2kh:/$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 15:05 ?        00:00:00 /bin/bash /usr/local/bin/docker-entrypoint.sh 
root         8     1  0 15:05 ?        00:00:00 /usr/bin/gobgpd -f /etc/gobgpd/gobgpd.conf
root        13     1  0 15:05 ?        00:00:00 /usr/sbin/sshd
root        83     0  0 15:06 pts/0    00:00:00 bash
root       143    83  0 15:07 pts/0    00:00:00 ps -ef

总结

总之,容器的设计原本就是单一的前台进程,应尽量将不同的服务放在不同的容器中,以避免产生问题。

Logo

开源、云原生的融合云平台

更多推荐