Kubernetes 的 Pod 抽象基于 Linux 的 namespace 和 cgroups,为一组容器共同提供了隔离的运行环境。了解 KVM 底层的应该知道,虚拟机与容器一样底层都使用 cgroups 做资源配额,而且概念上都抽离出一个隔离的运行时环境,只是区别在于资源隔离的实现。提出 Pod Sandbox 概念就是为 Kubernetes 兼容不同运行时环境(甚至包括虚拟机!)预留空间,让运行时根据各自的实现来创建不同的 Pod 。对于基于 hypervisor 的运行时(KVM,kata 等),Pod Sandbox 就是虚拟机。对于 Linux 容器,Pod Sandbox 就是 Linux Namespace(Network Namespace 等)。
Q&A
- 创建pause容器的时候怎么指定是否创建新的ns还是共享父进程的?
- Kubernetes Pod 网络精髓:pause 容器详解
- 起一个bash,默认的netns为宿主机的ns
Linux 的 namespace
Linux Namespace
提供了一种内核级别隔离系统资源的方法,通过将系统的全局资源放在不同的Namespace
中,来实现资源隔离的目的。不同Namespace
的程序,可以享有一份独立的系统资源。目前Linux中提供了六类系统资源的隔离机制,分别是:
Mount
: 隔离文件系统挂载点UTS
: 隔离主机名和域名信息IPC
: 隔离进程间通信PID
: 隔离进程的IDNetwork
: 隔离网络资源User
: 隔离用户和用户组的ID
namespace API 的使用方法:clone()
、unshare()
和setns()
系统调用会使用CLONE_NEW*
常量来区别六种不同的 namespace 类型。以及还有/proc
下的部分文件。
CLONE_NEWNS
: 用于指定Mount NamespaceCLONE_NEWUTS
: 用于指定UTS NamespaceCLONE_NEWIPC
: 用于指定IPC NamespaceCLONE_NEWPID
: 用于指定PID NamespaceCLONE_NEWNET
: 用于指定Network NamespaceCLONE_NEWUSER
: 用于指定User Namespace
注意:可以通过挂载的方式打开文件描述符(只要文件描述符是被打开的,即使当初创建它的进程被销毁,ns会依然存在):
touch ~/mnt
mount --bind /proc/${pid}}/ns/mnt~/mnt
/proc/${pid}/ns
readlink /proc/${pid}/ns/net
该目录下的每个软链接的内容是一串字符,由 namespace 类型和 inode number 组成:
$ ll /proc/${pid}/ns/
cgroup -> cgroup:[4026531835]
ipc -> ipc:[4026531839]
mnt -> mnt:[4026531840]
net -> net:[4026534999]
pid -> pid:[4026531836]
pid_for_children -> pid:[4026531836]
user -> user:[4026531837]
uts -> uts:[4026531838]
特点:
- 若几个进程中对应 namespace 软链接内容一致,则这几个进程同属于同一个 namespace;
- 即使 namespace 中的进程全部终结了,只要其软链接文件一直处于 open 状态,则 namespace 将一直存在;
network namespace
CLONE_NEWNET
指代的是 network namespace。
- 创建namespace:
clone()
系统调用 - 维持namespace存在:/proc/${pid}/ns
- 往namespace里添加进程:
setns()
系统调用 - 帮助进程逃离namespace:
unshare()
系统调用
docker network
四种模式:docker network ls
- none 模式(= clone(CLONE_NEWNET) )
- bridge 桥接模式(= clone(CLONE_NEWNET ) && netlink add)
- host 模式(=/proc/1/ns/net)
- container 模式(=/proc/${container_pid}/ns/net)
docker run
时使用 –net=
参数指定,默认是 bridge
模式。
- 父进程通过
clone
创建子进程时,使用namespace
技术,实现子进程与其他进程(包含父进程)的命名空间隔离; - 子进程创建完毕之后,使用cgroup技术来处理子进程,实现进程的资源使用限制;
- 系统在子进程所处namespace内部,创建需要的隔离环境,如隔离的网络栈等;
- namespace和cgroup两种技术都用上之后,进程所处的“隔离”环境才真正建立,这时“容器”才真正诞生!
Kubernetes 的 Pod 网络
Kubernetes的Pod网络采用的是Docker的container模式网络(pause container --net=none)。两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过lo网卡设备通信。
kubernetes
中的pause容器
主要为每个业务容器提供以下功能(pause clone flags=CLONE_NEW*|):
- PID命名空间:Pod中的不同应用程序可以看到其他应用程序的进程ID。
- 网络命名空间:Pod中的多个容器能够访问同一个IP和端口范围。
- IPC命名空间:Pod中的多个容器能够使用SystemV IPC或POSIX消息队列进行通信。
- UTS命名空间:Pod中的多个容器共享一个主机名;Volumes(共享存储卷):
- Pod中的各个容器可以访问在Pod级别定义的Volumes。
docker run -d --name pause_test --net=none -p 8880:80 ${psimg} // default is --net=bridge
docker inspect pause_test
docker run -d --name nginx_test --net=container:pause_test --ipc=container:pause_test --pid=container:pause_test nginx
# k8s 的 Pod 创建方式,以下是 CNI 的操作简要步骤
ip link add dev tt0 type veth peer tt1
ip link set tt0 name eth0 netns net
ip netns exec net ip link set lo up
ip netns exec net ip link set eth0 up
ip netns exec net ip add add 192.168.0.10/24 dev eth0
ip netns exec net ip route add default via 192.168.0.1
pause 源码
pause 就是占坑用的(pid下使用了隔离的ns)。
syscall
clone fork vfork 区别
- 直接调用
fork()
等效于调用clone(2)
时仅指定flags为SIGCHLD
(共享信号句柄表)。 - 如果设置了
CLONE_VM
,则调用进程和子进程在同一内存空间中运行。 特别是,由调用进程或子进程执行的内存写入在另一个进程中也是可见的。 - 在glibc的
sysdeps/unix/sysv/linux/createthread.c
源码中创建线程的函数create_thread
中使用了clone()
,并指定了相关的flags:
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);
Coding
pause 容器源码
属于子进程?包含多有的ns?
#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;
}
Kubernetes 的 pause 容器没有复杂的逻辑,里面运行着一个非常简单的进程,它不执行任何功能,基本上是永远“睡觉”的,源代码在 kubernetes 项目的 build/pause/ 目录中
它执行另一个重要的功能——即它扮演 PID 1 的角色,并在子进程成为孤儿进程的时候通过调用 wait() 收割这些僵尸子进程。这样我们就不用担心我们的 Pod 的 PID namespace 里会堆满僵尸进程了。
setns(pidns)
对应的docker 命令
docker ps |grep ${pod_name}
docker inspect ${pscid} -f {{.State.Pid}}
ll /proc/{ps_pid}/ns/ // pause 容器和第二个容器共享一个netns,如果是hostnetwork的pod,则跟pid=1共享一个netns
ps -eLf | head -1;ps -eLf |grep ${ps_pid}
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#define STACK_SIZE (1024 * 1024)
int idle(void *args)
{
printf("I'm child process, and my pid is: %d\n", getpid());
for (;;) {
sleep(1);
}
return 0;
}
pid_t clone_wrapper(int (*func)(void *), int flag, void *args)
{
char *stack, *stack_top;
stack = (char *)malloc(STACK_SIZE);
if (stack == NULL) {
printf("alloc stack for child failed!\n");
return -1;
}
/* 栈空间为什么传尾指针,因为栈是反着的 */
stack_top = stack + STACK_SIZE; /* Assume stack grows downward */
return clone(func, stack_top, flag , args);
}
char *get_pid_ns(int pid)
{
char bytes[32];
sprintf(bytes, "/proc/%d/ns/pid", pid);
return strdup(bytes);
}
int main(void)
{
pid_t childs[2];
char *ns_file;
int fd;
printf("I'm parent, and my pid is: %d\n", getpid());
childs[0] = clone_wrapper(idle, CLONE_NEWPID, NULL);
if (childs[0] == -1) {
printf("error: create child thread failed!\n");
return ;
}
printf("first child's pid is: %d\n", childs[0]);
ns_file = get_pid_ns(childs[0]);
if (!ns_file) {
printf("get child pid ns failed!\n");
return -1;
}
fd = open(ns_file, O_RDONLY);
if (fd == -1) {
printf("open child pid ns failed!\n");
return -1;
}
if (setns(fd, 0) == -1) {
printf("set ns failed!\n");
return -1;
}
printf("I'm parent, and my pid is: %d\n", getpid());
childs[1] = clone_wrapper(idle, 0, NULL);
if (childs[1] == -1) {
printf("error: create child thread failed!\n");
return -1;
}
printf("second child's pid is: %d\n", childs[1]);
sleep(3);
kill(childs[0], SIGTERM);
kill(childs[1], SIGTERM);
waitpid(childs[0], NULL, 0);
waitpid(childs[1], NULL, 0);
return 0;
}
其中,父进程在创建第一个子进程时指定了 CLONE_NEWPID
,然后父进程调用 setns(), 将父进程的 pid namespace 设置为第一个子进程的 pid namespace,接下来父进程又创建了第二个子进程,则此时,第一个进程和第二个处在同一个 pid namespace 中,在父进程的 pid namespace 中,它们的 PID 分别是:18612、18613,在子进程的 pid namespace 中,它们的 PID 分别是:1、2。