首页 > 其他分享 >关于pause容器

关于pause容器

时间:2022-11-14 20:35:54浏览次数:60  
标签:容器 pause PID namespace nginx 关于 进程

当检查Kubernetes集群的节点时,在节点上执行命令docker ps,用户可能会注意到一些被称为pause的容器,例如:

$ docker ps
CONTAINER ID        IMAGE                           COMMAND ...
...
3b45e983c859        gcr.io/google_containers/pause-amd64:3.0    "/pause" ...
...
dbfc35b00062        gcr.io/google_containers/pause-amd64:3.0    "/pause" ...
...
c4e998ec4d5d        gcr.io/google_containers/pause-amd64:3.0    "/pause" ...
...
508102acf1e7        gcr.io/google_containers/pause-amd64:3.0    "/pause" ...

Kubernetes中的pause容器可以说是网络模型的精髓,理解pause容器能够更好地理解Kubernetes Pod的设计初衷。

我们知道Kubernetes的Pod抽象基于Linux的namespace和cgroups,为容器提供了隔离的环境。从网络的角度看,同一个Pod中的不同容器犹如运行在同一个主机上,可以通过localhost进行通信。

为什么要发明Pod呢?直接使用Docker容器不好吗?Docker容器非常适合部署单个软件单元。但是当你想要一起运行多个软件时,尤其是在一个容器里管理多个进程时,这种模式会变得有点麻烦。Kubernetes非常不建议“富容器”这种方式,认为将这些应用程序部署在部分隔离并且部分共享资源的容器组中更为有用。为此,Kubernetes为这种使用场景提供了一个称为Pod的抽象。

原则上,任何人都可以配置Docker来控制容器组之间的共享级别 — 只需创建一个父容器,并创建与父容器共享资源的新容器,然后管理这些容器的生命周期。在Kubernetes中,pause容器被当作Pod中所有

容器的“父容器”,并为每个业务容器提供以下功能:

  • 在Pod中,它作为共享Linux namespace(Network、UTS等)的基础;
  • 启用PID namespace共享,它为每个Pod提供1号进程,并收集Pod内的僵尸进程。

pause容器源码

在Kubernetes中,pause容器运行着一个非常简单的进程,它不执行任何功能,基本上是永远“睡觉”的,源代码在Kubernetes项目的build/pause/目录中。它比较简单,完整的源代码如下所示:

/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

static void sigdown(int signo) {
  psignal(signo, "Shutting down, got signal");
  exit(0);
}

static void sigreap(int signo) {
  while (waitpid(-1, NULL, WNOHANG) > 0);
}

int main() {
  if (getpid() != 1)
    /* Not an error because pause sees use outside of infra containers. */
    fprintf(stderr, "Warning: pause should be the first process\n");

  if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 1;
  if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 2;
  if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                             .sa_flags = SA_NOCLDSTOP},
                NULL) < 0)
    return 3;

  for (;;)
    pause();
  fprintf(stderr, "Error: infinite loop terminated\n");
  return 42;
}

如上所示,这个pause容器运行一个非常简单的进程,它不执行任何功能,一启动就永远把自己阻塞住了(见pause()系统调用)。正如你看到的,它当然不会只知道“睡觉”。它执行另一个重要的功能——即它扮演PID 1的角色,并在子进程成为“孤儿进程”的时候,通过调用wait()收割这些僵尸子进程。这样就不用担心我们的Pod的PID namespace里会堆满僵尸进程了。这也是为什么Kubernetes不随便找个容器(例如Nginx)作为父容器,让用户容器加入的原因。

1.从namespace看pause容器

在Linux系统中运行新进程时,该进程从父进程继承了其namespace。在namespace中运行进程的方法是通过取消与父进程的共享namespace,从而创建一个新的namespace。以下是使用unshare工具,在新的PID、UTS、IPC和Mount namespace中运行shell的示例。

sudo unshare --pid --uts --ipc --mount -f chroot rootfs /bin/sh

一旦进程运行,用户可以将其他进程添加到该进程的namespace中以形成一个Pod,Pod中的容器在其中共享namespace。通过setns系统调用能够将新进程添加到现有namespace中,Docker也提供命令行功能自动完成此过程。下面来看一下如何使用pause容器和共享namespace创建Pod。

首先,我们使用Docker启动pause容器,以便可以将其他容器添加到Pod中,如下所示:

$ docker run -d --name pause -p 8080:80 gcr.io/google_containers/pause-amd64:3.0

注意:我们将主机端口8080映射到pause容器的80端口上,而不是nginx容器上,因为pause容器设置了nginx将要加入的初始网络命名空间。

PS:如果下载不了镜像,建议更换为:registry.cn-hangzhou.aliyuncs.com/google_containers/pause-amd64:3.0

然后,我们在Pod中运行其他容器,分别是Nginx代理和ghost博客应用。

Nginx代理的后端配置成http://127.0.0.1:2368,也就是ghost进程监听的地址,如下所示:

$ cat <<EOF >> nginx.conf
error_log stderr;
events { worker_connections  1024; }
http {
    access_log /dev/stdout combined;
    server {
        listen 80 default_server;
        server_name example.com www.example.com;
        location / {
            proxy_pass http://127.0.0.1:2368;
        }
    }
}
EOF

$ docker run -d --name nginx \
-v `pwd`/nginx.conf:/etc/nginx/nginx.conf \
--net=container:pause \
--ipc=container:pause \
--pid=container:pause nginx

注意:在执行上面的程序的时候,可能会遇到这个错误

docker: Error response from daemon: can't join IPC of container 85970c2e34fef8be83897c5f2f7ff64c8bd6ee37f16bd9c2b9337a25dd4e46d4: non-shareable IPC (hint: use IpcMode:shareable for the donor container).

问题原因:由于ipc的模式被默认设置为private导致的

解决方法:修改/etc/docker/daemon.json文件,添加字段

{
  "default-ipc-mode": "shareable"
}

完成后,重启docker进程

sudo systemctl daemon-reload
sudo systemctl restart docker

为ghost博客应用程序创建另一个容器,如下所示:

$ docker run -d --name ghost \
--net=container:pause \
--ipc=container:pause \
--pid=container:pause ghost

访问http://localhost:8080/

 

 

在这个例子中,我们将pause容器指定为要加入其namespace的容器。如果访问http://localhost:8080/,那么应该能够看到ghost通过Nginx代理运行,因为pause、Nginx和ghost容器之间共享network namespace,如图所示。

 

 

如果你认为所有这些都很复杂,是的,确实如此。我们甚至还没有讨论如何监视和管理这些容器的生命周期。Kubernetes的好处是通过POD,Kubernetes为你管理所有这些。

通过Pod,Kubernetes屏蔽了以上所有复杂度。

2.从PID看pause容器

关于僵尸进程的收割,见:

https://www.ianlewis.org/en/almighty-pause-container

在UNIX系统中,PID为1的进程是init进程,即所有进程的父进程。init进程比较特殊,它维护一张进程表并且不断地检查其他进程的状态。init进程的其中一个作用是当某个子进程由于父进程的错误退出而变成了“孤儿进程”,便会被init进程“收养”并在该进程退出时回收资源。

进程可以使用fork和exec两个系统调用启动其他进程。当启动了其他进程后,新进程的父进程就是调用fork系统调用的进程。fork用于启动正在运行的进程的另一个副本,而exec则用于启动不同的进程。每个进程在操作系统进程表中都有一个条目。这将记录有关进程的状态和退出代码。当子进程运行完成后,它的进程表条目仍然保留,直到父进程使用wait系统调用获得其退出代码后才会清理进程条目。这被称为“收割”僵尸进程,并且僵尸进程无法通过kill命令清除。

僵尸进程是已停止运行但进程表条目仍然存在的进程,父进程尚未通过wait系统调用进行检索。从技术层面来说,终止的每个进程都算是一个僵尸进程,尽管只是在很短的时间内发生的。当用户程序写得不好并且简单地省略wait系统调用,或者当父进程在子进程之前异常退出并且新的父进程没有调用wait检索子进程时,会出现较长时间的僵尸进程。系统中存在过多僵尸进程将占用大量操作系统进程表资源。

当进程的父进程在子进程完成前退出时,OS将子进程分配给init进程。init进程“收养”子进程并成为其父进程。这意味着当子进程退出时,新的父进程(init进程)必须调用wait获取其退出代码,否则其进程表项将一直保留,并且它也将成为一个僵尸进程。同时,init进程必须拥有“信号屏蔽”功能,不能处理某个信号逻辑,从而防止init进程被误杀。所以不是随随便便一个进程都能当init进程的。

容器使用PID namespace对PID进行隔离,因此每个容器中均可以有独立的init进程。当在主机上发送SIGKILL或者SIGSTOP(也就是docker kill或者docker stop命令)强制终止容器的运行时,其实就是在终止容器内的init进程。一旦init进程被销毁,同一PID namespace下的进程也随之被销毁。

