03 容器的镜像与UnionFS

容器的镜像构成直接关系到你对启动一个容器的理解 ,非常重要

一 引入问题

每启动一个容器,在容器内都有一个操作系统,这里有两个问题我们需要搞清楚

(1)容器的操作系统来源于什么?

(2)我们是如何把操作系统安装到容器里的?

二 容器的操作系统来源

2.1 操作系统的来源

你一定为物理机/虚拟机安装过操作系统,具体你是先下载了一个系统镜像文件,然后把该镜像写到了u盘或光盘里称之为启动盘,然后用来为机器安装操作系统。也就说一台机器的操作系统来自于一个镜像文件,我们称之为系统镜像。

一个容器就相当于一台“虚拟机”,既然虚拟机的操作系统来源于镜像,那么容器内的操作系统必然也来源于镜像了?完全正确

但因为容器要做到全方位的轻量级,所以容器的操作系统镜像必然也要是在一个完整操作系统镜像的基础上做精简,具体如何实现的呢?我们来一一解析

2.2 完整操作系统的镜像

镜像的本质就是一个iso格式的压缩包,操作系统镜像里面存放了该系统所有的内容,具体来说分为两大部分

一个典型的 Linux 文件系统由 bootfs 和 rootfs 两部分组成

  • 1、bootfs(boot file system) 主要包含 bootloader 和 kernel,bootloader 主要用于引导加载 kernel,当 kernel 被加载到内存中后 bootfs 会被 umount 掉,从而释放内存,同样的内核版本不同Linux发行版,其bootfs都是一致的。
  • 2、rootfs (root file system) 包含的就是典型 Linux 系统中的/dev,/proc,/bin,/etc 等标准目录和文件。Linux系统在启动时,rootfs首先会被挂载为只读模式,然后在启动完成后被修改为读写模式,随后它们就可以被修改了。不同的 linux 发行版(如 ubuntu 和 CentOS ) 在 rootfs 这一层会有所区别,体现发行版本的差异性。

2.3 docker image是什么?如何构成的

上一小节提到,一个完整的文件系统是由bootfs+rootfs组成的

而容器image镜像其实就是把rootfs打包到一起(镜像的本质就是一种压缩包),这个rootfs系统里包含了各种依赖文件,以及你的应用程序文件都在一起。

也就说世界上第一个镜像是怎么产生的呢?

就是某人在一台物理机上安装了一个linux操作系统,包括bootfs,也包括rootfs,然后他在rootfs里安装了各种依赖包,做好了软件启动的一切配置,然后他把这个rootfs(译:根文件系统)打包压缩成了一个文件,这个文件就称之为docker的image,也就是镜像。

我们启动一个容器,必须指定一个镜像,镜像里包含的是rootfs,目的就是把镜像文件共享给了容器的名称空间里,在容器内,可以看到完全独立的文件系统,查看容器中的根文件系统 (rootfs)。然后,你会发现,它和宿主机上的根文件系统也是不一样的。所以此时你应该知道了,在容器里看到的根文件系统,就是镜像里的东西。下图docker image 中最基础的两层结构,不同的 linux 发行版(如 ubuntu 和 CentOS ) 在 rootfs 这一层会有所区别,体现发行版本的差异性。

需要再次强调的一点就是,容器本质是没有内核的、它只有rootfs,至于下层的bootfs内核部分是共享自物理机的,并且所有容器共享的都是物理主机上的运行系统的内核即bootfs相关内容。

ps:物理机运行的文件系统内核如果有漏洞,那么必然会影响到容器。

三 如何为容器安装操作系统

3.1 UnionFS(联合文件系统)的由来

容器就相当于一台虚拟机嘛,所以我们还是对比着来看。

安装操作系统的原理本质很简单,操作系统的开发者在他们自己的机器上把操作系统代码写好了之后,要给我们用、要放到我们的机器上跑起来,怎么做?

  • 第一步:操作系统的开发者们会把自己编写的操作系统代码打包成一个压缩包,格式为iso,称之为系统镜像,镜像顾名思义,你去照镜子,镜子里的像给你是完全一样的,所以系统镜像的意思就是说,该文件就是完全拷贝了开发者们开发的操作系统代码文件,一模一样。

  • 第二步:把iso文件“释放/解压”到目标主机,这就是安装操作系统。如何释放呢?如果目标位置是一块裸盘,那就需要借助一个传输介质,也就是我们说的U启、系统光盘等传输介质

