k8s容器内如何避免僵尸进程

548 阅读5分钟

之前有文章讨论了僵尸进程,以及脚本中出现僵尸进程时候,主进程调用wait可以一定程度上避免出现僵尸进程。但是我发现,并不是在调用完wait后,僵尸进程就一定不会存在了。

参考:github.com/milvus-io/m…
那么如何在k8s环境中,避免僵尸进程的出现呢?
本文提出两种方案来避免僵尸进程的出现。

僵尸进程产生的原理

参考:juejin.cn/post/728895…

一:操作系统中

实际上,任何一个进程都会存在僵尸进程阶段,只不过,如下两种情况就可以避免僵尸进程出现:

  • 情况1: 如果父进程还存在,但是父进程没有正确调用wait/waitpid,则僵尸进程就不会消失。
  • 情况2: 如果父进程早于子进程结束,那么子进程结束后,会由init进程(进程号为1)接管,init调用wait,同样不会出现僵尸进程。

二:容器环境中

容器环境中,进程号为1的进程是容器主进程,该进程不具备回收僵尸进程的能力,因此,当容器内产生孤儿进程托管给主进程(进程号为1)的进程后,就变成了没有人处理的僵尸进程

解决方法

方法一:

  • k8s 中 pause 容器 是所有容器的 父容器(parent container),它有2个作用

    • 它是pod中 Linux命名空间 共享的基础
    • 启用 PID 命名空间 共享,pod 中 PID 1 的 init 进程有它维护,并接收收割僵尸进程

那么,如果把pause进程变成进程号为1的容器主进程,是不是就可以处理孤儿进程了呢?
答案是肯定的~~~ 有如下两种方法,能够将pause容器变成主进程

  • kubelet 通过配置 --docker-disable-shared-pid=false 开启共享 pid namespace,也可以在 yml 中指定
  • 可以在yaml编排文件中指定
cat << EOF >> test.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
spec:
  selector:
    matchLabels:
      app: pod1
  replicas: 1
  template:
    metadata:
      labels:
        app: pod1
    spec:
      shareProcessNamespace: true
      containers:
      - name: sshd
        image: circleci/sshd:0.1
        env:
        - name: PUBKEY
          value: "abc"
      - name: pod1
        image: busybox
        securityContext:
          capabilities:
            add:
            - SYS_PTRACE
        stdin: true
        tty: true
        ports:
        - name: p80
          containerPort: 80
        - name: p22
          containerPort: 22
EOF

在编排文件的spec中,加入 shareProcessNamespace: true

从Kubernetes v1.17+起,shareProcessNamespace这一特性已经stable。顾名思义,Pod中容器将共享同一进程命名空间。通过在Pod的Sepc中设置shareProcessNamespace:true 来启用该特性。

开启后,可以在容器中,看到如下进程列表:

[root@sc-master-1 ~]# kubectl  -n milvus-1 exec -it elasticsearch-etcd-2 --  ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
65535          1       0  0 03:09 ?        00:00:00 /pause
1001           7       0  1 03:09 ?        00:03:55 etcd
1001       53340       0  0 07:46 pts/0    00:00:00 ps -ef

如上,可以看到pause容器变成了进程号为1的主进程,将会负责处理孤儿进程。

参考1:www.xiexianbin.cn/kubernetes/…
参考2:kubernetes.io/zh-cn/docs/…

注意

  • 容器进程将不再具有PID1。一些容器镜像拒绝在没有PID 1的情况下启动(例如,使用systemd的容器)或运行诸如kill -HUP 1之类的命令来发出容器进程信号。在具有共享进程名称空间的Pod中,kill -HUP 1将向Pod沙箱发出信号(/在以上示例中为/pause)。
  • 进程信息对Pod中的其他容器可见:这包括/proc中所有可见的信息,例如作为参数或环境变量传递的密码。这些仅受常规Unix权限保护。
  • 通过/proc/$pid/rootlink,容器文件系统对Pod中的其他容器可见。

方法二:

参考一: www.xiexianbin.cn/docker/dock…
参考二:cloud-atlas.readthedocs.io/zh_CN/lates…
参考三:cloud-atlas.readthedocs.io/zh_CN/lates…

tini 是一个针对容器开发的、精简的 init 服务。tini 的作用是生成子进程、避免僵尸进程生成、转发信号量(pid 为 1 时)到子进程并等待它退出。使用 tini 可以优雅的结束容器内的进程。

参考:github.com/krallin/tin…

背景:

  • Linux 中 pid 为 1 的 init 进程可以接受中断信号量,并将该信号量转发到所有子进程
  • 非 init 需要程序实现转发信号的功能才能关闭其他程序,一般的 bashsh 不具备上述功能
  • 使用 docker stop 时,容器内 pid 为 1 进程将接受中断信号量

打包镜像:

FROM rockylinux:9.2.20230513

ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
# Run your program under Tini
# or docker run your-image /your/program ...
COPY <<EOF /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=0
EOF
RUN dnf update -y && dnf install epel-release -y   && dnf -y install s3cmd  && dnf install -y wget  && dnf -y install net-tools  && dnf install -y telnet  && dnf -y install traceroute  && dnf -y install tcpdump  && dnf install -y fio  && dnf -y install bind-utils  && dnf install -y kubectl-1.24.16-0 && dnf install -y vim && dnf install -y expect  && dnf install -y git && dnf install -y procps && dnf install psmisc -y && chmod +x /tini
ENTRYPOINT ["/tini", "--"]
CMD [ "sleep","100000" ]

#docker run --rm --name app -e MX_MEM=128m -v /tmp/apps:/data -p 18080:8080 -p 17070:7070 registry.knowdee.com/tests/app:v1.0.0


#docker build    -t registry.knowdee.com/library/tools:v-rock9.2  . -f ./tools.dockerfile --progress=plain

# dnf --showduplicates list kubectl

yaml编排文件:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
  name: midware
  namespace: tools
spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: midware
  serviceName: midware
  template:
    metadata:
      annotations:
        # prometheus.io/port: "8000"
        # prometheus.io/scrape: "true"
      labels:
        app: midware
    spec:
      tolerations:
      - effect: NoExecute
        key: taint.knowdee.io/apps
        operator: Exists
      containers:
      - args:
        - /bin/sh
        - -c 
        - |
          git config --global user.name "knowdee"
          git config --global user.email "it@knowdee.com"
          sleep 30000s
        envFrom:
        - configMapRef:
            name: midware-config
        volumeMounts:
        - name: kubecfg
          mountPath: /root/.kube
        - mountPath: /data
          name: data
        image: registry.knowdee.com/library/tools:v-rock9.2
        imagePullPolicy: IfNotPresent
        name: midware
        # ports:
        # - containerPort: 8000
        #   name: http
        #   protocol: TCP
        resources:
          requests:
            cpu: "1"
            memory: 512Mi
      dnsPolicy: ClusterFirst
  volumeClaimTemplates:
  - apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      creationTimestamp: null
      name: data
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 200Mi
      volumeMode: Filesystem
  updateStrategy:
    type: RollingUpdate

在启动后的容器中,可以看到:

root           1       0  0 06:27 ?        00:00:00 /tini -- /bin/sh -c git config --global user.name "knowdee" git config --global user.email "it@knowdee.com" cp /data/.s3cfg /root/
root           7       1  0 06:27 ?        00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 30000s
root          73       0  0 06:27 pts/0    00:00:00 /bin/bash
root         793      73  0 08:32 pts/0    00:00:00 ps -ef

tini进程为进程号为1的进程,则负责孤儿进程的处理,防止出现僵尸进程。

方法三:

docker init, docker自带的init进程(即tini) tini, 可回收孤儿进程/僵尸进程,kill进程组等 dumb-init, 可管理进程,重写信号等 经过测试,tini进程只能回收前台程序,对于后台程序则无能为力(例如nohup, &启动的程序),dumb-init在主进程退出时,会传递信号给子进程,符合预期。

开启dumb-init进程的dockerfile如下,tini也类似 github.com/Yelp/dumb-i…

FROM nginx:alpine

# tini
# RUN apk add --no-cache tini
# ENTRYPOINT ["/sbin/tini", "-s", "-g", "--"]

# dumb-init
RUN wget -O /usr/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64
RUN chmod +x /usr/bin/dumb-init
ENTRYPOINT ["/usr/bin/dumb-init", "-v", "--"]

CMD ["nginx", "-g", "daemon off;"]

原文链接:blog.csdn.net/u012986012/…