Pause(Infra)容器

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

Linux 的 namespace

Linux Namespace提供了一种内核级别隔离系统资源的方法,通过将系统的全局资源放在不同的Namespace中,来实现资源隔离的目的。不同Namespace的程序,可以享有一份独立的系统资源。目前Linux中提供了六类系统资源的隔离机制,分别是:

  • Mount: 隔离文件系统挂载点
  • UTS: 隔离主机名和域名信息
  • IPC: 隔离进程间通信
  • PID: 隔离进程的ID
  • Network: 隔离网络资源
  • User: 隔离用户和用户组的ID

namespace API 的使用方法:clone()unshare()setns()系统调用会使用CLONE_NEW*常量来区别六种不同的 namespace 类型。以及还有/proc下的部分文件。

  • CLONE_NEWNS: 用于指定Mount Namespace
  • CLONE_NEWUTS: 用于指定UTS Namespace
  • CLONE_NEWIPC: 用于指定IPC Namespace
  • CLONE_NEWPID: 用于指定PID Namespace
  • CLONE_NEWNET: 用于指定Network Namespace
  • CLONE_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

  1. 创建namespace:clone()系统调用
  2. 维持namespace存在:/proc/${pid}/ns
  3. 往namespace里添加进程:setns()系统调用
  4. 帮助进程逃离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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值