那如何为容器安装操作系统呢?原理都是一样的,但对于容器来说,我们是想在宿主机上启动多个容器,让它们拥有操作系统,所以宿主机就是我们的传输介质,目标位置就是容器,我们需要做的就是把容器的镜像下载到宿主机就可以,让把容器的镜像文件关联给每个容器1就行,如果你想让容器有不同的系统,那就关联不同的镜像就可以,不同的镜像就是不同的系统嘛

但说到这里,我们就需要思考一个问题,对于虚拟机来说,它很笨重,每个虚拟机要想拥有自己的操作系统,我们都需要把一份操作系统镜像的内存完整地拷贝给它,如果一个镜像为1G,部署了10台虚拟机,就需要耗费10G的空间,而这10G的内容都是冗余的,

所以对于容器来说,具体做法并不是把容器镜像直接拷贝给每个容器的名称空间,而是通过挂载的方式完成的,而且是以只读的方式挂载的,至于容器内只负责存自己新增的改动就可以,不影响其他容器,这就用到了UnionFS(联合文件系统)技术,

UnionFS(联合文件系统)详解如下

3.2 总结同一个镜像启动多个容器的本质

容器的镜像的本质就是rootfs,启动容器,指定镜像的意义,就是为了把rootfs共享给容器的名称空间,这就如同给容器安装了一个操作系统一样。但是等一等,为何我这里用的是共享这个词呢?镜像的本质就是把rootfs打包压缩得到一个压缩包,那么启动容器不应该是把该压缩包解压到该容器的名称空间内吗?

思路完全正确,但具体操作不是这样的!

如果真的这么做,那假设我们一个镜像10G,那么我们用这同一个镜像启动10个容器,岂不是要解压10次,数据冗余达到了50G,太浪费了。

那如何解决该问题呢?

先说答案:我之前提到的共享一次的意思指的就是,我们用一个镜像启动容器,其实是把该镜像里的目录都mount挂载到了容器名称空间内,并且设置为只读(如何写入,后续在介绍),也就说,我们用同一个镜像启动了10个容器,其实本质就是把这个镜像里的目录mount给了10个容器名称空间而已,因此大家是共享镜像的。

ps:与namespace联合在一起思考,namespace中隔离的诸多资源中就包括mount挂载,如此,哪怕是同一个镜像,mount到了不同的容器或者说名称空间里,肯定是与其他容器/名称空间隔离的

3.3 容器内如何写入数据

上一小节提到多个容器用一个镜像启动,本质是以只读方式把镜像目录都mount到了每个容器的名称空间里。那容器内岂不是都无法写入数据了,但是我们明明是可以在容器内写入数据的啊?难道说以只读方式挂在镜像是错误的?这肯定是没错的,要想理解明明启动容器是以只读方式把rootfs里包含的目录挂载到容器里,但是却可以在容器里执行增删改查操作,那就需要了解一下UnionFS联合文件系统。

什么是联合文件系统,先说答案:

容器内的rootfs根文件系统类型,并不是大在普通linux节点上看到的Ext4或者XFS之类的常见文件系统,而是Overlay,Overlay是一种联合文件系统UnionFS(OverlayFS简称overlay 是 UnionFS 的一种具体实现,更多实现详解3.8小节),联合文件系统顾名思义,可以把文件系统上多个目录(分支)内容联合挂载到同一个目录下,使用者也就是容器看起来认为是一个目录,而实际上在物理机里是由多个目录构成的。

OverlayFS使用两个目录,把一个目录置放于另一个之上,并且对外提供单个统一的视角。这两个目录通常被称作层,这个分层的技术被称作union mount。术语上,下层的目录叫做lowerdir,上层的叫做upperdir。对外展示的统一视图称作merged

lowerdir—>镜像层,只读

