07 | 白话容器基础(三):深入理解容器镜像

07 | 白话容器基础(三):深入理解容器镜像

朗读人:张磊    19′34′′ | 8.97M

你好,我是张磊。我在今天这篇文章的最后,放置了一张 Kubernetes 的技能图谱,希望对你有帮助。

在前两次的分享中,我讲解了 Linux 容器最基础的两种技术:Namespace 和 Cgroups。希望此时,你已经彻底理解了“容器的本质是一种特殊的进程”这个最重要的概念。

而正如我前面所说的,Namespace 的作用是“隔离”,它让应用进程只能看到该 Namespace 内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一圈看不见的墙。这么一折腾,进程就真的被“装”在了一个与世隔绝的房间里,而这些房间就是 PaaS 项目赖以生存的应用“沙盒”。

可是,还有一个问题不知道你有没有仔细思考过:这个房间四周虽然有了墙,但是如果容器进程低头一看地面,又是怎样一副景象呢?

换句话说,容器里的进程看到的文件系统又是什么样子的呢?

可能你立刻就能想到,这一定是一个关于 Mount Namespace 的问题:容器里的应用进程,理应看到一份完全独立的文件系统。这样,它就可以在自己的容器目录(比如 /tmp)下进行操作,而完全不会受宿主机以及其他容器的影响。

那么,真实情况是这样吗?

“左耳朵耗子”叔在多年前写的一篇关于 Docker 基础知识的博客里,曾经介绍过一段小程序。这段小程序的作用是,在创建子进程时开启指定的 Namespace。

下面,我们不妨使用它来验证一下刚刚提到的问题。

#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 | SIGCHLD , NULL);
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
复制代码

这段代码的功能非常简单:在 main 函数里,我们通过 clone() 系统调用创建了一个新的子进程 container_main,并且声明要为它启用 Mount Namespace(即:CLONE_NEWNS 标志)。

而这个子进程执行的,是一个“/bin/bash”程序,也就是一个 shell。所以这个 shell 就运行在了 Mount Namespace 的隔离环境中。

我们来一起编译一下这个程序:

$ gcc -o ns ns.c
$ ./ns
Parent - start a container!
Container - inside the container!
复制代码

这样,我们就进入了这个“容器”当中。可是,如果在“容器”里执行一下 ls 指令的话,我们就会发现一个有趣的现象: /tmp 目录下的内容跟宿主机的内容是一样的。

$ ls /tmp
# 你会看到好多宿主机的文件
复制代码

也就是说:

即使开启了 Mount Namespace,容器进程看到的文件系统也跟宿主机完全一样。

这是怎么回事呢?

仔细思考一下,你会发现这其实并不难理解:Mount Namespace 修改的,是容器进程对文件系统“挂载点”的认知。但是,这也就意味着,只有在“挂载”这个操作发生之后,进程的视图才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。

这时,你可能已经想到了一个解决办法:创建新进程时,除了声明要启用 Mount Namespace 之外,我们还可以告诉容器进程,有哪些目录需要重新挂载,就比如这个 /tmp 目录。于是,我们在容器进程执行前可以添加一步重新挂载 /tmp 目录的操作:

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;
}
复制代码

可以看到,在修改后的代码里,我在容器进程启动之前,加上了一句 mount(“none”, “/tmp”, “tmpfs”, 0, “”) 语句。就这样,我告诉了容器以 tmpfs(内存盘)格式,重新挂载了 /tmp 目录。

这段修改后的代码,编译执行后的结果又如何呢?我们可以试验一下:

$ gcc -o ns ns.c
$ ./ns
Parent - start a container!
Container - inside the container!
$ ls /tmp
复制代码

可以看到,这次 /tmp 变成了一个空目录,这意味着重新挂载生效了。我们可以用 mount -l 检查一下:

$ mount -l | grep tmpfs
none on /tmp type tmpfs (rw,relatime)
复制代码

可以看到,容器里的 /tmp 目录是以 tmpfs 方式单独挂载的。

更重要的是,因为我们创建的新进程启用了 Mount Namespace,所以这次重新挂载的操作,只在容器进程的 Mount Namespace 中有效。如果在宿主机上用 mount -l 来检查一下这个挂载,你会发现它是不存在的:

# 在宿主机上
$ mount -l | grep tmpfs
复制代码

这就是 Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。

可是,作为一个普通用户,我们希望的是一个更友好的情况:每当创建一个新容器时,我希望容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统。怎么才能做到这一点呢?

不难想到,我们可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以容器进程就可以在里面随便折腾了。

在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工作。顾名思义,它的作用就是帮你“change root file system”,即改变进程的根目录到你指定的位置。它的用法也非常简单。

假设,我们现在有一个 $HOME/test 目录,想要把它作为一个 /bin/bash 进程的根目录。

首先,创建一个 test 目录和几个 lib 文件夹:

$ mkdir -p $HOME/test
$ mkdir -p $HOME/test/{bin,lib64,lib}
$ cd $T
复制代码

然后,把 bash 命令拷贝到 test 目录对应的 bin 路径下:

$ cp -v /bin/{bash,ls} $HOME/test/bin
复制代码

接下来,把 bash 命令需要的所有 so 文件,也拷贝到 test 目录对应的 lib 路径下。找到 so 文件可以用 ldd 命令:

$ T=$HOME/test
$ list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
$ for i in $list; do cp -v "$i" "${T}${i}"; done
复制代码

最后,执行 chroot 命令,告诉操作系统,我们将使用 $HOME/test 目录作为 /bin/bash 进程的根目录:

$ chroot $HOME/test /bin/bash
复制代码

这时,你如果执行 "ls /",就会看到,它返回的都是 $HOME/test 目录下面的内容,而不是宿主机的内容。

更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”成 $HOME/test 了。

这种视图被修改的原理,是不是跟我之前介绍的 Linux Namespace 很类似呢?

没错!

实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。

当然,为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。这样,在容器启动之后,我们在容器里通过执行 "ls /" 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。

而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。

所以,一个最常见的 rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等:

$ ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var
复制代码

而你进入容器之后执行的 /bin/bash,就是 /bin 目录下的可执行文件,与宿主机的 /bin/bash 完全不同。

现在,你应该可以理解,对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

  1. 启用 Linux Namespace 配置;

  2. 设置指定的 Cgroups 参数;

  3. 切换进程的根目录(Change Root)。

这样,一个完整的容器就诞生了。不过,Docker 项目在最后一步的切换上会优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot。这两个系统调用虽然功能类似,但是也有细微的区别,这一部分小知识就交给你课后去探索了。

另外,需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。

所以说,rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。

那么,对于容器来说,这个操作系统的“灵魂”又在哪里呢?

实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。

这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

这也是容器相比于虚拟机的主要缺陷之一:毕竟后者不仅有模拟出来的硬件机器充当沙盒,而且每个沙盒里还运行着一个完整的 Guest OS 给应用随便折腾。

不过,正是由于 rootfs 的存在,容器才有了一个被反复宣传至今的重要特性:一致性。

什么是容器的“一致性”呢?

我在专栏的第一篇文章《小鲸鱼大事记(一):初出茅庐》中曾经提到过:由于云端与本地服务器环境不同,应用的打包过程,一直是使用 PaaS 时最“痛苦”的一个步骤。

但有了容器之后,更准确地说,有了容器镜像(即 rootfs)之后,这个问题被非常优雅地解决了。

由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。

事实上,对于大多数开发者而言,他们对应用依赖的理解,一直局限在编程语言层面。比如 Golang 的 Godeps.json。但实际上,一个一直以来很容易被忽视的事实是,对一个应用来说,操作系统本身才是它运行所需要的最完整的“依赖库”。

有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。

这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。

不过,这时你可能已经发现了另一个非常棘手的问题:难道我每开发一个应用,或者升级一下现有的应用,都要重复制作一次 rootfs 吗?

比如,我现在用 Ubuntu 操作系统的 ISO 做了一个 rootfs,然后又在里面安装了 Java 环境,用来部署我的 Java 应用。那么,我的另一个同事在发布他的 Java 应用时,显然希望能够直接使用我安装过 Java 环境的 rootfs,而不是重复这个流程。

一种比较直观的解决办法是,我在制作 rootfs 的时候,每做一步“有意义”的操作,就保存一个 rootfs 出来,这样其他同事就可以按需求去用他需要的 rootfs 了。

但是,这个解决办法并不具备推广性。原因在于,一旦你的同事们修改了这个 rootfs,新旧两个 rootfs 之间就没有任何关系了。这样做的结果就是极度的碎片化。

那么,既然这些修改都基于一个旧的 rootfs,我们能不能以增量的方式去做这些修改呢?这样做的好处是,所有人都只需要维护相对于 base rootfs 修改的增量内容,而不是每次修改都制造一个“fork”。

答案当然是肯定的。

这也正是为何,Docker 公司在实现 Docker 镜像时并没有沿用以前制作 rootfs 的标准流程,而是做了一个小小的创新:

Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。

当然,这个想法不是凭空臆造出来的,而是用到了一种叫作联合文件系统(Union File System)的能力。

Union File System 也叫 UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。比如,我现在有两个目录 A 和 B,它们分别有两个文件:

$ tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x
复制代码

然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:

$ mkdir C
$ mount -t aufs -o dirs=./A:./B none ./C
复制代码

这时,我再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:

$ tree ./C
./C
├── a
├── b
└── x
复制代码

可以看到,在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是“合并”的含义。此外,如果你在目录 C 里对 a、b、x 文件做修改,这些修改也会在对应的目录 A、B 中生效。

那么,在 Docker 项目中,又是如何使用这种 Union File System 的呢?

我的环境是 Ubuntu 16.04 和 Docker CE 18.05,这对组合默认使用的是 AuFS 这个联合文件系统的实现。你可以通过 docker info 命令,查看到这个信息。

AuFS 的全称是 Another UnionFS,后改名为 Alternative UnionFS,再后来干脆改名叫作 Advance UnionFS,从这些名字中你应该能看出这样两个事实:

  1. 它是对 Linux 原生 UnionFS 的重写和改进;

  2. 它的作者怨气好像很大。我猜是 Linus Torvalds(Linux 之父)一直不让 AuFS 进入 Linux 内核主干的缘故,所以我们只能在 Ubuntu 和 Debian 这些发行版上使用它。

对于 AuFS 来说,它最关键的目录结构在 /var/lib/docker 路径下的 diff 目录:

/var/lib/docker/aufs/diff/<layer_id>
复制代码

而这个目录的作用,我们不妨通过一个具体例子来看一下。

现在,我们启动一个容器,比如:

$ docker run -d ubuntu:latest sleep 3600
复制代码

这时候,Docker 就会从 Docker Hub 上拉取一个 Ubuntu 镜像到本地。

这个所谓的“镜像”,实际上就是一个 Ubuntu 操作系统的 rootfs,它的内容是 Ubuntu 操作系统的所有文件和目录。不过,与之前我们讲述的 rootfs 稍微不同的是,Docker 镜像使用的 rootfs,往往由多个“层”组成:

$ docker image inspect ubuntu:latest
...
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:f49017d4d5ce9c0f544c...",
"sha256:8f2b771487e9d6354080...",
"sha256:ccd4d61916aaa2159429...",
"sha256:c01d74f99de40e097c73...",
"sha256:268a067217b5fe78e000..."
]
}
复制代码

可以看到,这个 Ubuntu 镜像,实际上由五个层组成。这五个层就是五个增量 rootfs,每一层都是 Ubuntu 操作系统文件与目录的一部分;而在使用镜像时,Docker 会把这些增量联合挂载在一个统一的挂载点上(等价于前面例子里的“/C”目录)。

这个挂载点就是 /var/lib/docker/aufs/mnt/,比如:

/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
复制代码

不出意外的,这个目录里面正是一个完整的 Ubuntu 操作系统:

$ ls /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
复制代码

那么,前面提到的五个镜像层,又是如何被联合挂载成这样一个完整的 Ubuntu 文件系统的呢?

这个信息记录在 AuFS 的系统目录 /sys/fs/aufs 下面。

首先,通过查看 AuFS 的挂载信息,我们可以找到这个目录对应的 AuFS 的内部 ID(也叫:si):

$ cat /proc/mounts| grep aufs
none /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fc... aufs rw,relatime,si=972c6d361e6b32ba,dio,dirperm1 0 0
复制代码

即,si=972c6d361e6b32ba。

然后使用这个 ID,你就可以在 /sys/fs/aufs 下查看被联合挂载在一起的各个层的信息:

$ cat /sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]*
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh
/var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh
/var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh
/var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh
/var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh
/var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh
复制代码

从这些信息里,我们可以看到,镜像的层都放置在 /var/lib/docker/aufs/diff 目录下,然后被联合挂载在 /var/lib/docker/aufs/mnt 里面。

而且,从这个结构可以看出来,这个容器的 rootfs 由如下图所示的三部分组成:

第一部分,只读层。

它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。可以看到,它们的挂载方式都是只读的(ro+wh,即 readonly+whiteout,至于什么是 whiteout,我下面马上会讲到)。

这时,我们可以分别查看一下这些层的内容:

$ ls /var/lib/docker/aufs/diff/72b0744e06247c7d0...
etc sbin usr var
$ ls /var/lib/docker/aufs/diff/32e8e20064858c0f2...
run
$ ls /var/lib/docker/aufs/diff/a524a729adadedb900...
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
复制代码

可以看到,这些层,都以增量的方式分别包含了 Ubuntu 操作系统的一部分。

第二部分,可读写层。

它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。

可是,你有没有想到这样一个问题:如果我现在要做的,是删除只读层里的一个文件呢?

为了实现这样的删除操作,AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。

比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。我喜欢把 whiteout 形象地翻译为:“白障”。

所以,最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量 rootfs 的好处。

第三部分,Init 层。

它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。

需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。

可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。

所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。

最终,这 7 个层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下,表现为一个完整的 Ubuntu 操作系统供容器使用。

总结

在今天的分享中,我着重介绍了 Linux 容器文件系统的实现方式。而这种机制,正是我们经常提到的容器镜像,也叫作:rootfs。它只是一个操作系统的所有文件和目录,并不包含内核,最多也就几百兆。而相比之下,传统虚拟机的镜像大多是一个磁盘的“快照”,磁盘有多大,镜像就至少有多大。

通过结合使用 Mount Namespace 和 rootfs,容器就能够为进程构建出一个完善的文件系统隔离环境。当然,这个功能的实现还必须感谢 chroot 和 pivot_root 这两个系统调用切换进程根目录的能力。

而在 rootfs 的基础上,Docker 公司创新性地提出了使用多个增量 rootfs 联合挂载一个完整 rootfs 的方案,这就是容器镜像中“层”的概念。

通过“分层镜像”的设计,以 Docker 镜像为核心,来自不同公司、不同团队的技术人员被紧密地联系在了一起。而且,由于容器镜像的操作是增量式的,这样每次镜像拉取、推送的内容,比原本多个完整的操作系统的大小要小得多;而共享层的存在,可以使得所有这些容器镜像需要的总空间,也比每个镜像的总和要小。这样就使得基于容器镜像的团队协作,要比基于动则几个 GB 的虚拟机磁盘镜像的协作要敏捷得多。

更重要的是,一旦这个镜像被发布,那么你在全世界的任何一个地方下载这个镜像,得到的内容都完全一致,可以完全复现这个镜像制作者当初的完整环境。这,就是容器技术“强一致性”的重要体现。

而这种价值正是支撑 Docker 公司在 2014~2016 年间迅猛发展的核心动力。容器镜像的发明,不仅打通了“开发 - 测试 - 部署”流程的每一个环节,更重要的是:

容器镜像将会成为未来软件的主流发布方式。

思考题

  1. 既然容器的 rootfs(比如,Ubuntu 镜像),是以只读方式挂载的,那么又如何在容器里修改 Ubuntu 镜像的内容呢?(提示:Copy-on-Write)

  2. 除了 AuFS,你知道 Docker 项目还支持哪些 UnionFS 实现吗?你能说出不同宿主机环境下推荐使用哪种实现吗?

感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。

点击这里查看大图。

版权归极客邦科技所有,未经许可不得转载

精选留言

  • Geek_6ef93d
    有读者反映,咱们重新挂载/tmp目录的实验执行完成后,在宿主机上居然可以看到这个挂载信息。。这是怎么回事呢?实际上,大家自己装的虚拟机,或者云上的虚拟机的根目录,很多都是以share方式的挂载的。这时候,你在容器里做mount也会继承share方式。这样就会把容器内挂载传播到宿主机上。解决这个问题,你可以在重新挂载/tmp之前,在容器内先执行一句:mount(“”, “/“, NULL, MS_PRIVATE, “”) 这样,容器内的根目录就是private挂载的了。
    2018-09-08
  • Cloud*
    1. 上面的读写层通常也称为容器层,下面的只读层称为镜像层,所有的增删查改操作都只会作用在容器层,相同的文件上层会覆盖掉下层。知道这一点,就不难理解镜像文件的修改,比如修改一个文件的时候,首先会从上到下查找有没有这个文件,找到,就复制到容器层中,修改,修改的结果就会作用到下层的文件,这种方式也被称为copy-on-write。

    2. 查了一下,包括但不限于以下这几种:aufs, device mapper, btrfs, overlayfs, vfs, zfs。aufs是ubuntu 常用的,device mapper 是 centos,btrfs 是 SUSE,overlayfs ubuntu 和 centos 都会使用,现在最新的 docker 版本中默认两个系统都是使用的 overlayfs,vfs 和 zfs 常用在 solaris 系统。

    欢迎补充和指正。
    2018-09-07
    作者回复

    课代表又出现啦

    2018-09-07

  • 栖枝
    whiteout,这就是我每次想把之前不需要的东西删了,但是镜像也没变小的原因啊,手动捂脸
    2018-09-07
  • asdf100
    目录联合挂载时,如果A和B目录里的x文件内容不一样,这时如何处理?
    2018-09-07
    作者回复

    好问题。aufs是一层一层往上盖的,所以我给的例子里,A里面的x会覆盖B里面的x。

    2018-09-07

  • 女朋友1 3 5追电视剧,我1 3 5追k8s。
    2018-09-07
    作者回复

    女朋友应该很开心😃

    2018-09-07

  • Jeff.W
    继Namespace构建了四周的围墙(进程隔离),Cgroups构建了受控的天空优先使用阳光雨露(资源限制),Mount namespace与rootfs构建了脚下的大地,这片土地是你熟悉和喜欢的,不管你走到哪里,都可以带着它,就好像你从未离开过家乡,没有丝毫的陌生感(容器的一致性)~
    2018-09-13
    作者回复

    于是,你就做了一句诗

    2018-09-13

  • oddrock
    请问老师,听了您今天的课,认识到容器使用的内核是和宿主机内核一致的,但如果容器需要不同的内核怎么办?
    2018-09-07
    作者回复

    没!办!法!所以我去年一直在搞katacontainers,这种基于虚拟化的容器是有独立内核的。

    2018-09-07

  • snakorse
    精彩到炸裂!!!
    2018-09-07
  • 汤尼房
    老师你好,很好奇dockerhub上关于系统镜像的制作过程,比如对于centos7.4的镜像,首先能想到的是应该依据centos官方发布的centos7.4的系统镜像,然后将此镜像中的内核给去掉,仅保留剩下的文件、配置和目录或者是保留必要的文件、配置和目录;但通过查看centos7.4的Dockerfile之后发现其依赖的base image是scratch,然后在scratch的基础上添加centos-7.4.1708-docker.tar.xz,后来查看ubuntu镜像发现其base image也是scratch,老师能简单说下scratch基础镜像吗(镜像文件的制作过程等),猜想scratch应该是包含了多个系统之间共同的东西,否则为啥多个版本的系统镜像以及不同版本的系统镜像的基础镜像都用的是scratch,所以对scratch基础镜像很好奇,望老师指点一下
    2018-09-10
    作者回复

    很简单啊,因为scratch本身就是个空镜像。你想想,假如你是centos公司,你在发布centos镜像的时候总得FROM吧?所以docker公司就给你做了个scratch:万能的base镜像。

    2018-09-11

  • 武坤
    在容器中修改文件时,Docker会从上到下依次在各镜像层中查找比文件。找到后,会把此文件复制到容器层(可读写层),然后修改。这就是 Copy on Write.
    2018-09-07
  • 请教一个低级的问题,我现在用docker部署,在多次部署以后,会导致磁盘占用空间急剧的增大,老是需要扩容,虽然对不使用的镜像定时做了删除,但还是会出现这样的问题,产生的文件也不敢在生产环境随便进行删除,只能清理镜像和容器。
    2018-09-07
    作者回复

    用kubernetes ,打开GC功能,定时清理

    2018-09-07

  • shupian418
    ns.c 启动失败
    Parent - start a container
    Parent - container stopped
    2018-09-10
    作者回复

    得看看报错,八成是依赖之类的

    2018-09-11

  • Hunsbiously
    你好,这个视频是我看到的一个很好的视频,希望老师能够找机会,详细分析下.
    https://m.youtube.com/watch?v=90kZRyPcRZw#fauxfullscreen
    2018-09-09
    作者回复

    2018-09-10

  • silencedoctor
    老师你好我想请问一下 我的系统是Ubuntu16.04.4 docker是18.06.0 /var/lib/docker下并没有aufs这个文件夹 执行docker image他又确实分了层这是什么原因呢
    2018-09-07
  • long904
    请教老师,以我理解容器镜像依赖宿主机内核。那么如果镜像是基于Linux的系统制作而成(线上运行环境就是Linux),那么如果在Windows系统上它是不能运行这个镜像了,对吗?甚至目标机器如果内核跟镜像制作不同(比如centos5和centos7)如果是,请问该怎么理解容器跨平台部署一说?谢谢。
    2018-09-07
    作者回复

    如果你的应用依赖内核版本,那果断跨不了平台,除非再创建对应的虚拟机出来做宿主。说跨平台其实是因为大多数应用没有内核依赖。windows系统会给容器外面套一个vm,所以也能运行linux容器。

    2018-09-07

  • 贾鹏
    问题1,我理解的是每次发生了修改,都会在上层生成一个新的镜像layer,而没有发生修改的内容是不会被记录layer里的。就像dockerfile里面的run指令,每一个run都会生成一个layer(docker history能看到顺序)。并且如果是相关动作,其实是以最后的指令为准的(也就是覆盖掉了前面的)。

    问题2,devicemapper的磁盘使用率低 overlay aufs容器启动快,磁盘使用率高

    都是个人理解,也没有查资料确认,有问题欢迎指正
    2018-09-07
  • 假装乐
    可读写层增改好理解,删是怎么实现的呢
    2018-09-07
    作者回复

    再仔细看看whiteout的解释?

    2018-09-07

  • Vinsec
    从基本原理讲起,再引导至容器上的具体应用和改进,深入浅出。我觉得作者不仅技术牛,文笔的逻辑也是非常好的,能在抽象和具体中不停切换而且毫无枯燥之感。好了,夸完了。目前来说,课程完全是值得这个价格的,期待大佬后续知识付费产品。
    2018-09-14
  • 王由华
    被whiteout遮挡的文件会被清除吗?用什么命令清除的?否则镜像不是就膨胀了。
    2018-09-13
    作者回复

    哈哈,是会膨胀啦。可以压缩镜像,做法网上可以搜到。

    2018-09-13

  • 陈华
    老师说没有内核所以rootfs会比较小,请问一般安装的linux系统内核文件在哪里存放呢?,
    2018-09-10
    作者回复

    首先,docker镜像比较小不只是因为没有内核,内核本身其实不大。大小的差异主要因为我们平常看见的虚拟机镜像实际上是整个磁盘的快照。其次,一般情况下,内核放下安装盘里,解压到磁盘上,加载到内存中。有兴趣可以读这里:https://blog.csdn.net/gatieme/article/details/50914250

    2018-09-10

  • kyleqian
    请问所谓的“应用依赖内核”是指程序直接进行内核调用,而不是仅仅使用c运行库,可以这样理解吗?
    2018-09-09
    作者回复

    是的。或者,应用依赖某个特定内核版本才有的特性。

    2018-09-10

  • Kaer
    老师你好,如果依赖了内核运行的应用,必须得重新在跟宿主机相同内核下重新打包镜像才行吗?有没有完美解决方案:比如在镜像打包的时候,打一个兼容内核的补丁。
    2018-09-09
    作者回复

    重新打包也解决不了问题,你的宿主机必须跟开发环境一致才行。linuxkit这个工具就是干这个的。

    2018-09-10

  • linbo
    为啥我在宿主机上能看到mount的内容呢
    2018-09-07
  • 明珠
    /etc/rc.local可以挂载在init层吗?用于容器启动的时候可以执行这里命令
    2018-09-07
  • droid
    代码中的"container_stack+STACK_SIZE"一开始没看明白,后来发现是栈,要传栈底指针。大家见笑。
    2018-09-07
    作者回复

    看得这么细,当然要表扬才是。

    2018-09-07

  • 陶希阳
    linux基础知识有些薄弱,要掌握到什么程度才能理解容器基础知识
    2018-09-07
    作者回复

    就本专栏里提到的东西,不懂的搜一下,就足够了。

    2018-09-07

  • Vincen
    简单易懂
    2018-09-07
  • 再见孙悟空
    我不会做题,等下面的大大
    2018-09-07
  • 老师,问题是这样的,只要我这个镜像没有运行的容器,这个镜像过段时间就会自动被删掉,我用docker images 看不到那个镜像了 。磁盘还有100多G,已占用96%。老师怎么继续再评论再评论
    2018-10-19
  • 老师,问题是这样的,只要我这个镜像没有运行的容器,这个镜像过段时间就会自动被删掉,我用docker images 看不到那个镜像了
    2018-10-19
    作者回复

    这是kubele的image gc功能,你磁盘不够大了吧

    2018-10-19

  • hai:
    针对x86制作的镜像在ARM平台上不能使用,是因为内核不相同吗?
    2018-10-17
    作者回复

    不止内核呀,指令集都不一样

    2018-10-17

  • 老师好,请问,如果我在ubuntu:16.04的基础镜像上加了自己的东西,然后build新的镜像,这时候其他人用我这个新镜像的时候,是不是只读层就包含了我添加的东东?
    2018-10-10
    作者回复

    没错

    2018-10-11

  • 凯文1985
    如果对docker原始镜像进行修改 比如在ubuntu镜像上安装Java 那么增加的层是在最下面的只读层上吗
    2018-10-07
    作者回复

    修改当然是可读写层。提交后变成只读层的最上面一层。

    2018-10-08

  • 广宇
    对aufs的讲解,这一篇很精彩
    2018-10-05
  • 李博越
    既然容器里的rootfs 只是一个操作系统所包含的文件、配置和目录,那为啥我在dockerfile中配置keepalive参数‘/proc/sys/net/ipv4/tcp_keepalive_time’不生效呢?为啥在容器宿主机上配置才生效?
    查询了下官方文档,是因为这个配置是kernel层面的所以必须修改宿主机吗?但是这个配置是在/proc下的呀?
    参考文档:http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html
    2018-10-04
    作者回复

    这个例子不正好说明了容器镜像只有文件没有内核么?你通过proc文件不管改了啥,必然都影响的是宿主机内核啊。

    2018-10-04

  • 尧尧尧
    感觉unionfs和git有些类似哎
    2018-10-03
  • 多霸
    老师好,请教一个问题,现在刚开始学习docker,目前从网上下载的镜像都是多层的,并且最底层的我理解一般都是一套完整的操作系统目录结构吧,那如果我自己做镜像,只想运行个简单的可执行文件,想和宿主机的操作系统文件完全保持一致,那是不是可以不打操作系统那层镜像呢?如果可行,这么做的话,镜像就能很小了,但同时是不是也有一些缺点,比如依赖性,安全性上?还理解不到位,所以还请多多指教,谢谢
    2018-09-30
    作者回复

    确实不到位……容器文件系统跟宿主是完全隔离的

    2018-09-30

  • ╯梦深处゛
    喜欢这种从出现到发展、由浅入深、由一小段代码讲述某种实现的讲述,既容易理解,也深入了解了原理。娓娓道来,很有条理,就像身临当时docker的实现场景!
    2018-09-27
    作者回复

    我一向反对源码分析。那是偷懒的态度。

    2018-09-27

  • ╯梦深处゛
    只读层:

    ```
    $ ls /var/lib/docker/aufs/diff/72b0744e06247c7d0...
    etc sbin usr var
    $ ls /var/lib/docker/aufs/diff/32e8e20064858c0f2...
    run
    $ ls /var/lib/docker/aufs/diff/a524a729adadedb900...
    bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
    ```

    理论上只读层上层的会覆盖下层的,但是增量修改应该发生在读写层,那为什么这个代码中会出现上层目录和最下层目录相同的情况?按理来说这些目录不是应该不会变化的吗?有多个的原因是什么?
    2018-09-27
     作者回复
    名字一样不代表里面的内容一样啊,内容一样也不代表权限一样啊

    2018-09-27
    ------------------------------------------------------------------------------------------------------

    感谢回答!还有个疑问:为什么其他两层都只有一层,而只读层却有5层呢?而5层却可能存在同目录或文件不一样的情况,为什么不采取从最上层往最下层覆盖,最后只保留一层?
    2018-09-27
  • ╯梦深处゛
    只读层:

    ```
    $ ls /var/lib/docker/aufs/diff/72b0744e06247c7d0...
    etc sbin usr var
    $ ls /var/lib/docker/aufs/diff/32e8e20064858c0f2...
    run
    $ ls /var/lib/docker/aufs/diff/a524a729adadedb900...
    bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
    ```

    理论上只读层上层的会覆盖下层的,但是增量修改应该发生在读写层,那为什么这个代码中会出现上层目录和最下层目录相同的情况?按理来说这些目录不是应该不会变化的吗?有多个的原因是什么?
    2018-09-27
    作者回复

    名字一样不代表里面的内容一样啊,内容一样也不代表权限一样啊

    2018-09-27

  • Leon廖
    image展开后是一系列目录,但push/pull在网络上传送的image是tar格式的?
    就好像Java的jar其实是zip格式的
    2018-09-26
    作者回复

    对,压缩的

    2018-09-26

  • 祁勇
    容器启动,看到的内存和宿主机一样大,如何改变这个大小
    2018-09-22
    作者回复

    参考某一篇的思考题

    2018-09-23

  • liuy1226
    老师您好,我们知道docker镜像采用了镜像共享技术减少了镜像存储空间的大小,现在的问题是docker镜像这种实现的数据结构模型是什么,比如多个镜像共享了一个基础镜像。
    2018-09-19
    作者回复

    当然是 图

    2018-09-19

  • manatee
    老师您好,我下载了18.06的docker已经不使用aufs而是overlay作为磁盘存储,能讲一下这当面的内容吗
    2018-09-19
    作者回复

    先理解aufs为主,其他类型的原理其实非常类似,有机会介绍

    2018-09-19

  • 黄朋飞
    请问老师,目录挂载到容器后,这个目录对于容器内是可见的,对于宿主机不可见,那相应的容器挂载的这个目录占用的是宿主机的磁盘空间吧?我这时候查看宿主机的磁盘空间发现一部分被占用,我还不知道被谁占用了,是不是很奇怪?
    2018-09-19
    作者回复

    容器只是一个进程,所有空间都占的是宿主机的

    2018-09-19

  • cxyfreedom
    想问一下老师那段程序启用了 Mount Namespace,仍然能在宿主机上面同样查看到挂载的 /tmp 目录,谢谢
    2018-09-14
  • Leon📷
    你好,我编译那个容器程序,没有进入容器,程序直接退出了
    2018-09-13
  • 阿恺
    现在的docker版本好像是使用overlay(至少我在Ubuntu18.04是这样),overlay里面的workdir不明白什么含义,老师能否介绍一下。
    2018-09-13
    作者回复

    篇幅所限,暂时不能全覆盖了。不过原理相通,可以先自己尝试学习一下。

    2018-09-13

  • 青空
    老师好 请问图中init层也是ro+wh的 那比如改了host最后是怎么生效的 感觉会和底下的那层一样做法?
    2018-09-12
    作者回复

    是。跟只读层道理一样。

    2018-09-13

  • 殷智伟
    在容器里面可以看到/etc/hostname文件的内容,这个文件在init层,但是在/var/lib/docker/overlay2/xxxx/merged/etc和/var/lib/docker/overlay2/xxxx-init/diff/etc目录下,hostname文件都看不见内容。init层在容器启动时被修改的内容保存在哪里去了?
    2018-09-12
  • 大麦08
    请问老师,在centos里aufs存放在哪里啊
    2018-09-12
    作者回复

    centos默认用的一般是devicemapper

    2018-09-12

  • 从一路
    同样遇到 ./ns 启动失败
    Parent - start a container
    Parent - container stopped

    我切换到root用户就好了
    2018-09-11
  • melon
    老师,怎么根据 containerid 找到它的联合挂载点呢?也就是怎么确定某个容器的 /var/lib/docker/aufs/mnt/6e....,试了一下docker inspect 没发现可用的信息
    2018-09-11
    作者回复

    可以通过观察mnt里新增加的目录找到

    2018-09-11

  • 继富
    读写层的内容会持久保存吗,保存在哪里?又或者每次重启容器就没了?
    2018-09-09
  • 初学者
    对镜像中的init层不是很理解,这一层中的文件的修改到底由谁以及什么时候触发的,如果是在容器启动阶段,修改的结果不是应该放到容器读写层吗?
    2018-09-09
    作者回复

    当然是docker引擎做的,在容器启动之前就做好这个层,和其他层一起挂载好。然后容器才会创建。

    2018-09-10

  • 择动
    所以我用mac的话,也是和windows一样,又多跑一个vm来运行linux容器吗?那么,由前几节讲的是不是运行vm就会比在linux运行直接运营容器多一些性能损耗?
    2018-09-08
  • 随风
    请教,istio 才1.0,有大的系统用它实现的嘛?现在旧系统服务化,是不是采用spring cloud 比较稳妥?
    2018-09-08
    作者回复

    是这样,还比较初步

    2018-09-10

  • 随风
    请教一下,IBM 推 icp,里边集成了istio,可用嘛,谢谢,目前对于微服务治理,开发,您有什么建议?spring cloud?
    2018-09-08
    作者回复

    istio本身当然是最建议采用的。

    2018-09-08

  • 假装乐
    竟然听漏了,我晕,明白了明白了
    2018-09-07
  • 心情不错
    需要修改的都是可读写层 所以修改镜像内容不需要关心只读层
    2018-09-07
  • xianhai
    docker占用的port和宿主机的port互不干扰,docker是怎么实现port space独立的?
    2018-09-07
    作者回复

    就是network namespace啊

    2018-09-07

  • Leo
    只读层如何增加增量,是可读写层上的修改转过去的吗?
    2018-09-07
    作者回复

    只读层永远不会变

    2018-09-07