在容器中,必须要有一个进程充当每个PID namespace的init进程,使用Docker的话,ENTRYPOINT进程是init进程。如果多个容器之间共享PID namespace,那么拥有PID namespace的那个进程须承担init进程的角色,其他容器则作为init进程的子进程添加到PID namespace中。

下面给出一个例子来说明用户容器和pause容器的PID关系。

先启动一个pause容器:

$ docker run -idt --name pause gcr.io/google_containers/pause-amd64:3.0

再运行一个busybox容器,加入pause容器的namespace(Network、PID、IPC)中:

$ docker run -itd --name busybox \
--net=container:pause \
--pid=container:pause \
--ipc=container:pause \
busybox

上述这种加入pause容器的方式也是Kubernetes启动Pod的原理。

接下来,让我们进入busybox容器查看里面的进程,发现里面PID=1的进程是/pause:

$ docker exec -it busybox ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    7 root      0:00 sh
   19 root      0:00 ps aux

我们完全可以在父容器中运行Nginx,并将ghost添加到Nginx容器的PID命名空间:

$ docker run -d --name nginx \
    -v `pwd`/nginx.conf:/etc/nginx/nginx.conf \
    -p 8080:80   nginx

$ docker run -d --name ghost \
--net=container:nginx \
--pid=container:nginx \
--ipc=container:nginx \
ghost

在这种情况下,Nginx将承担PID 1的作用,并将ghost添加为Nginx的子进程。虽然这样貌似不错,但从技术角度看,Nginx需要负责ghost进程的所有子进程。例如,如果ghost在其子进程完成之前异常退出,那么这些子进程将被Nginx收养。但是,Nginx并不是设计用来作为一个init进程运行并收割僵尸进程的。这意味着将会有很多这种僵尸进程,并且这种情况将持续整个容器的生命周期。

3.在Kubernetes中使用PID namespace共享/隔离

共享/隔离Pod内容器的PID namespace是一个见仁见智的问题。支持共享的人觉得方便了进程间通信,例如可以在容器中给另外一个容器内的进程发送信号,还不用担心僵尸进程回收问题。

在Kubernetes 1.8版本之前,默认是启用PID namespace共享的,非将Kubelet的标志--docker-disable-shared-pid设置成true,来禁用PID namespace共享。然而在Kubernetes 1.8版本以后,情况刚好相反,在默认情况下,Kubelet标志--docker-disable-sharedpid设置成true,如果要开启,还要设置成false。下面就来看看Kubernetes提供的关于是否共享PID namespace的downward API

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  shareProcessNamespace: true
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  - name: shell
    image: busybox
    imagePullPolicy: IfNotPresent
    securityContext:
      capabilities:
        add:
        - SYS_PTRACE
    stdin: true
    tty: true

如上所示,podSpec.shareProcessNamespace指示了是否启用PID namespace共享。

(1)执行上面的文件

(2)获取容器 shell,执行 ps

$  kubectl exec -it nginx -c shell -- sh
/ # ps aux
PID   USER     TIME  COMMAND
    1 65535     0:00 /pause
    6 root      0:00 nginx: master process nginx -g daemon off;
   34 root      0:00 sh
   39 101       0:00 nginx: worker process
   40 101       0:00 nginx: worker process
   47 root      0:00 sh
   52 root      0:00 ps aux
/ # 

(3)可以使用 /proc/$pid/root 链接访问另一个容器的文件系统。

# 如有必要,更改 “6” 为 Nginx 进程的 PID
$ cat /proc/6/root/etc/nginx/nginx.conf

输出类似于:

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
...

 

PS:shareProcessNamespace,在调试容器的时候也能有些作用,不过更多使用的是kubectl debug 。目前很多容器使用了distroless镜像,没有bash等命令行,这种情况下,就可以找一个具有相应命令行的容器,如:koolkits等,将它和待调试容器置入到同一个POD,开启进程命名空间共享,就能看到另外一个容器中的内容了。

 

文章引用链接:

1、《Kubernetes网络权威指南:基础、原理与实践 杜军》

2、《The Almighty Pause Container》

3、《在 Pod 中的容器之间共享进程命名空间》

标签:容器,pause,PID,namespace,nginx,关于,进程
From: https://www.cnblogs.com/cosmos-wong/p/16890305.html

相关文章