upperdir—->容器层,修改相关的内容都在这里放着

merged—->容器映射层,我们在容器里看到的就是这一层,所以该层也可以称之为展现层。

针对这三层,我们从下往下看,看到的merged里的内容来自与upperdir与lowdir,upperdir里的内容会遮挡住lowerdir里的内容。

了解

image 里面是一层层文件系统,叫做 Union FS(联合文件系统)。联合文件系统,可以将几层目录挂载到一起,形成一个虚拟文件系统。虚拟文件系统的目录结构就像普通 linux 的目录结构一样,docker 通过这些文件再加上宿主机的内核提供了一个 linux 的虚拟环境。每一层文件系统我们叫做一层 layer,联合文件系统可以对每一层文件系统设置三种权限,只读(readonly)、读写(readwrite)和写出(whiteout-able),但是 docker 镜像中每一层文件系统都是只读的。

lowerDir:挂载的是镜像层,init里放的是一些hosts、hostname、resolv.conf文件,因为每个容器启动
        肯能指定的网络,还有主机名都不一样,所以这些文件会单独挂载,docker引擎启动时候会动态修改保证每个容器
        的主机名等信息不一样,后面的diff目录就是镜像的rootfs,有几个就是代表这个镜像有几层(初始情况就一层,
        有可能该镜像是别人基于基础的centos镜像commit了很多次,即被制作了很多次,那么镜像的层级就会很多)

upperdir:是容器的可写层,改动的文件都在这里

upperdir的内容+lowerdir的内容共同构成了merged的内容,一切以upperdir为准,没有的去lowerdir中要
                如果是读,upperdir中没有则直接读lowerdir
                如果是写,则将文件从lowerdir中拷贝到upperdir中才能写,写完后,下次再写就直接从upperdir中取

                如果是删除,则在upperdir中将文件设置为隐藏,并不会真的删除掉lowerdir中的文件
                    详解如下:
                        容器会在镜像层创建一个whiteout文件,而镜像层的文件并没有删除,但是whiteout文件会隐藏它。
                        容器中删除一个目录,容器层会创建一个不透明目录,这和whiteout文件隐藏镜像层的文件类似

                如果是重命名目录 只有在源文件和目的路径都在顶层容器层upperdir时,才允许执行rename操作

容器overlay读写有三种场景

1、lowerdir里有,upperdir里没有

容器会通过overlay只读访问文件 容器层不存在的文件 如果容器只读打开一个文件,但该容器不在容器层(upperdir),就要从镜像层(lowerdir)中读取。这会引起很小的性能消耗。

2、lowerdir里没有,upperdir里有

只存在于容器层的文件 如果容器只读权限打开一个文件,并且容器只存在于容器层(upperdir)而不是镜像层(lowerdir),那么直接从镜像层读取文件,无额外的性能损耗

3、lowerdir里有,upperdir里也有

文件同时存在于容器层和镜像层 那么会读取容器层的文件,因为容器层(upperdir)隐层了镜像层(lowerdir)的同名文件,因此,也没有额外的性能损耗

总结一句话就是,lowerdir镜像层里的内容是只读的(可以通过挂载配置只读),增删改的文件都写到upperdir里,upperdir里的同名文件会遮挡住lowdir里的内容、优先级更高,lowerdir+upperdir联合挂载到了merged里

至此,你应该明白,镜像层里的内容肯定是只读的,但是读写的内容其实都放到了upperdir里,并且因为upperdir对lowerdir有遮挡效果,在当前容器里,可以看到修改,在其他容器里可能没有修改、于是在upperdir里看不到修改,没有遮挡,则看到的仍是lowerdir里的内容

了解:

有以下场景容器修改文件 第一次写一个文件,容器第一次写一个已经存在的文件,容器层不存在这个文件。overlay/overlay2驱动执行copy-up操作,将文件从镜像层拷贝到容器层。然后容器修改容器层新拷贝的文件

copy-up 操作只发生在第一次写文件时,后续的对同一个文件的鞋操作都是直接针对拷贝到容器层的文件
OverlayFS只工作在两层中。这比AUFS要在多层镜像中查找时性能要好
删除文件和目录 删除文件时,容器会在镜像层创建一个whiteout文件,而镜像层的文件并没有删除,但是whiteout文件会隐藏它。容器中删除一个目录,容器层会创建一个不透明目录,这和whiteout文件隐藏镜像层的文件类似

重命名目录 只有在源文件和目的路径都在顶层容器层时,才允许执行rename操作,否则返回EXDEV。因此,应用需要能够处理EXDEV,并且回滚操作,执行替代的”拷贝和删除”策略

为何部署docker与k8s时内核建议升级到4.0以上:overlay与overlay2

overlay驱动只能工作在两层之上,也就是说多层镜像不能用多层OverlayFS实现。替代的,每个镜像层在/var/lib/docker/overlay中用自己的目录来实现,使用硬链接这种有效利用空间的方法,来引用底层分享的数据。

注意: Docker1.10之后,镜像层ID和/var/lib/docker中的目录名不再一一对应

创建一个容器,overlay驱动联合镜像层和一个新目录给容器。镜像顶层中的overlay是只读lowerdir,容器的新目录是可写的upperdir

OverlayFS (overlay2)镜像分层与共享overlay驱动只工作在一个lower OverlayFS层之上,因此需要硬链接来实现多层镜像,但overlay2驱动原生地支持多层lower OverlayFS镜像(最多128层)。因此overlay2驱动在合层相关的命令(如build何commit)中提供了更好的性能,与overlay驱动对比,减少了inode消耗

在Docker中配置overlay2 存储驱动为了给Docker配置overlay存储驱动,你的Docker host必须在Linux kernel3.18版本之上,并且加载了overlay内核驱动。对于overlay2驱动,kernel版本必须在4.0或以上。OverlayFS可以运行在大多数Linux文件系统之上。

针对overlay2小结overlay2存储驱动已经成为了Docker首选存储驱动,并且性能优于AUFS和devicemapper。不过,也带来了一些与其他文件系统不兼容性,如对open和rename操作的支持,另外,overlay和overlay2相比,overlay2支持了多层镜像,优化了inode的使用。

配置docker使用overlay2
将配置持久化到配置文件/etc/docker/daemon.json中
"storage-driver":"overlay2"

[root@yq01-aip-aikefu19 ~]# docker info |grep overlay
 Storage Driver: overlay2

推荐阅读:https://www.cnblogs.com/linhaifeng/p/16443917.html

3.4 联合文件系统挂载实验

# 1、联合挂载
mkdir -p /test/lower1
mkdir -p /test/lower2
mkdir -p /test/lower3
mkdir -p /test/upperdir
mkdir -p /test/work
mkdir /test/merged

echo 111 > /test/lower1/1.txt
echo 222 > /test/lower2/2.txt
echo 333 > /test/lower3/3.txt

mount -t overlay overlay -o lowerdir=/test/lower1:/test/lower2:/test/lower3,upperdir=/test/upperdir,workdir=/test/work /test/merged

# 2、查看挂载
[root@yq01-aip-aikefu19 ~]# df
overlay         94465716  81721684  12727648  87% /test/merged

# 3、查看merge目录
[root@yq01-aip-aikefu19 ~]# ls /test/merged/
1.txt  2.txt  3.txt

# 4、修改
[root@yq01-aip-aikefu19 ~]# cd /test/merged/
[root@yq01-aip-aikefu19 merged]# echo 666 > 1.txt 
[root@yq01-aip-aikefu19 merged]# cat 1.txt 
666

# 然后我们去查看lowerdir,猜一下,1.txt改了没有,肯定没改啊
[root@yq01-aip-aikefu19 merged]# cat /test/lower1/1.txt 
111
# 修改的内容都在upperdir里
[root@yq01-aip-aikefu19 merged]# cat /test/upperdir/1.txt 
666

# 我们在merged目录下新增文件,本质也都是建立到了upperdir里
[root@yq01-aip-aikefu19 merged]# cd /test/merged/
[root@yq01-aip-aikefu19 merged]# echo 444 > 4.txt
[root@yq01-aip-aikefu19 merged]# ls
1.txt  2.txt  3.txt  4.txt
[root@yq01-aip-aikefu19 merged]# ls /test/upperdir/
1.txt  4.txt
[root@yq01-aip-aikefu19 merged]# 
[root@yq01-aip-aikefu19 merged]# ls /test/lower1/
1.txt
[root@yq01-aip-aikefu19 merged]# ls /test/lower2/
2.txt
[root@yq01-aip-aikefu19 merged]# ls /test/lower3/
3.txt

# 在merge目录下删除文件
[root@yq01-aip-aikefu19 merged]# rm -rf 2.txt 
[root@yq01-aip-aikefu19 merged]# 
[root@yq01-aip-aikefu19 merged]# ls -l /test/upperdir/
total 8
-rw-r--r-- 1 root root    4 Jun 28 15:47 1.txt
c--------- 1 root root 0, 0 Jun 28 15:50 2.txt  # 在upperdir里新建一个特殊文件来告诉OverlayFS这个文件不能出现在merged里,即代表它已经被删除了
-rw-r--r-- 1 root root    4 Jun 28 15:50 4.txt

总结:

我们可以看到,OverlayFS 的一个 mount 命令牵涉到四类目录,分别是 lower,upper, merged 和 work,那它们是什么关系呢?

1、首先,最下面的"lower/",也就是被 mount 两层目录中底下的这层(lowerdir)。

在 OverlayFS 中,最底下这一层里的文件是不会被修改的,你可以认为它是只读的/ro。我还 想提醒你一点,在这个例子里我们只有一个 lower/ 目录,不过 OverlayFS 是支持多个 lowerdir 的。

2、然后我们看"uppder/",它是被 mount 两层目录中上面的这层 (upperdir)。在 OverlayFS 中,如果有文件的创建,修改,删除操作,那么都会在这一层反映出来,它是 可读写的/rw。

3、接着是最上面的"merged" ,它是挂载点(mount point)目录,也是用户看到的目录, 用户的实际文件操作在这里进行。

4、其实还有一个"work/",这个目录没有在这个图里,它只是一个存放临时文件的目录, OverlayFS 中如果有文件修改,就会在中间过程中临时存放文件到这里。

我们最主要的内容是理解容器文件系统。为什么要有容器自己的文件系统?很重要的一点是减少相同镜像文件在同一个节点上的数据冗余,可以节省磁盘空间,也可以减 少镜像文件下载占用的网络资源。
作为容器文件系统,UnionFS 通过多个目录挂载的方式工作。OverlayFS 就是 UnionFS 的一种实现,是目前主流 Linux 发行版本中缺省使用的容器文件系统。
OverlayFS 也是把多个目录合并挂载,被挂载的目录分为两大类:lowerdir 和 upperdir。
lowerdir 允许有多个目录,在被挂载后,这些目录里的文件都是不会被修改或者删除的, 也就是只读的;upperdir 只有一个,不过这个目录是可读写的,挂载点目录中的所有文件 修改都会在 upperdir 中反映出来。
容器的镜像文件中各层正好作为 OverlayFS 的 lowerdir 的目录,然后加上一个空的 upperdir 一起挂载好后,就组成了容器的文件系统。
有时候我们会发现在容器中读写文件怎么变慢了???
OverlayFS 在 Linux 内核中还在不断的完善,在 kenel 5.4 中,Linux 为了完善 OverlayFS,增加了 OverlayFS 自己的 read/write 函数接口,从而不再直接调用 OverlayFS 后端文件系统(比如 XFS,Ext4)的 读写接口。但是它只实现了同步 I/O(sync I/O),并没有实现异步 I/O
即在 kenel 5.4 中对 异步 I/O 操作的是缺失的,
而在 fio 做文件系统性能测试的时候使用的是异步 I/O,这样才可以得到文件系统的性能最 大值。所以,在内核 5.4 上就无法对 OverlayFS 测出最高的性能指标了。
这也是我们在使用容器文件系统的时候需要注意的。

3.5 查看一个容器的挂载情况

联系管理员微信tutu19192010,注册账号

上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术