Featured image of post k8s的pod是如何实现的?

k8s的pod是如何实现的?

pod

Pod中文直译为豆荚,一个豆荚中可能存在多个豆子,他们长相可能不尽相同,但是都是为了发芽做准备,类似地,在 Kubernetes 中,Pod 是容器组的概念,一个 Pod 可以包含多个容器,这些容器共享网络、存储等资源,虽然每个容器可能功能不尽相同,通过组合最终体现为某一个完整的能力对外服务。

那它是如何做到资源共享的呢?这得从容器的实现开始讲起,容器的本质是一个特殊的进程,特殊在为其创建了 NameSpace 隔离运行环境,并用 Cgroups 控制资源开销,还借助了一些 Linux 网络虚拟化技术解决了网络通信的问题。

Pod 所做的则是让多个容器加入同一个 NameSpace 以实现资源共享。

Linux Namespace

什么是Namespace?Namespace中文直译为命名空间,它是操作系统内核在不同进程间实现的一种「环境隔离机制」。

举例来说:现在有两个进程A,B。他们处于两个不同的 PID Namespace 下:ns_A / ns_B。在ns_A下,A 进程的 PID 可以被设置为1,在 ns_B 下,B 进程的 PID 也可以设置为1。但是它们两个并不会冲突,因为 Linux PID Namespace 对 PID 这个资源在进程 A,B 之间做了隔离。A 进程在 ns_A 下是不知道 B 进程在 ns_B 下面的 PID 的。

这种环境隔离机制是实现容器技术的基础。因为在整个操作系统的视角下,一个容器表现出来的就是一个进程。

Linux 一共构建了 6 种不同的 Namespace,用于不同场景下的隔离:

  1. Mount - isolate filesystem mount points (隔离文件系统挂载点 --> 每个进程都存在于一个mount Namespace里面,mount Namespace为进程提供了一个文件层次视图。如果不设定这个flag,子进程和父进程将共享一个mount Namespace,其后子进程调用mount或umount将会影响到所有该Namespace内的进程, 如果子进程在一个独立的mount Namespace里面,就可以调用mount或umount建立一份新的文件层次视图。)
  2. UTS - isolate hostname and domainname(隔离主机名和域名信息)
  3. IPC - isolate interprocess communication (IPC) resources (隔离进程间通信 -->用于隔离进程间通讯所需的资源( System V IPC, POSIX message queues),PID命名空间和IPC命名空间可以组合起来用,同一个IPC名字空间内的进程可以彼此看见,允许进行交互,不同空间进程无法交互)
  4. PID - isolate the PID number space (隔离进程的ID --> linux通过命名空间管理进程号,同一个进程,在不同的命名空间进程号不同,进程命名空间是一个父子结构,子空间对于父空间可见。)
  5. Network - isolate network interfaces (隔离网络资源 --> Network Namespace为进程提供了一个完全独立的网络协议栈的视图。包括网络设备接口,IPv4和IPv6协议栈,IP路由表,防火墙规则,sockets等等。一个Network Namespace提供了一份独立的网络环境,就跟一个独立的系统一样。)
  6. User - isolate UID/GID number spaces (隔离用户和用户组的ID)

这些东西我们都可以在/proc/$pid/ns这个目录下找到(图我就不放了,自己去看看)。

k8s的Pod实现

共享的基础实现

假设我们要创建两个容器,一个容器作为web应用(nginx),另外一个作为调试容器,正常来说两个容器分别属于不同的Namespace,我是查不到另外的容器的进程的,我们应该如何做才能实现这个需求呢?

  1. 启动一个nginx容器(a: 允许Namespace被共享,b: 挂载文件系统)
  2. 再启动一个容器加入上一个容器的NameSpace,并且挂载同一个文件树。

具体操作如下:

  • 启动一个web容器
1
2
$ docker run -d --name nginx --ipc="shareable" -v $PWD/log:/var/log/nginx -v $PWD/html:/usr/share/nginx/html nginx
# 默认情况下,Docker 的 IPC Namespace 是私有的,我们可以使用 --ipc="shareable" 来指定允许共享, -v 参数的作用就不多讲了, 挂载文件。
  • 接下来启动 busybox 容器,并加入到 nginx 容器的 NET、IPC、PID NameSpace 中,同时,我们共享 nginx 容器的 Volume ,以便可以访问 nginx 的日志文件
1
$ docker run -d --name busybox --net=container:nginx --ipc=container:nginx --pid=container:nginx -v $PWD/log:/var/log/nginx yauritux/busybox-curl /bin/sh -c 'while true; do sleep 1h; done;'

两个容器都启动后,就可以在 busybox 容器中直接调试 nginx 容器的资源了。

[root@vm ~]# echo “hello pod” > $PWD/html/index.html

[root@vm ~]# docker exec -it busybox ps

PID USER TIME COMMAND

1 root 0:00 nginx: master process nginx -g daemon off;

29 101 0:00 nginx: worker process

30 101 0:00 nginx: worker process

31 root 0:00 /bin/sh -c while true; do sleep 1h; done;

37 root 0:00 sleep 1h

38 root 0:00 ps

[root@vm ~]# docker exec -it busybox curl localhost

hello pod

[root@vm ~]# docker exec -it busybox tail /var/log/nginx/access.log

127.0.0.1 - - [30/Mar/2023:08:07:58 +0000] “GET / HTTP/1.1” 200 10 “-” “curl/7.81.0” “-”

这个我们就实现了一个简单版本的pod,但是有一个问题:由于 Namespace 是由 nginx 容器创建的,如果 nginx 意外崩溃,那么所有 Namespace 都会一同被删除,busybox 容器也会被终止。

parse容器

显然让业务容器充当共享基础容器是不可取的,必须保证每个容器都是对等的关系,而不是父子关系,所以k8s引入了parse容器。

Pause 容器,又叫 Infra 容器,为了解决共享基础容器的安全问题, Kubernetes 会在每个 Pod 里,额外起一个 Infra 容器来共享整个 Pod 的 Namespace 。

Pause 容器会在 Pod 创建时首先启动,并创建 Namespace 、配置网络 IP 地址及路由等相关信息,等 Pause 容器启动完成后,其它容器才接着启动,并与 Pause 容器共享 Namespace,这样,每个容器就都可以访问 Pod 中其他容器的资源了。

Pod结束时,这个容器会最后退出,可以说,Pause 容器的生命周期就相当于是整个 Pod 的生命周期。