07 | 白话容器基础(三):深入理解容器镜像
2018-09-07 张磊
深入剖析Kubernetes
进入课程
讲述:张磊
时长 19:34 大小 8.97M
你好,我是张磊。我在今天这篇文章的最后,放置了一张 Kubernetes 的技能图谱,希望
对你有帮助。
在前两次的分享中,我讲解了 Linux 容器最基础的两种技术:Namespace 和 Cgroups。
希望此时,你已经彻底理解了“容器的本质是一种特殊的进程”这个最重要的概念。
而正如我前面所说的,Namespace 的作用是“隔离”,它让应用进程只能看到该
Namespace 内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一
圈看不见的墙。这么一折腾,进程就真的被“装”在了一个与世隔绝的房间里,而这些房间
就是 PaaS 项目赖以生存的应用“沙盒”。
下载APP
可是,还有一个问题不知道你有没有仔细思考过:这个房间四周虽然有了墙,但是如果容器
进程低头一看地面,又是怎样一副景象呢?
换句话说,容器里的进程看到的文件系统又是什么样子的呢?
可能你立刻就能想到,这一定是一个关于 Mount Namespace 的问题:容器里的应用进
程,理应看到一份完全独立的文件系统。这样,它就可以在自己的容器目录(比如 /tmp)
下进行操作,而完全不会受宿主机以及其他容器的影响。
那么,真实情况是这样吗?
“左耳朵耗子”叔在多年前写的一篇关于 Docker 基础知识的博客里,曾经介绍过一段小程
序。这段小程序的作用是,在创建子进程时开启指定的 Namespace。
下面,我们不妨使用它来验证一下刚刚提到的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define _GNU_SOURCE
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int container_main(void* arg)
{
printf("Container - inside the container!\n");
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container!\n");
int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SI
复制代码
这段代码的功能非常简单:在 main 函数里,我们通过 clone() 系统调用创建了一个新的子
进程 container_main,并且声明要为它启用 Mount Namespace(即:CLONE_NEWNS
标志)。
而这个子进程执行的,是一个“/bin/bash”程序,也就是一个 shell。所以这个 shell 就运
行在了 Mount Namespace 的隔离环境中。
我们来一起编译一下这个程序:
这样,我们就进入了这个“容器”当中。可是,如果在“容器”里执行一下 ls 指令的话,
我们就会发现一个有趣的现象: /tmp 目录下的内容跟宿主机的内容是一样的。
也就是说:
这是怎么回事呢?
28
29
30
31
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
1
2
3
4
$ gcc -o ns ns.c
$ ./ns
Parent - start a container!
Container - inside the container!
复制代码
1
2
$ ls /tmp
# 你会看到好多宿主机的文件
复制代码
即使开启了 Mount Namespace,容器进程看到的文件系统也跟宿主机完全
一样。
仔细思考一下,你会发现这其实并不难理解:Mount Namespace 修改的,是容器进程对
文件系统“挂载点”的认知。但是,这也就意味着,只有在“挂载”这个操作发生之后,进
程的视图才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。
这时,你可能已经想到了一个解决办法:创建新进程时,除了声明要启用 Mount
Namespace 之外,我们还可以告诉容器进程,有哪些目录需要重新挂载,就比如这个
/tmp 目录。于是,我们在容器进程执行前可以添加一步重新挂载 /tmp 目录的操作:
可以看到,在修改后的代码里,我在容器进程启动之前,加上了一句 mount(“none”,
“/tmp”, “tmpfs”, 0, “”) 语句。就这样,我告诉了容器以 tmpfs(内存盘)格式,
重新挂载了 /tmp 目录。
这段修改后的代码,编译执行后的结果又如何呢?我们可以试验一下:
可以看到,这次 /tmp 变成了一个空目录,这意味着重新挂载生效了。我们可以用 mount
-l 检查一下:
1
2
3
4
5
6
7
8
9
10
int container_main(void* arg)
{
printf("Container - inside the container!\n");
// 如果你的机器的根目录的挂载类型是 shared,那必须先重新挂载根目录
// mount("", "/", NULL, MS_PRIVATE, "");
mount("none", "/tmp", "tmpfs", 0, "");
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
复制代码
1
2
3
4
5
$ gcc -o ns ns.c
$ ./ns
Parent - start a container!
Container - inside the container!
$ ls /tmp
复制代码
可以看到,容器里的 /tmp 目录是以 tmpfs 方式单独挂载的。
更重要的是,因为我们创建的新进程启用了 Mount Namespace,所以这次重新挂载的操
作,只在容器进程的 Mount Namespace 中有效。如果在宿主机上用 mount -l 来检查一
下这个挂载,你会发现它是不存在的:
这就是 Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程
视图的改变,一定是伴随着挂载操作(mount)才能生效。
可是,作为一个普通用户,我们希望的是一个更友好的情况:每当创建一个新容器时,我希
望容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统。怎
么才能做到这一点呢?
不难想到,我们可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount
Namespace 的存在,这个挂载对宿主机不可见,所以容器进程就可以在里面随便折腾了。
在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工
作。顾名思义,它的作用就是帮你“change root file system”,即改变进程的根目录到
你指定的位置。它的用法也非常简单。
假设,我们现在有一个 $HOME/test 目录,想要把它作为一个 /bin/bash 进程的根目录。
首先,创建一个 test 目录和几个 lib 文件夹:
1
2
$ mount -l | grep tmpfs
none on /tmp type tmpfs (rw,relatime)
复制代码
1
2
# 在宿主机上
$ mount -l | grep tmpfs
复制代码
1 $ mkdir -p $HOME/test
复制代码
评论0