10

k8s网络学习(2)——pause容器的秘密

 3 years ago
source link: https://niyanchun.com/k8s-network-2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上一篇文章介绍了单机容器网络的基础知识,本篇开始介绍K8s的网络。本文假定你已经对K8s的各种基本概念(比如Node、Pod、Service等)非常了解,如果还不了解,可以参考我之前的博客: Kubernetes架构及资源关系简单总结

K8s的网络主要需要解决4个问题:

  1. 高度耦合的容器间的通信问题 (本文)
  2. Pod与Pod之间的通信
  3. Pod和Service之间的通信
  4. 外部系统和Service之前的通信

本文介绍第一个问题:高度耦合的容器间的通信问题。什么是高度耦合?举个例子,比如我使用的博客系统typecho就是由nginx+php+mysql组成。如果要容器化部署的话,那就需要3个容器:nginx、php、mysql。这3个容器单独都无法提供博客功能,只有组合到一起时才可以成为一个完整可用的对外系统,这种就是高度耦合的容器。如果你是使用docker去部署这么一个博客系统,那就需要自己去配置这3个容器之间的网络通信。而K8s在设计之初就考虑到了这个问题,提出了Pod的概念。简单理解,一个Pod有点类似于传统的虚机、物理机,不过非常轻量化,Pod也是K8s里面最小的部署单元。对Pod还不熟悉的朋友可以参考官方文档: Pods ,本文就不介绍Pod的细节了,只介绍Pod的网络部分。从最佳实践的角度讲,高度耦合的容器一般部署在一个Pod里面,这样他们就类似于部署在同一个虚机或者物理机上面,可以直接通过本地回环地址(127.0.0.1)进行通信。那这个是如何实现的呢?

秘密就在 Pause容器 。K8s的pause容器有两大作用:

  1. 它作为Pod内的“系统容器”,提供一个基础的Linux Namespace,以供其它用户容器加入。
  2. 如果开启了PID Namespace共享的话,它还作为Pod的“init进程”(pid=1,也即所有其它容器的父容器,所有其它进程的父进程),负责僵尸进程的回收。

下面分别介绍。

Pause容器的代码

这里先贴一下pause容器的代码,方便后面分析。 Github: pause.c :

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

#define STRINGIFY(x) #x
#define VERSION_STRING(x) STRINGIFY(x)

#ifndef VERSION
#define VERSION HEAD
#endif

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(int argc, char **argv) {
  int i;
  for (i = 1; i < argc; ++i) {
    if (!strcasecmp(argv[i], "-v")) {
      printf("pause.c %s\n", VERSION_STRING(VERSION));
      return 0;
    }
  }

  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;
}

这段C代码去除注释和空行,还不到50行,功能也非常简单,但其作用却非常大,真的很神奇。

功能1:共享Namespace

在Linux系统中,当我们创建一个新的子进程的时候,该进程就会继承父进程的Namespace,K8s的Pod就是模拟这种方式。创建Pod时,里面第一个被创建的容器永远都是pause容器(只不过是系统自动创建的),pause容器创建好之后,才会创建用户容器。系统(CRI和CNI)会为pause容器创建好Namespace,后面的用户容器都加入pause的Namespace,这样大家就处于同一个Namespace了,类似于大家都在一个“虚拟机”里面部署。对于网络而言,就是共享Network Namespace了。下面用docker模拟一下k8s的Pod过程。

第一步:模拟创建Pod的“系统容器”pause容器。

➜ ~ docker run -d --name pause -p 8080:80  --ipc=shareable  registry.aliyuncs.com/k8sxio/pause:3.2
184cfffc577e032902a293bf1848f6353bd8a335f57d6b20625b96ebca8dfaa0
➜  nginx docker ps -l
CONTAINER ID        IMAGE                                    COMMAND             CREATED             STATUS              PORTS                  NAMES
184cfffc577e        registry.aliyuncs.com/k8sxio/pause:3.2   "/pause"            13 seconds ago      Up 11 seconds       0.0.0.0:8080->80/tcp   pause

这里的操作就是创建一个docker容器,并将容器内的80端口映射到外部的8080端口。需要注意的是为了让其他容器可以共享该容器的IPC Namespace,需要增加 --ipc=shareable

第二步:创建“用户容器” nginx。nginx默认是监听80端口的,先创建一个配置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;
         }
     }
}

配置非常简单,将所有请求转发到127.0.0.1:2368。然后启动nginx容器,不过该容器共享pause容器的Namespace,而不是像docker默认行为那样创建自己的Namespace:

➜ ~ docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf --net=container:pause --ipc=container:pause --pid=container:pause nginx
660af561bc2d5936b3da727067ac30663780c5f7f73efe288b5838a5044c1331
➜  nginx docker ps -l
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
660af561bc2d        nginx               "/docker-entrypoint.…"   4 seconds ago       Up 3 seconds                            nginx

此时,curl一下本机的8080(也就是pause容器的80)端口,就可以看到以下信息:

➜  ~ curl 127.0.0.1:8080
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.19.6</center>
</body>
</html>

也就是说pause容器的80端口上面是nginx服务,这是因为它们都处于一个Network Namespace。这里报502是因为我们再nginx配置里面配了所有请求都转发到2368端口,但这个端口没有服务监听。

第三步:再创建一个“用户容器” ghost(一个博客系统,默认监听2368端口):

➜  ~ docker run -d --name ghost --net=container:nginx --ipc=container:nginx --pid=container:nginx ghost
b455cb94dcec8a1896e55fc3ee9ac1133ba9f44440311a986a036efe09eb9227
➜  ~ docker ps -l
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
b455cb94dcec        ghost               "docker-entrypoint.s…"   4 seconds ago       Up 3 seconds                            ghost

容器启动后,我们直接在浏览器访问本机的8080端口,就可以看到ghost的web页面了:

ayINFfv.png!mobile

此时,高度耦合的两个容器nginx+ghost就在一个Namespace里面了,特别是处于同一个Network Namespace(可以通过127.0.0.1相互访问),让他们的通信变得极其方便。如果再加上pause容器,那他们3个就是k8s里面的一个Pod了。其关系如下图(图片来自第一篇引用的文章):

BJB3QbQ.png!mobile

这整个过程便是我们在k8s中部署容器时Pod里面发生的过程。

最后再回头看一下 pause.c ,我们发现该代码最后是一个死循环+pause调用,pause函数的作用是让当前进程暂停,进入睡眠状态,直到被信号中断。先不考虑信号中断的话(下面介绍),这个程序几乎就是无限睡眠。这样设计的原因是在Linux里面, 要维持Namespace的存在,就必须有一个属于该Namespace的进程或者文件等对象存在 (虽然有手动删除Namespace的命令,但如果有对象存在,删除其实是个假删除,真正删除发生在最后一个对象消亡时)。所以为了维护Pod里面pause创建的Namespace,pause就必须一直存在。

好,最后总结一下,当我们在k8s里面创建容器时,CRI(容器运行时,比如docker)和CNI(容器网络接口,负责容器网络)首先创建好pause容器这个系统容器,这就包含创建并初始化各种Namespace(Network Namespace部分由CNI负责)。之后再创建用户容器,用户容器加入pause的Namespace。其中Network Namespace是默认就共享的,PID Namespace则需要看配置。

功能2:充当系统init进程

