【 Namespace、Cgroups】 浅析与实践(上)

780 阅读6分钟

说明: Docker中使用的Namespace、Cgroups(下文讲解)技术来自Linux 内核(Kernel)提供的功能

资源隔离:Linux Namespace

Linux Namespace(Linux 命名空间)是 Linux 内核(Kernel)提供的功能,它可以隔离一系列的系统资源,如 PID(进程 ID,Process ID)、User ID、Network、文件系统等

用户运行在容器的应用进程,跟宿主机上的其他进程一样,都由宿主机的操作系统进行统一管理,只不过这些进程通过设置Namespace参数达到被隔离的效果,而Docker在这方面更多的是辅助与管理的工作

目前 Linux 主要提供 6 中不同类型的 Namespace,如下表所示

每种Namespace都有自己的功能和特性,目前为止内核共引入了六类Namespace,它们的隔离特性是Docker容器的技术基石

Namespace类型描述参数形式
Mount Namespaceisolate filesystem mount points
隔离文件系统挂载点
CLONE_NEWS
UTS Namespaceisolate hostname and domainname
隔离 hostname(主机名) 和 domainname(域名)
CLONE_NEWUTS
IPC Namespaceisolate interprocess communication (IPC) resources
隔离进程间通讯
CLONE_NEWIPC
PID Namespaceisolate the PID number space
隔离进程ID
CLONE_NEWPID
Networ Namespaceisolate network interfaces
隔离网络
CLONE_NEWNET
User Namespaceisolate UID/GID number spaces
隔离用户和用户组
CLONE_NEWUSER

可对Namespace进行操作的三个函数,如下表所示

系统调用函数名称函数信息说明
clone使用 man 2 clone创建新的进程和新的namespace,新创建的进程 attach 到新创建的 namespace
unshare使用 man 2 unshare不创建新的进程,创建新的 namespace 并attach当前进程上
setns使用 man 2 setnsattach 进程到已有的 namespace 上

查看Linux Namespace

环境说明:

  1. Docker上运行了3个容器,其中 my-tomcat1 与 my-tomcat2 状态为 UP其进程ID分别为3202、3318,而my-tomcat状态为Exited

  2. 另外Linux系统上运行了一个Redis进程,其进程ID为11436

分析结果:

  1. 1号进程与进程ID为11436(Redis)具有相同的ns命名空间,冒号后面中括号内的数值表示该namespace的inode。如果不同进程的namespace inode相同,说明这些进程属于同一个namespace
  2. 进程ID为3202(my-tomcat1)与进程ID为3318(my-tomcat2)与其他进程相比各自具有独立的namespace inode所以ns命名空间不同
  3. 当使用系统命令kill掉3318完成后(而非Docker 命令),使用系统查看进程命令发现该进程已不存在,使用Docker命令查看进程发现其状态有原来的UP变为Exited,也就是说容器内的应用同样接受操作系统管理

Docker -- Namespace、Cgroups实战-1.png Docker -- Namespace、Cgroups实战-3.png Docker -- Namespace、Cgroups实战-2.png

总结:

每个进程都用于自己的命名空间,有的进程共享同一命名空间,有的则是独立与其他进程之间,不同的命名空间赋予当前进程不通的特性,但是这些进程依旧归操作系统管理

实践Linux Namespace

说明:

Linux 主要提供 6 中不同类型的 Namespace,本次实践以Networ Namespace为例,通过了解相互隔离的网络命名空间之间的通讯,引出Docker与应用之间的网络通讯模式

通过clone函数中的CLONE_NEWNS参数,为该进程创建单独的网络命名空间,其实Linux 系统提供了直接创建命名空间的命令,这里之所以使用clone函数是想读者可以将clone创建出的进程抽象成应用(比如启动一个tomcat),将其具体

相互隔离的网络命名空间是如何通讯的?

  1. 创建编译 clone.c:

提示:

实践使用的代码来自 man 2 clone 中的示例代码,这里将原来的CLONE_NEWUTS 修改为CLONE_NEWNET,也就是说创建的进程各自拥有独立的网络命名空间,修改后代码如下:

pid = clone(childFunc, stackTop, CLONE_NEWNS | SIGCHLD, argv[1]);

步骤顺序指令说明
1touch clone.c在当前文件夹内创建 clone.c文件
2vim clone.c编辑clone.c文件,将代码复制进去,保存并退出
3gcc clone.c编译clone.c文件
4./a.out执行编辑后文件 a.out
  1. 创建 Virtual Ethernet:

Virtual Ethernet是Linux提供的另外一种特殊的网络设备,中文称为虚拟网卡接口。它总是成对出现,通过将其绑定到不同的网络命名空间上,可以实现不同网络命名空间之间网络通信,这是完成通讯的关键技术

步骤顺序指令说明
5ip link add veth-ns1 type veth peer name veth-ns2创建了一对虚拟网卡,名字为:
veth-ns1 ,veth-ns2

Namespace、Cgroups 浅析与实践-1.png 3. 创建两个进程

步骤顺序指令说明
6./a.out会话1中执行,返回的进程ID:4110
7./a.out会话1中执行,返回的进程ID:4112

Namespace、Cgroups 浅析与实践-2.png Namespace、Cgroups 浅析与实践-3.png

  1. 对进程的网路命名空间绑定虚拟网卡,完成通讯

Namespace、Cgroups 浅析与实践-4png.png Namespace、Cgroups 浅析与实践-5.png

  1. 总结示意图如下:

不同进程相互隔离的网络命名空间,通过设置veth-pair虚拟网卡对,对进程进网络命名空间行绑定,设置IP,启动网卡等一系列操作完成通讯

Namespace、Cgroups 浅析与实践-6.png

思考Docker中的应用是如何通讯的?

我们不妨换个思路想问题:

  1. 首先我们需要验证,启动的docker容器之间命名空间是否相互隔离?

  2. 验证各个容器之间否能够完成网络通讯

  3. 查看新建的docker容器内的网卡信息与一端在docker0(网桥)内的网卡信息是否为一对

  1. 启动Docker容器,命名空间是否相互隔离 Namespace、Cgroups 浅析与实践-9.png

  2. 容器之间是否能够完成通讯 Namespace、Cgroups 浅析与实践-10.png

  3. 验证docker0内的网卡信息与容器内的网卡信息是否成对 Namespace、Cgroups 浅析与实践-11.png

  4. 总结示意图如下: Namespace、Cgroups 浅析与实践-12.png

总结:

Docker通过使用Linux Namespace技术完成容器之间隔离,使用veth-pair(一对虚拟网卡)一端连接到容器上,一端连接到docker0网桥上(类似以docker0为中心组成一个局网)完成通讯

clone.c

#define _GNU_SOURCE
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define errExit(msg)  do { perror(msg); exit(EXIT_FAILURE); \
                               } while (0)
                               
       /* Start function for cloned child */
       static int childFunc(void *arg)
       {
           struct utsname uts;

           /* Change hostname in UTS namespace of child */

           if (sethostname(arg, strlen(arg)) == -1)
               errExit("sethostname");

           /* Retrieve and display hostname */

           if (uname(&uts) == -1)
               errExit("uname");
           printf("uts.nodename in child:  %s\n", uts.nodename);

           /* Keep the namespace open for a while, by sleeping.
            * This allows some experimentation--for example, another
            * process might join the namespace. */

           // 这个里防止子进程提前退出
           sleep(20000000);

           return 0;           /* Child terminates now */
       }

       /* Stack size for cloned child */
       #define STACK_SIZE (1024 * 1024)   

       int main(int argc, char *argv[])
       {
           char *stack;                    /* Start of stack buffer */
           char *stackTop;                 /* End of stack buffer */
           pid_t pid;
           struct utsname uts;

           if (argc < 2) {
               fprintf(stderr, "Usage: %s <child-hostname>\n", argv[0]);
               exit(EXIT_SUCCESS);
           }

           /* Allocate stack for child */

           stack = malloc(STACK_SIZE);
           if (stack == NULL)
               errExit("malloc");
           stackTop = stack + STACK_SIZE;  /* Assume stack grows downward */

           /* Create child that has its own UTS namespace;
            * child commences execution in childFunc() */

           pid = clone(childFunc, stackTop, CLONE_NEWNET | SIGCHLD, argv[1]);
           if (pid == -1)
               errExit("clone");
           printf("clone() returned %ld\n", (long) pid);

           /* Parent falls through to here */

           // 防止父进程提前退出
           sleep(100000000);           
           /* Give child time to change its hostname */
           /* Display hostname in parent's UTS namespace. This will be
            * different from hostname in child's UTS namespace. */

           if (uname(&uts) == -1)
               errExit("uname");
           printf("uts.nodename in parent: %s\n", uts.nodename);

           if (waitpid(pid, NULL, 0) == -1)    /* Wait for child */
               errExit("waitpid");
           printf("child has terminated\n");

           exit(EXIT_SUCCESS);
       }