容器进阶:运行环境的一致性
参考:深入剖析Kubernetes 镜像分层结构理论详解1 深入理解Docker容器和镜像 Union File System
上文简单介绍了容器的隔离与限制,下文会更进一步去讲解docker是如何保持一致性的。
Docker在镜像的设计中引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量rootfs(根文件系统)。
这里涉及到几个概念:rootfs、image、layer和Union File System 。
rootfs
什么是rootfs?rootfs是内核启动时所mount的第一个文件系统,内核代码映像文件保存在根文件系统中,而系统引导启动程序会在根文件系统挂载之后从中把一些基本的初始化脚本和服务等加载到内存中去运行。
docker架构下沿用了linux中的rootfs的思想,当 docker daemon 为 docker 容器挂载 rootfs 时与传统的linux内核极其类似,将其设置为只读模式,在rootfs挂载完成之后,和linux内核不一样的是,docker daemon没有将它切换为读写模块。
可以这样理解,对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
- 启用 Linux Namespace 配置;
- 设置指定的 Cgroups 参数;
- 切换进程的根目录(Change Root)
image和layer
容器镜像的rootfs是容器只读的文件系统,rootfs又是由多个只读的image构成。于是,rootfs中每个只读的image都可以称为一层layer。
除了只读的image之外,Docker Daemon在创建容器时会在容器的rootfs之上,再mount一层read-write filesystem,而这一层文件系统,也称为容器的一层layer,常被称为top layer。
Docker的设计理念中,top layer转变为image的行为(Docker中称为commit操作),大大释放了容器rootfs的灵活性。Docker的开发者完全可以基于某个镜像创建容器做开发工作,并且无论在开发周期的哪个时间点,都可以对容器进行commit,将所有top layer中的内容打包为一个image,构成一个新的镜像。Commit完毕之后,用户完全可以基于新的镜像,进行开发、分发、测试、部署等。不仅docker commit的原理如此,基于Dockerfile的docker build,其追核心的思想,也是不断将容器的top layer转化为image。
Union File System
Docker的存储驱动的实现是基于Union File System,简称UnionFS,他是一种为Linux 、FreeBSD 和NetBSD 操作系统设计的,把其他文件系统联合到一个联合挂载点的文件系统服务。它用到了一个重要的资源管理技术,叫写时复制。写时复制(copy-on-write),也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。对于一个重复资源,若不修改,则无需立刻创建一个新的资源,该资源可以被共享使用。当发生修改的时候,才会创建新资源。这会大大减少对于未修改资源复制的消耗。
OverlayFS采用UFS模式,但相对于AUFS,其性能更高。在Docker中主要有overlay和overlay2两种实现。Docker-CE默认采用overlay2。
OverlayFS中使用了两个目录,把一个目录置放于另一个之上,并且对外提供单个统一的视角。下层的目录叫做lowerdir,上层的叫做upperdir。对外展示的统一视图称作merged。创建一个容器,overlay驱动联合镜像层和一个新目录给容器。镜像顶层是overlay中的只读 lowerdir,容器的新目录是 可读写 的upperdir。它们默认存储于/var/lib/docker/overlay2/目录下。例如:
[root@test-04 overlay2]# tree -L 2
.
├── 19435179b7c97d5363928de2e44c675528dac3c58c855e33396d907f023621cd
│ ├── committed
│ ├── diff
│ └── link
├── 31dd5a960bc9d2b35771eaca22a598da3bfb089ac37120d14282eaaa20d83de2
│ ├── diff
│ ├── link
│ ├── lower
│ └── work
├── 91ce67fbd28ed8938a39248405656dbbd9a5d66025cae63ee58d044e61c8907f
│ ├── committed
│ ├── diff
│ └── link
├── 9c696d2632aa0894128ad9ab3141a8ed6cc8d6b012d5634b93527ef72b538106
│ ├── diff
│ ├── link
│ ├── lower
│ └── work
├── 9c696d2632aa0894128ad9ab3141a8ed6cc8d6b012d5634b93527ef72b538106-init
│ ├── committed
│ ├── diff
│ ├── link
│ ├── lower
│ └── work
├── 9f557c818388f90c28b782095bb37e8ad39c0d2d8888d0d4f96958d0ac941630
│ ├── committed
│ ├── diff
│ ├── link
│ ├── lower
│ └── work
├── backingFsBlockDev
└── l
├── 44ISUTVEWAUB3GXP5K5ZTY66S4 -> ../9c696d2632aa0894128ad9ab3141a8ed6cc8d6b012d5634b93527ef72b538106/diff
├── 6WQVWTVP4BK4LSS6FEMXYI7UJL -> ../91ce67fbd28ed8938a39248405656dbbd9a5d66025cae63ee58d044e61c8907f/diff
├── FUB2EZA444WCH3SP5VKRH3SPES -> ../19435179b7c97d5363928de2e44c675528dac3c58c855e33396d907f023621cd/diff
├── IXUFBMAQP26LTFBD7APAIALQ2B -> ../31dd5a960bc9d2b35771eaca22a598da3bfb089ac37120d14282eaaa20d83de2/diff
├── OPVHT4CAHBKEYLSAQ7BCJ7RCGL -> ../9c696d2632aa0894128ad9ab3141a8ed6cc8d6b012d5634b93527ef72b538106-init/diff
└── R4JCGPBIE4JRIOHLUFHILT5XJR -> ../9f557c818388f90c28b782095bb37e8ad39c0d2d8888d0d4f96958d0ac941630/diff
说明:
- l:目录下存储了多个软链接,使用短名指向其他各层;
- diff:包含该层镜像的具体内容,即upperdir和lowerdir,此处都为lowerdir;
- link:记录该目录对应的短名;
- lower:记录该目录的所有lowerdir,每一级间使用
:
分隔; - work:该目录是OverlayFS功能需要的,会被如copy_up之类的操作使用;
-init
以结尾的是顶层的lowerdir
的父目录,目的是为了初始化container配置信息;merged
:该目录就是container的mount point,这就是暴露的lowerdir
和upperdir
的统一视图。任何对容器的改变也影响这个目录。
总结
- Namespace+Cgroups 构成的隔离环境,这一部分我们称为容器运行时(Container Runtime),是容器的动态视图。
- 挂载在 /var/lib/docker/overlay2/<directory_of_running_container>上的 rootfs,这一部分我们称为容器镜像(Container Image),是容器的静态视图,用于保持容器的一致性;
在 rootfs 的基础上,Docker 公司创新性地提出了使用多个增量 rootfs 联合挂载一个完整 rootfs 的方案,这就是容器镜像中“层”的概念。通过“分层镜像”的设计,以 Docker 镜像为核心,来自不同公司、不同团队的技术人员被紧密地联系在了一起。而且,由于容器镜像的操作是增量式的,这样每次镜像拉取、推送的内容,比原本多个完整的操作系统的大小要小得多;而共享层的存在,可以使得所有这些容器镜像需要的总空间,也比每个镜像的总和要小。这样就使得基于容器镜像的团队协作,要比基于动则几个 GB 的虚拟机磁盘镜像的协作要敏捷得多。
一旦这个镜像被发布,那么你在全世界的任何一个地方下载这个镜像,得到的内容都完全一致,可以完全复现这个镜像制作者当初的完整环境。这,就是容器技术“强一致性”的重要体现。