Docker系列:联合文件系统OverlayFS
在讲解联合文件系统OverlayFS之前,我们先来了解一下容器技术中的两个重要概念:镜像、容器和层。这些概念对理解OverlayFS的工作原理非常重要。
一、镜像、容器和层
在容器技术的世界中,镜像是构建容器的基础。它类似于一个模板或者蓝图,包含了我们想要运行的应用程序及其所需的所有依赖。镜像是只读的,一旦创建就不可更改。
容器则是镜像的一个实例化对象。我们可以通过镜像创建多个容器,每个容器都可以独立运行,并且不会相互影响。容器中运行的应用程序可以读写文件,但对于容器外的文件系统是只读的。
容器和镜像之间存在一个重要的概念,那就是层。每个镜像都可以由多个层堆叠而成,每个层都只记录了与前一层的差异。这种分层的结构使得镜像的构建和传输更加高效。
联合挂载是一种文件系统,它可以产生将多个目录的内容合并到一个目录中的错觉,而无需修改其原始(物理)源。这可能很有用,因为我们可能将相关的文件集存储在不同的位置或媒体中,但我们希望在单个合并视图中显示它们。例如,/home
远程 NFS 服务器上的一堆用户目录全部合并为一个目录,或者将分割的 ISO 映像合并为一个完整的目录。
联合挂载或联合文件系统是;然而,不是文件系统类型,而是一个具有多种实现的概念。其中一些更快,一些更简单,具有不同的目标或具有不同的成熟度。因此,在我们开始深入研究细节之前,让我们快速概述一下可用的一些更流行的实现:
二、rootFs
在容器技术中,每个容器需要一个根文件系统(root filesystem,简称rootfs)。rootfs提供了容器在其中运行的操作系统和应用所需的所有文件和目录。
与传统的虚拟机不同,容器的rootfs是与主机共享的。这种共享的特性使得容器的启动和运行速度非常快,同时也节省了主机上的存储空间。
三、联合文件系统OverlayFS
OverlayFS 在一台 Linux 主机上采用两个目录,将一个目录放在另一个目录之上,并提供一个统一的视图。这些目录通常称为层,用于对它们进行分层的技术称为 联合挂载。OverlayFS 术语“lowerdir”表示底层,“upperdir”表示顶层。统一视图通过其自己的名为“merged”的目录公开。
下图显示了 Docker 镜像和 Docker 容器是如何分层的。镜像层是“lowerdir”,容器层是“upperdir”。统一视图通过名为“merged”的目录公开,该目录实际上是容器挂载点。该图显示了 Docker 如何构造到 OverlayFS 构造的映射。
请注意图像层和容器层如何包含相同的文件。发生这种情况时,容器层(“upperdir”)中的文件占主导地位,并且掩盖了映像层(“lowerdir”)中相同文件的存在。容器挂载(“合并”)呈现统一视图。 。
四、OverlayFS是如何工作的
我们首先解释它在一般(非容器)情况下如何工作 - 让我们想象一下,我们希望将两个目录(upper
和lower
)联合挂载到同一个挂载点上,并拥有它们的联合视图:
.
├── upper
│ ├── code.py # Content: `print("Hello Overlay!")`
│ └── script.py
└── lower
├── code.py # Content: `print("This is some code...")`
└── config.yaml
在联合挂载术语中,这些目录称为分支。每个分支都被分配了优先级。此优先级用于确定在多个源分支中存在同名文件的情况下哪个文件将显示在合并视图中。查看上面的文件和目录 - 很明显,如果我们尝试覆盖它们,我们将创建这种冲突(code.py
文件)。
~ $ mount -t overlay \
-o lowerdir=./lower,\
upperdir=./upper,\
workdir=./workdir \
overlay /mnt/merged
~ $ ls /mnt/merged
code.py config.yaml script.py
~ $ cat /mnt/merged/code.py
print("Hello Overlay!")
结果真的很像git 的merge
在上面的示例中,我们使用mount
带有 type 的命令overlay
将lower
目录(只读;较低优先级)和upper
目录(读写;较高优先级)组合到/mnt/merged
. 我们还包含了一个选项,该选项可作为在原子操作中移动之前workdir=./workdir
准备合并视图的位置。lowerdir``upperdir``/mnt/merged
另外查看cat
上面命令的输出,我们可以看到目录中文件的内容确实upper
在合并视图中优先。
那么,现在我们知道如何合并 2 个目录以及如果存在冲突会发生什么,但是如果我们尝试从合并视图修改某些文件会发生什么?这就是写时复制 (CoW) 发挥作用的地方。那么,它到底是什么?CoW 是一种优化技术,如果两个调用者请求相同的资源,您可以为他们提供指向相同资源的指针,而无需复制它。仅当其中一个调用者尝试写入其 “副本” 时,才需要进行复制- 因此术语 “copy on (first attempts to) write” 。
在联合挂载的情况下,这意味着当我们尝试修改共享文件(或只读文件)时,它首先被复制到顶部可写分支(),该分支的优先级高于只读较低upperdir
分支(lowerdir
)。然后,当它位于可写分支时,可以安全地修改它,并且它的新内容将在合并视图中可见,因为顶层具有更高的优先级。
我们可能想要执行的最后一个操作是删除文件。为了执行 “删除” ,在可写分支中创建一个whiteout文件来清除我们想要删除的文件。 这意味着该文件实际上并未被删除,而是隐藏在合并视图中。
五、实战演练
在执行此操作之前,我们首先需要清理工作区并获取要使用的图像:
~ $ docker image prune -af
...
Total reclaimed space: ...MB
~ $ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
a076a628af6f: Pull complete
0732ab25fa22: Pull complete
d7f36f6fe38f: Pull complete
f72584a26f32: Pull complete
7125e4df9063: Pull complete
Digest: sha256:10b8cc432d56da8b61b070f4c7d2543a9ed17c2b23010b43af434fd40e2ca4aa
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
我们有一个图像 ( nginx
) 可以使用,所以接下来,让我们检查它的图层。我们可以通过docker inspect
在图像上运行并检查字段GraphDriver
或浏览/var/lib/docker/overlay2
存储所有图像层的目录来检查图像层。那么,让我们同时执行这两个操作,看看里面有什么:
~ $ cd /var/lib/docker/overlay2
~ $ ls -l
total 0
drwx------. 4 root root 55 Feb 6 19:19 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd
drwx------. 3 root root 47 Feb 6 19:19 410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46
drwx------. 4 root root 72 Feb 6 19:19 685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e
brw-------. 1 root root 253, 0 Jan 31 18:15 backingFsBlockDev
drwx------. 4 root root 72 Feb 6 19:19 d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e
drwx------. 4 root root 72 Feb 6 19:19 fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505
drwx------. 2 root root 176 Feb 6 19:19 l
~ $ tree 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/
3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/
├── diff
│ └── docker-entrypoint.d
│ └── 20-envsubst-on-templates.sh
├── link
├── lower
└── work
~ $ docker inspect nginx | jq .[0].GraphDriver.Data
{
"LowerDir": "/var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff:/var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff:/var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff:/var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff",
"MergedDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/merged",
"UpperDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff",
"WorkDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/work"
}
看看上面的输出,它看起来与我们在mount
命令中看到的非常相似,对吗?进一步来说:
LowerDir
: 是只读图像层的目录,用冒号分隔MergedDir
:图像和容器中所有层的合并视图UpperDir
:写入更改的读写层WorkDir
:Linux OverlayFS 用于准备合并视图的工作目录
接下来,让我们更进一步,运行一个容器并检查它的层:
~ $ docker run -d --name container nginx
~ $ docker inspect container | jq .[0].GraphDriver.Data
{
"LowerDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4-init/diff:/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff:/var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff:/var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff:/var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff:/var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff",
"MergedDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/merged",
"UpperDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff",
"WorkDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/work"
}
~ $ tree -l 3 /var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff # The UpperDir
/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff
├── etc
│ └── nginx
│ └── conf.d
│ └── default.conf
├── run
│ └── nginx.pid
└── var
└── cache
└── nginx
├── client_temp
├── fastcgi_temp
├── proxy_temp
├── scgi_temp
└── uwsgi_temp
docker inspect nginx
上面的输出显示,之前在MergedDir
、UpperDir
和的输出中列出的相同目录WorkDir
(带有 id 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd
)现在是容器的一部分LowerDir
。这里LowerDir
是由所有nginx
相互堆叠的图像层组成的。它们之上是可写层UpperDir
,其中包含/etc
、/run
和/var
。此外,如果我们列出上面的内容,您将看到容器可用的整个文件系统,包括来自和 的MergedDir
所有内容。UpperDir``LowerDir
六、如何清理/var/lib/docker/overlay2
每次创建一个容器时,都会有一些文件和目录被创建,例如:
/var/lib/docker/containers/ID目录,如果容器使用了默认的日志模式,他的所有日志都会以JSON形式保存到此目录下。 /var/lib/docker/overlay2 目录下含有容器的读写层,如果容器使用自己的文件系统保存了数据,那么就会写到此目录下。 进入到/var/lib/docker/containers 目录下 通过 du -h --max-depth=1 查找大文件占用,发现这些文件中占用空间最大的是***-json.log**文件。而此文件的内容为docker生成的日志文件。
下面是一个清理的脚本
#!/bin/sh
echo "======== start clean docker containers logs ========"
logs=$(find /var/lib/docker/containers/ -name *-json.log)
for log in $logs
do
echo "clean logs : $log"
cat /dev/null > $log
done
echo "======== end clean docker containers logs ========"