本文字数:8814 字
精读时间:18 分钟
也可在 8 分钟内完成速读
容器到底是什么?容器是怎么工作的?容器如何隔离资源?为啥容器启动那么快?...如果你是个好奇宝宝,平时在使用容器的时候内心定会泛起类似疑问。本文将通过讲解其三大核心技术:Linux Namespace,Control Groups(cgroups)和UnionFS (联合文件系统)来解答你心中对容器原理的种种疑问。
Linux Namespace
Linux Namespaces是Linux内核提供的一种资源隔离方案。Namespaces之间的资源相互独立。目前Linux中提供七种namespace。
参考:http://man7.org/linux/man-pages/man7/namespaces.7.html
Namespace | Flag | 说明 |
Cgroup |
CLONE_NEWCGROUP | 隔离cgroup |
IPC |
CLONE_NEWIPC | 隔离进程间通信 |
Network |
CLONE_NEWN |
隔离网络资源 |
Mount |
CLONE_NEWNS | 隔离挂载点 |
PID | CLONE_NEWPID | 隔离进程的ID |
User |
CLONE_NEWUSER | 隔离用户和用户组的ID |
UTS |
CLONE_NEWUTS | 隔离主机名和域名信息 |
向clone
系统调用传入上述表格中对应的Flag
参数,可以为新建的进程创建相应的namespace。也可以使用setns
系统调用将进程加入到一个已经存在的namespace中。容器通过namespace技术来实现资源隔离。
namespaces限制容器能看到哪些资源。
示例:linux下通过shell创建一个容器
Talk is cheap, show me the code。
我们直接用一个示例来演示一下namespace隔离资源的效果。在命令行下,我们可以通过unshare
命令来启动一个新进程,并为其新建相应的命名空间。在这个示例中,我们将通过unshare
为我们的容器创建除cgroup
和user
之外的所有命名空间,这也是docker run something
默认为容器创建的命名空间。本示例依赖docker
环境来为我们提供一些配置上的便利。完整的示例script放在这里,方便大家scriptreplay
回看过程。
git clone https://github.com/DrmagicE/build-container-in-shell
cd ./build-container-in-shell
scriptreplay build_container.time build_container.his
step1: 准备一个rootfs
首先,我们要为我们的容器准备自己的rootfs,用来为容器进程提供隔离后执行环境的文件系统。这里我们直接导出alpine
镜像作为我们的rootfs,选择/root/container
目录作为镜像rootfs:
[root@drmagic container]# pwd
/root/container
[root@drmagic container]# # 修改mount类型为private,确保后续的mount/umount不会在namespace之间传播
[root@drmagic container]# mount --make-rprivate /
[root@drmagic container]# CID=$(docker run -d alpine true)
[root@drmagic container]# docker export $CID | tar -xf-
[root@drmagic container]# ls # rootfs建立好啦
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
step2: 命名空间隔离
[root@drmagic container]# # 使用unshare为新的shell创建命名空间
[root@drmagic container]# unshare --mount --uts --ipc --net --pid --fork /bin/bash
[root@drmagic container]# echo $$ # 看看新进程的pid
1
[root@drmagic container]# hostname unshare-bash # 修改一下hostname
[root@drmagic container]# exec bash #替换bash,显现hostname修改后的效果
[root@unshare-bash container]# # hostname变化了
通过上面的过程,我们可以看到UTS
和PID
这两个命名空间的隔离效果。
如果你在这一步使用ps来查看所有的进程,结果可能会令你失望——你仍然会看到系统中的所有进程,就像没有隔离成功一样。但这是正常的,因为ps读取/proc下的信息,此时的/proc还是host的/proc,所以ps还是能看到所有的进程。
step3:隔离挂载信息
[root@unshare-bash container]# mount # 还是能看到host上的mount
/dev/vda2 on / type xfs (rw,relatime,attr2,inode64,noquota)
devtmpfs on /dev type devtmpfs (rw,nosuid,size=1929332k,nr_inodes=482333,mode=755)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
mqueue on /dev/mqueue type mqueue (rw,relatime)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime)
.....
我们发现mount
依然能够获取全局挂载信息,难道是mount
命名空间隔离没生效?非也,mount
命名空间已经生效了。当新建一个mount
命名空间时,他会拷贝父进程的挂载点,但对该命名空间挂载点的后续修改将不会影响到其他命名空间。
参考:
http://man7.org/linux/man-pages/man7/mount_namespaces.7.html#DESCRIPTION
命名空间内挂载点的修改不影响其他命名空间有一个前提条件——mount的propagation type要设置为MS_PRIVATE,这也是为什么一开始我们要执行 mount --make-rprivate / 的原因
因此我们看到的mount信息是父进程的一份拷贝,我们重新mount一下/proc,好让ps能正常显示。
[root@unshare-bash ~]# # 重新mount一下/proc
[root@unshare-bash ~]# mount -t proc none /proc
[root@unshare-bash ~]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:29 pts/0 00:00:00 bash
root 77 1 0 21:47 pts/0 00:00:00 ps -ef
[root@unshare-bash ~]# # 啊哈,现在我们的ps正常了!
处理完了/proc
的挂载,我们还需要清理旧的挂载点,将他们umount
掉,这一步我们需要借助pivot_root(new_root,put_old)
来完成。pivot_root
将当前mount namespace
下的所有进程(线程)的根目录挂载点切换至new_root
,并将旧的根目录挂载点放到put_old
目录下。使用pivot_root
的主要目的是用来umount
一些从父进程copy
过来的挂载点。
http://man7.org/linux/man-pages/man2/pivot_root.2.html
为了满足pivot_root
的一些参数要求,需要额外做一次bind mount: