一、容器镜像
在Linux操作系统中分为内核空间和用户空间。在linux启动时首先被加载的是内核,当内核启动之后会挂载root文件系统(根文件系统),而在文件系统中会包含各类可执行程序文件、设备文件等。例如我们去执行一个ls命令,这也是相当于运行一次ls这个可执行文件,同理当我们将自己的应用程序放在操作系统中并且启动后,也是运行在用户空间的。
而容器镜像,就相当于是一个 root 文件系统。比如ubuntu:20.04镜像其实就是一个类似精简版本的操作系统,里面包含了整个的操作系统目录结构。
容器镜像就像是一个将操作系统、依赖库、应用程序,配置参数等,所有容器运行时所需组合成的一个集合。它包含了我们的应用程序所需的所有依赖。另外容器镜像是可以接收增量变更的,也就是说我们可以对镜像进行一些自定义的调整与修改,而这些变更是可以在原有镜像基础之上做增量,而不是每次改动就产生一个新副本。
容器镜像为了实现这种增量保存的机制,设计出了一个“分层”概念。也就是将镜像分为多个层(layer)进行组织,每当发生变更时,就创建一个新层来保存这些变更的部分。多个层叠加在一起就是一个完整的镜像,而这种层与层组织叠加是有顺序的,最先创建出的层(基础镜像)总是在底层,而最新变更则通常处于最上层。
如上图所示,容器镜像就如同上图一样,从最底层的rootfs层层叠加,将每一次变更的增量使用一个layer来保存然后叠加上去,最终形成了含有多个layer的镜像包。
这里的“层”是一个抽象的表示,其实当我们在操作系统中拉取一个镜像后,这个过程将包含两个步骤,那就是下载与解压。当拉取镜像并在操作系统解压后,是以文件形式存在的。而镜像的多个层则对应多个文件夹,这些文件通常被存储在Docker默认的数据目录中(/var/lib/docker)。
Docker在处理镜像时会调用存储引擎的支持,而这在不同的操作系统中则有所不同,在RHEL系,例如CentOS中可能使用overlay存储引擎,除此之外例如还有AUFS存储引擎等。那目前在Ubuntu上Docker默认是使用overlay2存储引擎的一个实现。当你使用docker pull命令拉取一个镜像后,则会在docker数据目录(/var/lib/docker/overlay2)中出现对应的文件夹,这些文件夹是以sha256形式来命名的,例如这里以ubuntu系统为例拉取一个nginx镜像,命令如下:
docker pull nginx:1.19.6
然后使用tree命令查看该目录结构
tree -L 2 /var/lib/docker/overlay2
如上图所示,我们可以看到在overlay2中出现了5个sha256命名的文件夹,那么由此可以推断该nginx镜像应该有5个layer构成。而在overlay2下的l文件夹下则是5个链接文件,可以看到这些链接文件的指向则是以sha256命名的layer文件夹下diff目录。那么在这个diff文件夹中其实是存储着该层的增量。
我们可以继续使用tree命令并且增加查看的目录层级,就可以看到在有的layer文件夹中的diff目录中包含了完整的操作系统目录,而有的则包含了含有nginx文件的目录,那么这表示着这个nginx镜像是基于一个操作系统镜像来构建的。如下图所示
由上我们得知,一个镜像会被分为多个层,而在操作系统中则以多个文件夹形式存在,每个层对应的文件中的diff目录保存着属于该层的增量变更。那么要想得到一个完整的镜像就必须将多个增量,也就是多个层组合起来,这在我们使用镜像去创建容器时,Docker帮我们完成了这个操作。组合多个层,也就意味着需要将我们前面提到的操作系统中多个文件夹所包含的内容放在一起,即要保证这些原有层不被改变又要将新的变更单独存储,那么Docker又是怎样实现的呢?
这就不得不提UnionFS了,也就是联合文件系统。那么这类文件系统有一个特点就是允许将多个目录以挂载的方式,挂载至同一个挂载点上,并且以增加叠加的方式进行组织。
在我们前面内容中提到Docker会在不同的操作系统上使用不同的存储引擎支持,那么在ubuntu上默认使用的overlay2存储引擎的支持。那么这个overlay2就是属于这类联合文件系统,它具备这种特性。
我们下面通过一个示例来解释这种特性
创建一个demo文件夹,然后在文件夹分别创建lower、upper、worker、merged目录
mkdir ~/demo/ && cd ~/demo
mkdir lower upper worker merged
在lower、upper中创建不同的目录及文件
touch lower/lo.txt && mkdir lower/lo
touch upper/up.txt && mkdir upper/up
最终提到目录结构如下
demo
├── lower
│ ├── lo
│ └── lo.txt
├── merged
├── upper
│ ├── up
│ └── up.txt
└── worker
其中lower与upper代表镜像的两个layer增量,而merged则代表一个联合挂载点。那么接下来执行以下命令来完成这个联合挂载操作
cd ~/demo/
mount -t overlay overlay -o upperdir=upper,lowerdir=lower,workdir=worker ./merged/
此时使用mount -l 命令可查看到这一挂载点的存在
mount -l |grep overlay
overlay on /home/ops/demo/merged type overlay (rw,relatime,lowerdir=lower,upperdir=upper,workdir=worker,xino=off)
此时再看目录结构中的变化,可以看到在merged目录中则包含了lower与upper两者的增量。
../demo
├── lower
│ ├── lo
│ └── lo.txt
├── merged
│ ├── lo
│ ├── lo.txt
│ ├── up
│ └── up.txt
├── upper
│ ├── up
│ └── up.txt
└── worker
└── work
而当我们使用一个镜像运行容器时,docker所完成操作也是与此类似的操作。容器引擎会将该镜像的多层layer使用联合挂载(UnionFS)方式挂载到一个路径下,并创建一个新的layer叠加在其最上方,用于保存当前容器的更改。最上层为可读写层,因此您可在当前容器做出变更,而当您使用docker commit 提交这个容器ID时,这就等同于将最上层layer进行一个固化操作,它将变成一个只读层。而在下一次基于该镜像创建容器时,将再次创建一个新layer作为可读写层。
其实可以将容器镜像的layer分为三类:只读层、初始化层、可读写层
只读层:当您拉取一个镜像在操作系统中时,这时该镜像所包含的layer全部为只读层,只读层是不可改变的;
初始化层:当您使用一个镜像创建容器时,这时会创建一个可读写层与初始化层,初始化层是用于存储一些初始配置,例如hosts文件resolv.conf文件等。
可读写层:当基于一个镜像创建一个容器里,将为该镜像创建一个新layer,该layer则用于保存当前容器的更改。
我们可以使用nginx镜像来运行一个容器,来更好的观察这种实际上变化。
docker run -d --name=nginx nginx:1.19.6
那么此时我们使用tree命令可查看/var/lib/docker/overlay2下的变化
如上图所示,此时多出来一个文件夹和以init结尾的文件夹,那么这就是为当前容器变更创建的一个可读写层,以及一个初始化层。
那么我们可以使用mount -l来检查onverlay挂载情况
mount -l |grep overlay
overlay on /var/lib/docker/overlay2/3ddd1a1a6c547d37a049b38319726d1661453b2079bc488ccb5a6a6baefabfe2/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/XQUHREY7FXWHZEXHYEJ4TSUCCJ:/var/lib/docker/overlay2/l/5QSCJWINHKIRXG7FGKRCSLENZF:/var/lib/dockeroverlay2/l/UCNDROVAJA2YKWI262MWDLM62D:/var/lib/docker/overlay2/l/I3NP2SHEZHZX6BMPZH7H5OHTYN:/var/lib/docker/overlay2/l/A22OYLNTFBMISPDIFS6OIDSP3J:/var/lib/docker/overlay2/l/53JLCAUYF4HNGB4T6RXV2UW7ZX,upperdir=/var/lib/docker/overlay2/3ddd1a1a6c547d37a049b38319726d1661453b2079bc488ccb5a6a6baefabfe2/diff,workdir=/var/lib/docker/overlay2/3ddd1a1a6c547d37a049b38319726d1661453b2079bc488ccb5a6a6baefabfe2/work,xino=off)
可以看到这里显示的与我们之前那个overlay的示例是相似的,只不过这里使用upper与lower字段都指定了多个目录,这是因为该镜像有多个layer。那么可以看到这里挂载时lower所指定的目录则是/var/lib/docker/overlay2/l文件夹中的链接文件,这实际上和指定layer文件夹实际路径是一样的,这里使用链接文件缩短了挂载时所设置的字符长度。
二、Docker Volume
1、Data Volume
bind mount | docker managed volume | |
---|---|---|
volume 位置 | 可任意指定 | /var/lib/docker/volumes/... |
有mount point 影响 | 隐藏并替换为 volume | 原有数据复制到 volume |
是否支持单个文件 | 支持 | 不支持,只能是目录 |
权限控制 | 可设置为只读,默认为读写权限 | 无控制,均为读写权限 |
移植性 | 移植性弱,与 host path 绑定 | 移植性强,无需指定 host 目录 |
1、Bind mount
使用nginx:latest镜像创建—个名为testweb的容器,并且将宿主机的/usr/share/nginx/html目录挂载到容器的/root/html目录上
# mkdir html
# cd html/
# echo "docker-test1" >index.html
# docker run -itd --name testweb -p80 --restart=always -v /root/html:/usr/share/nginx/html nginx:latest
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4f55734e7447 nginx:latest "/docker-entrypoint.…" 6 seconds ago Up 4 seconds 0.0.0.0:32770->80/tcp testweb
916df5b5b4c0 registry:2 "/entrypoint.sh /etc…" 7 days ago Up 7 days 0.0.0.0:5000->5000/tcp registry
# curl 127.0.0.1:32770
docker-test1
限制容器对挂载目录只读权限
# docker run -itd --name testweb1 -p80 --restart=always -v /root/html:/usr/share/nginx/html:ro nginx:latest
限制容器对挂载文件只读权限
# docker run -itd --name testweb2 -p80 --restart=always -v /root/html/index.html:/usr/share/nginx/html/index.html:ro nginx:latest
2、Docker Manager Volume
不需要指定 mount 源数据,-v参数可以专门提供数据卷给其他容器挂载使用。
# docker run -itd --name test1 -P -v /usr/share/nginx/html nginx:latest
查看容器配置信息
# docker inspect test1
[
"Mounts": [
{
"Type": "volume",
"Name": "c2c3d39174c2768de8c2af921853d2db4a258223eb30ecddcbfe9854785f4527",
"Source": "/var/lib/docker/volumes/c2c3d39174c2768de8c2af921853d2db4a258223eb30ecddcbfe9854785f4527/_data",
"Destination": "/usr/share/nginx/html",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
]
]
手动创建volume
# docker volume create web
web
# docker volume ls
DRIVER VOLUME NAME
local c2c3d39174c2768de8c2af921853d2db4a258223eb30ecddcbfe9854785f4527
local web
# docker run -itd -P --name web1 -v web:/usr/share/nginx nginx:latest
# docker volume inspect web
[
"Mounts": [
{
"Type": "volume",
"Name": "web",
"Source": "/var/lib/docker/volumes/web/_data",
"Destination": "/usr/share/nginx",
"Driver": "local",
"Mode": "z",
"RW": true,
"Propagation": ""
}
]
]
2、容器与容器的数据共享
volume container:给其他容器提供volume存储卷的容器。它可以提供bind mount,也可以提供docker manager volume。
创建一个vc_data容器
# docker create --name vc_data -v ~/html:/usr/share/nginx/html -v /other/useful/tools busybox
使用vc容器
# docker run -itd --name nginx1 -P --volumes-from vc_data nginx
3、容器的跨主机数据共享
# yum -y install nfs-utils
# mkdir /data
# vim /etc/exports
# systemctl start rpcbind
# systemctl start nfs
# systemctl enable nfs
Created symlink from /etc/systemd/system/multi-user.target.wants/nfs-server.service to /usr/lib/systemd/system/nfs-server.service.
# systemctl enable rpcbind
# echo "nginx-test" > /data/index.html
# showmount -e 192.168.1.30
Export list for 192.168.1.30:
/data *
# mkdir /htdocs
# mount -t nfs 192.168.1.30:/data /htdocs
# cat /htdocs/index.html
nginx-test
# docker run -itd --name web-1 -P -v /htdocs:/usr/local/apache2/htdocs httpd:latest
# curl 127.0.0.1:32778
nginx-test
# mkdir /htdocs
# mount -t nfs 192.168.1.30:/data /htdocs
# docker run -itd --name web-1 -P -v /htdocs:/usr/local/apache2/htdocs httpd:latest
# curl 127.0.0.1:32768
nginx-test
4、host
host卷也经常使用的一种数据卷类型,该类型卷将允许指定一个宿主机目录位置,然后将其挂载至容器内部。通过该方式挂载的卷将不会出现在/var/lib/docker/volumes目录中,也不能通过docker volume子命令进行管理。
使用容器运行时的宿主机文件系统目录,以绑定挂载的方式,挂载至容器内部
docker run -v <host path>:<container path> --name=nginx nginx:1.19.6
也可使用--mount选项,官方推荐使用mount选项,可实现与-v相同效果,但相较于-v选项,mount选项支持更多的挂载参数的设定,语义也更加明确。
示例
docker -v /tmp/host:/mnt/data --name=nginx nginx:1.19.6
5、tmpfs
该类型的卷使用宿主机部分内存容量,做为存储空间挂载至容器中,使用内存作为存储空间这使得该类型卷并不能持久保存数据,因此该类型的卷只适用于一些临时存储场景。
示例
docker run -d --name=nginx --tmpfs /app nginx:1.19.6
--mount语法
docker run -d --name=nginx --mount type=tmpfs,destatination=/app nginx:1.19.6
5、nfs
NFS卷允许直接访问由NFS Server提供的存储空间来作为容器的数据卷,挂载至容器内部进行使用。使用NFS卷首先需要拥有一个配置好的NFS服务,并且保证运行容器的宿主机同样能够访问该NFS服务。使用NFS卷并不需要额外的plugin支持,对于该类型卷Docker已经内置local volume实现支持。
如果是试验性的,您可以使用NFS镜像来部署NFS服务,如下所示拉取一个镜像
docker pull itsthenetwork/nfs-server-alpine
然后使用该镜像运行一个NFS容器,注意-e参数是用于指定容器内的变量设定,而SHARED_DIRECTORY变量的值应是需要对外提供访问的目录,其在容器中的绝对路径。
docker run -d --name nfs --privileged \
-p 2049:2049 \
-v /tmp/nfs:/nfs \
-e SHARED_DIRECTORY=/nfs \
itsthenetwork/nfs-server-alpine
在需要运行容器的主机中安装nfs软件包
yum install nfs -y
创建NFS卷,这里由于NFS服务的版本不同,在创建卷时需要提供的参数将有所不同,因此提供两个版本的示例以供参考。
NFS Server V3版本
docker volume create --driver local \
--opt type=nfs \
--opt o=addr=192.168.26.11,rw \
--opt device=:/ \
--name nfs-volume
NFS Server V4版本,不同之外在于需要显式的指定nfs版本
docker volume create --driver local \
--opt type=nfs \
--opt o=addr=192.168.26.11,rw,nfsvers=4 \
--opt device=:/ \
--name nfs-volume
::: warning
在以上命令中addr所指定的是运行NFS服务的主机IP,而device则用于指定该NFS服务中对外提供访问的目录,也就是在上一节中创建NFS容器SHARED_DIRECTORY变量的值。
:::
当您使用NFS卷时只需指定数据卷名称和容器中的挂载位置即可,如下
docker run -d --name=nginx -v nfs-volume:/mnt/nfs nginx:1.19.6
这里需要说明的是,对NFS挂载操作,是在使用时才发生的,也就是说在创建过程中并不会尝试实际挂载NFS卷。其实不止NFS,CIFS以及一些受plugin支持的数据卷,也是如此。
6、sshfs
该类型卷允许以SSH访问远程挂载主机上的具体目录,只要能够ssh连接至远程主机并且对远程主机中的目录有控制权限,那么就可以实现这种挂载,该挂载就像ssh连接主机一个简单。该类型卷需要卷驱动程序的支持,可以使用docker plugin来安装该插件
示例
在运行容器的宿主机中,执行以下命令安装sshfs插件
docker plugin install --grant-all-permissions vieux/sshfs
假设远程挂载的路径为(192.168.26.11:/home/ops/docker),那么创建数据卷命令如下
docker volume create --driver vieux/sshfs \
-o sshcmd=ops@192.168.26.11:/home/ops/docker \
-o password=123456 \
--name ssh-volume
如果您在实际操作中,请将以上命令中的ops以及远程路径替以及password的值换为实际值,在这里ops是远程连接的用户名,这与ssh连接方式很像。
三、故障排除
在docker迁移的过程中,当删除docker容器时会发现docker数据目录无法删除,会提示设备或资源忙,这是错误通常是由于存储驱动程序不稳定或某些进程占用了文件夹而导致的。在这里我使用lsof命令来查找占用文件夹的进程并杀死它们,然后再尝试删除文件夹。
显示所有链接数小于1的文件
# lsof +L1
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
docker-dr 11983 root cwd DIR 253,3 6 0 79059236 / (deleted)
docker-dr 11983 root rtd DIR 253,3 6 0 79059236 / (deleted)
docker-dr 11983 root txt REG 253,3 66183168 0 79058500 /bin/docker-driver (deleted)
进入docker数据目录中的overlay2下,尝试删除overlay2目录,会提示报错
# ls
overlay2
# rm -rf overlay2/
rm: cannot remove ‘overlay2/36b2b3f6a4bbd6b9fe37710199c8b385a3463d63eb374ab1017cdc945a13fa8a/merged’: Device or resource busy
将查找到进程号杀死,然后发现已经没有了docker进程存活
# kill -9 11983
# ps -ef | grep docker
root 50153 42183 0 15:26 pts/0 00:00:00 grep --color=auto docker
卸载目录后可以正常删除
# rm -rf overlay2/
rm: cannot remove ‘overlay2/36b2b3f6a4bbd6b9fe37710199c8b385a3463d63eb374ab1017cdc945a13fa8a/merged’: Device or resource busy
# umount overlay2/36b2b3f6a4bbd6b9fe37710199c8b385a3463d63eb374ab1017cdc945a13fa8a/merged
# rm -rf overlay2/
评论区