在Linux系统中,pid=1的进程我们称之为“init”进程,是内核启动的第一个用户级进程,现在比较新的Linux发行版的init进程就是systemd进程。这个进程有许多工作,其中一个重要工作就是负责“收养孤儿进程”,防止产生太多僵尸进程。简单介绍一下相关的基本概念:Linux系统维护了一个进程表,记录每个进程的状态信息和退出码(exit code),当一个进程退出时,它在表中的信息会一直保留,直到其父进程调用 wait (包括 waitpid )获取其退出码。所谓僵尸进程就是进程已经退出了,但它的信息还在进程表里面。正常情况下,进程退出时父进程会马上查询该表,并回收子进程的相关资源,所以僵尸进程的持续状态一般都很短。但如果(1)父进程启动子进程之后没有调用 wait 或者(2)父进程先于子进程挂掉了,那子进程就会变成僵尸进程。如果是第2种情况,即父进程先于子进程死掉了,那操作系统就会把init进程设置为该父进程所有子进程的父进程,即init进程收养了该父进程的所有子进程。当这些子进程退出时,init进程就会充当父进程的角色,从而避免长时间的僵尸进程。但对于第1种情况,一般认为是代码有缺陷,这种情况因为子进程的父进程存在(只是没有调用 wait 而已),init进程是不会做处理的。此时子进程会成为僵尸进程长期存在,如果要消除这种僵尸进程,只能kill掉父进程。

而pause容器的第二个功能就是充当这个init进程,负责回收僵尸进程。从上面的代码可以看到,pause启动的时候会判断自己的pid是否为1。不过 如果要实现该功能,则Pod内的所有容器必须和pause共享PID Namespace 。在K8s 1.8版本之前默认是开启PID Namespace共享的,之后版本默认关闭了,用户可以通过 --docker-disable-shared-pid=true/false 自行设置。开启PID Namespace的好处就是可以享受pause回收僵尸进程的功能,并且因为容器同处于一个PID Namespace,进程间通信也会变得非常方便。但也有一些弊端,比如有些容器进程的PID也必须为1(比如systemd进程),这就会和pause容器产生冲突,另外也涉及一些安全问题。这里只讨论回收僵尸进程的这个功能。为了更确切的观察,我们再启动一个busybox容器,并加入到之前pause容器的Namespace里面:

➜  ~ docker run -idt --name busybox  --net=container:pause --ipc=container:pause --pid=container:pause busybox:1.28
5b6a92bab7c0861b4bebc96115c43ae502b38f788a9b40a9d6c5f8bc77f8fc2d

➜  ~ docker exec -it busybox sh
/ # ps -ef
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    7 root      0:00 nginx: master process nginx -g daemon off;
   35 101       0:00 nginx: worker process
   46 root      0:00 sh
   59 1000      0:23 node current/index.js
  127 root      0:00 sh
  133 root      0:00 ps -ef

可以看到,pause容器是系统内的“init进程”(pid=1)。然后看一下上面 pause.c ,里面有对 SIGCHLD 信号的处理,该信号是子进程退出时发给父进程的。注册的信号处理函数 sigreap 里面调用了 waitpid 来等待所有的子进程(第一个参数为-1),这样就可以实现僵尸进程的回收了。那能否用其它进程作为init进程呢?比如不使用特殊的pause容器,而是直接创建一个nginx容器,然后ghost加入nginx进程?一般是不行的,因为普通的进程里面不一定有wait所有子进程的操作。不过在容器化里面,僵尸进程的问题已经不是很严重了,因为最佳实践都是一个容器里面一个进程,当进程挂掉的时候,容器也就销毁了。可能在传统的docker里面,有时为了方便高耦合的几个应用通信,会在一个容器里面启动多个进程,但在k8s里面基本不会有这种场景,因为有Pod这种设计的存在(这真是一个过分优秀的设计)。

总结

K8s通过Pod这个优秀的设计解决了高度耦合容器之间的通信,其机制也非常简单:就是增加了一个非常简单但却很精妙的pause容器,该容器由系统创建维护,对用户透明,它负责创建好Linux Namespace,用户容器直接加入即可。同时,该容器还有回收僵尸进程的作用。不得不说,Kubernetes的很多 设计 真的非常符合Unix/Linux设计哲学:小而简单(small, simple)!

Reference:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK