【云原生•容器】容器技术的秘密武器:90%的人不了解的Namespace魔法,必收藏!
容器案例实战
Mount Namespace
mount namespace 是用来隔离各个进程看到的挂载点视图,这样进程就只能看到自己的 mount namespace 中的文件系统挂载点。在 mount namespace 中调用mount() 和 unmount() 仅仅只会影响当前 namespace 内的文件系统,而对全局的文件系统是没有影响的。
❝mount namespace 是 Linux 内核实现的第一个 namespace,从内核的 2.4.19 版本开始加入,因此,它的系统调用参数是NEWNS(New Namespace的缩写),当时开发者还没有意识到,以后还会有很多类型的 namespace 加入进来。
下面继续上篇博文实战案例:
Linux系统下/tmp目录一般就是采用tmpfs文件系统挂载,它是一种基于内存的虚拟文件系统,即它的存储空间都在内存里,它最大的特点就是读写速度快,但是重启系统该目录下文件就会丢失。所以,它一般比较适合做如web缓存、上传下载临时文件等,一般容器里/tmp要重新挂载tmpfs文件系统,避免容器里/tmp可以看到宿主机上各种缓存文件信息。
下面我们就来操作如何将容器中/tmp重新挂载tmpfs文件系统,并且由于mount namespace可以隔离进程的挂载点视图,这样就和宿主机之间隔离的更加彻底。
1、切换到上面1号shell,在容器环境里查看/tmp目录,可以看到容器里/tmp目录下可以看到宿主机上应用产生的各种缓存文件:
2、将容器里/tmp重新挂载tmpfs文件系统:
[root@docker02 ~]# mount -t tmpfs -o size=200m tmpfs /tmp
3、再次查看容器环境下/tmp目录,已和宿主机上/tmp隔离开来:
4、在宿主机上查看挂载点,在容器里挂载操作并未影响宿主机,说明mount namespace生效:
容器根文件系统切换(pivot_root)
目前为止,一个完整的容器基本成型,mount namespace将容器和宿主机的文件系统挂载点资源隔离开来,切换到上面1号shell,查看容器里挂载点:
发现这时容器的挂载点和宿主机基本一样,这是为什么呢?
mount namespace虽然隔离进程挂载点资源,但是默认容器会复制父进程的挂载点,只有后续容器里挂载点调整对宿主机来说是不可见的,而且现在容器还有个比较大的问题:根文件系统和宿主机一致,即在容器里看到的文件系统和宿主机上基本一致。
这就需要用到 pivot_root 系统调用,它能够将一个 mount namespace 中的所有进程的根目录切换到一个指定路径。常见的使用场景:创建容器时,在创建新的 mount namespace 之后,通过 pivot_root()
将容器内进程的根目录切换到镜像文件对应的 rootfs
所在目录。比如 CentOS 宿主机上拉取一个 Ubuntu 镜像,通过调用 pivot_root 把容器根目录指向这个 Ubuntu 文件系统,那么这个新启动的进程就会傻乎乎的认为自己真的是在 Ubuntu 环境下;同理,如果提供的是一个简单的 busybox 的文件系统,新进程感知的就是一个简单 busybox 环境。
在继续pivot_root之前,我们先来了解下什么是rootfs?
典型的Linux文件系统由bootfs与rootfs两部分组成,bootfs(boot file system)可以看出系统内核部分,用户不会修改这个文件系统,对于同样内核版本的不同的 Linux 发行版的 bootfs 都是一致的。
rootfs(Root Filesystem)是分层文件树的顶端,系统启动时会将 rootfs 挂载到 /
目录,之后再挂载其他的文件系统到其子目录中。rootfs 可以看成一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。rootfs 包含典型的目录结构如 /dev, /proc, /bin, /etc, /lib, /usr, and /tmp 等,这个文件系统在不同的Linux 发行版中是不同的,而且用户可以对这个文件进行修改。
pivot_root的语法:
pivot_root new_root put_old
pivot_root将当前进程的根文件系统移动到put_old目录,并使new_root成为新的根文件系统,然后umount掉put_old。这里有几点需要注意:
new_root 目录必须是一个挂载点 ,并且new_root目录下有完整rootfs的各种文件
new_root 目录挂载点应该是一个与宿主机不同 mount namespace,不然会导致整个宿主机受到影响
put_old 文件夹位于 new_root 目录下
下面我们就来通过一个案例:在 CentOS 宿主机上构建一个 Ubuntu 系统环境的容器,实操下如何给容器指定根文件系统:
1、使用docker容器制作rootfs
先在宿主机上某一个命令上准备一个精简的ubuntu精简文件系统,作为后续容器运行时挂载使用的rootfs,随便打开一个宿主机shell执行下面操作即可:
1、拉取镜像:
[root@docker02 ~]# docker pull ubuntu
2、基于centos镜像创建容器:
[root@docker01 ~]# docker run -d ubuntu top -b
3db8a24af88bfee75e0ed9b214118e33712978744fb5d9490748da39f714d048
3、容器文件系统导出tar文件:
[root@docker02 ~]# docker export -o ubuntu.tar 3db
4、将容器导出的tar文件解压到/docker/ubuntu目录下
[root@docker02 ~]# mkdir -p /docker/ubuntu
[root@docker02 ~]# tar -xvf ubuntu.tar -C /docker/ubuntu
2、创建容器:
[root@docker02 ~]# unshare --uts --net --pid --ipc --fork --map-root-user --mount-proc /bin/bash
3、pivot_root切换根文件系统
上面容器环境shell下,在容器环境里切换当前容器根文件系统:
1、创建容器根文件系统路径:
[root@docker02 ~]# mkdir -p /containers/ubuntu-root
2、new-root必须是一个独立的挂载点:
[root@docker02 ~]# mount --bind /containers/ubuntu-root /containers/ubuntu-root
3、创建old-root:
[root@docker02 ~]# mkdir /containers/ubuntu-root/old-root
4、将上述制作的rootfs拷贝到new-root目录下,这样new-root文件夹里面有完整rootfs的各种文件:
[root@docker02 ~]# cp -r /docker/ubuntu/* /containers/ubuntu-root/
5、切换容器根文件系统:
[root@docker02 ~]# pivot_root /containers/ubuntu-root/ /containers/ubuntu-root/old-root
6、重新挂载proc文件系统:
[root@docker01 ~]# mount -t proc proc /proc
❝注意:
1、执行pivot_root切换根文件系统导致/proc挂载丢失,需要容器里对proc文件系统重新挂载,不然执行mount、ps等指令都会报错。
2、由于mount namespace隔离特性,/containers/ubuntu-root挂载点只在容器里有效,所以宿主机上/containers/ubuntu-root目录为空。
4、验证下容器环境根文件系统是否切换成功
继续在容器下shell执行mount查看容器环境挂载点:
发现容器里挂载点依然和宿主机差不多,但是发现有个变化,很多挂载点位置发生了变化,移入到了/old-root目录下,这正是pivot-root操作将原有挂载点移入到old-root指定的路径下,这些挂载点已没啥作用,使用umount指令卸载挂载点即可,卸载后再次查看容器挂载点已经和宿主机完全不一致:
查看下容器环境下系统信息,发现已经变成ubuntu系统,而当前宿主机系统是centos:
而且容器里根文件系统(/)和宿主机上隔离开来,容器里根文件系统切换到上面指定的/containers/ubuntu-root目录下:
总结
通过 mount namespace 和 pivot_root 技术,我们可以将容器根文件系统切换到任意指定路径,从而实现文件系统的彻底隔离,后续分析镜像原理时会发现,这些根文件系统主要基于 unionFS 联合文件系统技术叠加镜像层构建而来。
镜像的底层通常情况是一个操作系统,如Ubuntu、Debian 和CentOS等,用户的镜像通常构建在这些基础镜像层之上,因此,一个镜像就可以看成是一个rootfs,镜像打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。
有了容器镜像"打包操作系统"的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。