说明: 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 Namespace | isolate filesystem mount points 隔离文件系统挂载点 | CLONE_NEWS |
| UTS Namespace | isolate hostname and domainname 隔离 hostname(主机名) 和 domainname(域名) | CLONE_NEWUTS |
| IPC Namespace | isolate interprocess communication (IPC) resources 隔离进程间通讯 | CLONE_NEWIPC |
| PID Namespace | isolate the PID number space 隔离进程ID | CLONE_NEWPID |
| Networ Namespace | isolate network interfaces 隔离网络 | CLONE_NEWNET |
| User Namespace | isolate UID/GID number spaces 隔离用户和用户组 | CLONE_NEWUSER |
可对Namespace进行操作的三个函数,如下表所示
| 系统调用函数名称 | 函数信息 | 说明 |
|---|---|---|
| clone | 使用 man 2 clone | 创建新的进程和新的namespace,新创建的进程 attach 到新创建的 namespace |
| unshare | 使用 man 2 unshare | 不创建新的进程,创建新的 namespace 并attach当前进程上 |
| setns | 使用 man 2 setns | attach 进程到已有的 namespace 上 |
查看Linux Namespace
环境说明:
Docker上运行了3个容器,其中 my-tomcat1 与 my-tomcat2 状态为 UP其进程ID分别为3202、3318,而my-tomcat状态为Exited
另外Linux系统上运行了一个Redis进程,其进程ID为11436
分析结果:
- 1号进程与进程ID为11436(Redis)具有相同的ns命名空间,冒号后面中括号内的数值表示该namespace的inode。如果不同进程的namespace inode相同,说明这些进程属于同一个namespace
- 进程ID为3202(my-tomcat1)与进程ID为3318(my-tomcat2)与其他进程相比各自具有独立的namespace inode所以ns命名空间不同
- 当使用系统命令kill掉3318完成后(而非Docker 命令),使用系统查看进程命令发现该进程已不存在,使用Docker命令查看进程发现其状态有原来的UP变为Exited,也就是说容器内的应用同样接受操作系统管理
总结:
每个进程都用于自己的命名空间,有的进程共享同一命名空间,有的则是独立与其他进程之间,不同的命名空间赋予当前进程不通的特性,但是这些进程依旧归操作系统管理
实践Linux Namespace
说明:
Linux 主要提供 6 中不同类型的 Namespace,本次实践以Networ Namespace为例,通过了解相互隔离的网络命名空间之间的通讯,引出Docker与应用之间的网络通讯模式
通过clone函数中的
CLONE_NEWNS参数,为该进程创建单独的网络命名空间,其实Linux 系统提供了直接创建命名空间的命令,这里之所以使用clone函数是想读者可以将clone创建出的进程抽象成应用(比如启动一个tomcat),将其具体
相互隔离的网络命名空间是如何通讯的?
- 创建编译 clone.c:
提示:
实践使用的代码来自 man 2 clone 中的示例代码,这里将原来的CLONE_NEWUTS 修改为CLONE_NEWNET,也就是说创建的进程各自拥有独立的网络命名空间,修改后代码如下:
pid = clone(childFunc, stackTop, CLONE_NEWNS | SIGCHLD, argv[1]);
| 步骤顺序 | 指令 | 说明 |
|---|---|---|
| 1 | touch clone.c | 在当前文件夹内创建 clone.c文件 |
| 2 | vim clone.c | 编辑clone.c文件,将代码复制进去,保存并退出 |
| 3 | gcc clone.c | 编译clone.c文件 |
| 4 | ./a.out | 执行编辑后文件 a.out |
- 创建 Virtual Ethernet:
Virtual Ethernet是Linux提供的另外一种特殊的网络设备,中文称为虚拟网卡接口。它总是成对出现,通过将其绑定到不同的网络命名空间上,可以实现不同网络命名空间之间网络通信,这是完成通讯的关键技术
| 步骤顺序 | 指令 | 说明 |
|---|---|---|
| 5 | ip link add veth-ns1 type veth peer name veth-ns2 | 创建了一对虚拟网卡,名字为: veth-ns1 ,veth-ns2 |
3. 创建两个进程
| 步骤顺序 | 指令 | 说明 |
|---|---|---|
| 6 | ./a.out | 会话1中执行,返回的进程ID:4110 |
| 7 | ./a.out | 会话1中执行,返回的进程ID:4112 |
- 对进程的网路命名空间绑定虚拟网卡,完成通讯
- 总结示意图如下:
不同进程相互隔离的网络命名空间,通过设置veth-pair虚拟网卡对,对进程进网络命名空间行绑定,设置IP,启动网卡等一系列操作完成通讯
思考Docker中的应用是如何通讯的?
我们不妨换个思路想问题:
首先我们需要验证,启动的docker容器之间命名空间是否相互隔离?
验证各个容器之间否能够完成网络通讯
查看新建的docker容器内的网卡信息与一端在docker0(网桥)内的网卡信息是否为一对
-
启动Docker容器,命名空间是否相互隔离
-
容器之间是否能够完成通讯
-
验证docker0内的网卡信息与容器内的网卡信息是否成对
-
总结示意图如下:
总结:
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);
}