前言
上一节咱们着重分析了Docker容器,并提到Docker容器和镜像之间就是对像和类之间的关系,也就是说Docker容器是脱离不开镜像的,但是这个观点并不是百分之百正确。因此为了更深入的了解Docker镜像,本节呢咱们就来详细的介绍Docker镜像以及容器数据卷。相信看完之后,各位肯定会有所收获!
什么是镜像
虽然之前已经介绍过镜像,但是不够详细,而且都是浮于表面的,所以,我们还是先正式的介绍今天的 “主角” 镜像 。
Docker 镜像可以看作是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。正如我们上节讲解Docker容器提到的,我们可以把镜像当成一个模板,或者举个不太恰当的栗子,你可以把镜像想象成原版CD(内容不能被修改),使用CD播放器(Docker 引擎)之后就可以放出美妙动听的音乐,被复刻成MP3等流媒体格式,在任何网络播放器上都可以播放,此时这种流媒体格式音乐就好比一个容器,它是由镜像创建的,当然我们也可以将网上的流媒体格式的音乐刻在CD上,这种形式也被称为自定义镜像的创建。
谈到镜像,不得不了解UnionFS,因为它是Docker 镜像的基础。咱们先来看看介绍吧!
UnionFS(联合文件系统):Union文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。Union 文件系统是Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录,因此这种一层层叠加的方式跟俄罗斯套娃一样,里层的文件系统套的越多,那么这个镜像就会越庞大。
Docker镜像加载原理
刚刚咱们提到Docker的镜像实际上是由一层一层的文件系统组成,这种层级的文件系统称为UnionFS。而UnionFS又分为bootfs和rootfs ,咱们先看这两者的区别。
bootfs(boot file system) 主要包含bootloader启动加载器和kernel内核, bootloader主要是引导加载kernel, Linux刚启动时会加载bootfs文件系统,在Docker镜像的最底层是bootfs。这一层与我们典型的Linux/Unix系统是一样的,包含boot加载器和内核。当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs。
rootfs (root file system) 在bootfs之上。包含的就是典型 Linux 系统中的/dev、 /proc、/bin、/etc
等标准目录和文件。rootfs就是各种不同的操作系统发行版,比如Ubuntu,Centos等等。 但需要注意的是,不同的发行版的目录结构或者名称可能会存在差异,但是最基本的/dev、 /proc、/bin、/etc
等标准目录和文件是绝对不会缺席的!你可以把它当作是一种行业的标准。
了解了上面的bootfs和rootfs的基本概念之后,我们来思考一个问题:平时我们安装进虚拟机的CentOs都是好几个G,为什么Docker这里才202M??
其实最根本的原因是Docker对CentOs镜像精简化了,而对于一个精简的OS,rootfs可以很小,只需要包括最基本的命令、工具和程序库就可以了,因为底层直接用Host的kernel内核,也就是说bootfs是共享的,自己只需要提供 rootfs 就行了。由此可见对于不同的linux发行版,bootfs基本是一致的,而rootfs会有差别,但是标准化的东西是不会改变的!
分层的镜像
虽然我们上面提到了镜像分层的概念,并且在Docker系列博文的第一节,咱们在解释tomcat为何在Docker中有500多M之大时也提到了。不过咱们是按 “栗” 说话!
就以我们的pull为例吧,我这里重新在远程仓库中拉取一个redis,大家注意看,在下载的过程中redis的镜像好像是在一层一层的在下载,很明显,一共有6层之多,其实这每一层都代表着一个镜像,一般被依赖的镜像会先行下载。不知道大家留意第一个镜像没有?它的状态是Already Exists
?而且并没有去远程仓库下载,这意味这什么呢?
这其实和Docker的 “共享化” 机制有着很重要的关系,“共享化” 这个概念也不是第一次提了,之前在Maven的博文中我门也提到过这个概念,“共享化” 其实也就是去冗余,将一些重复的镜像共享,这个离不开Docker的设计理念,结合下面这张图,大家应该就能明白Docker处理镜像的原理了。
下面这张图也很好理解,和Maven的jar包管理很相似,当我们调用docker run
方法时Docker会默认使用pull拉取镜像,并去本地的镜像组中查找是否有该镜像,如果有的话就不会下载,并运行生成容器,如果没有则取远程仓库拉取,当远程仓库找不到该镜像时,会返回error信息,反之则会下载到本地的镜像组中,并运行生成容器。
由此,我们可以大胆的猜想,这个Already Exist
的镜像必定是之前通过依赖关系下载过的,并且极有可能就是Linux kernel
,因为几乎所有的镜像想要运行生成容器是必定离不开Linux内核的。
通过以上现象及分析过后,我们可以总结一下,Docker使用上面的这种分层结构最主要的原因就是共享资源
比如:有多个镜像都从相同的 base 镜像构建而来,那么宿主机只需在磁盘上保存一份 base镜像,
同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。
另外需要注意的是,Docker镜像都是只读的
当容器启动时,一个新的可写的镜像被加载到镜像层的顶部。
这一层通常被叫做容器层,容器层之下的都叫镜像层。
镜像的commit操作
刚才我们讲到容器可以被当作是一个特殊的可写的镜像,因此当镜像运行之后可以修改容器里面的内容,再提交成一个新的镜像,这就是我们之前提到的容器逆向成镜像的操作,其实结合之前我举得CD和音乐媒体数据流的栗子也是可以说的通的,比方说我们将CD里的媒体数据流加载到电脑之后,可以对媒体数据流进行剪裁,然后复刻到CD上,其实跟我们将要讲的东西差不多。
命令语法
docker commit -m='新的镜像的描述信息' -a='作者' 容器ID 要创建的目标镜像名:[标签名]
第一步:从Hub上拉一下tomcat镜像运行
docker run -it -d -p 8080:8080 tomcat
接着访问http://192.168.144.132:8080/docs/
,可以看出docs是可以访问的哦!
第二步:删除tomcat里面webapps里面的docs项目
依次执行命令docker exec -it 320734052f /bin/bash
和rm -rf docs/
删除tomcat里面webapps里面的docs项目
[root@localhost ~]# docker exec -it 320734052f /bin/bash
root@320734052f5f:/usr/local/tomcat# cd webapps/
root@320734052f5f:/usr/local/tomcat/webapps# ls -alh
total 8.0K
drwxr-xr-x. 7 root root 81 Aug 14 22:24 .
drwxr-sr-x. 1 root staff 42 Aug 22 00:26 ..
drwxr-xr-x. 3 root root 4.0K Aug 22 00:26 ROOT
drwxr-xr-x. 14 root root 4.0K Aug 22 00:26 docs
drwxr-xr-x. 6 root root 83 Aug 22 00:26 examples
drwxr-xr-x. 5 root root 87 Aug 22 00:26 host-manager
drwxr-xr-x. 5 root root 103 Aug 22 00:26 manager
root@320734052f5f:/usr/local/tomcat/webapps# rm -rf docs/
很显然,此时是访问不到http://192.168.144.132:8080/docs/
的
第三步:把当前运行的这个没有docs的容器生成一个新的镜像
紧接着咱们执行docker commit -a='marco' -m='delete docs' 320734052f5f marco/tomcat:1.0
将该容器生成一个新的镜像,使用docker images
查看,发现镜像生成成功了!
第四步:启动自己创建的镜像和之前的对比
咱们来测试运行一下,marco版的tomcat是否能跑起来!docker run -p 8080:8080 marco/tomcat:1.0
访问http://192.168.144.132:8080
,发现tomcat确实运行了
那么http://192.168.144.132:8080/docs
能运行么?
很显然,访问失败!
Docker容器数据卷
接下来咱们要介绍的内容就尤为重要啦,事关后期使用Docker如何实现项目的部署。那么先带大家初步认识一下Docker容器数据卷~
什么是容器数据卷
当我们在使用Docker容器的时候,会产生一系列的数据文件,当我们关闭Docker容器时这些数据文件是会随之消失的,但现在我有个需求,希望其中产生的部分内容持久化另作它用,而且希望容器于容器之间能够实现数据共享该怎么办?如果不通过docker commit
生成新的镜像,使得数据做为镜像的一部分保存下来,那么当容器删除后,数据自然也就没有了,有没有更好的解决办法呢?
Docker容器数据卷为我们解决了这个困扰!通俗来讲,Docker容器数据卷可以看成使我们生活中常用的U盘,它存在于一个或多个的容器中,由Docker挂载到容器,但不属于联合文件系统,Docker不会在容器被删除时删除其挂载的数据卷。
总的来说容器数据卷具有以下特性
1:可以在容器之间共享或重用数据
2:数据卷中的更改可以直接生效
3:数据卷中的更改不会包含在镜像的更新中
4:数据卷的生命周期一直持续到没有容器使用它为止
Docker容器产生的数据,如果不通过docker commit生成新的镜像,使得数据做为镜像的一部分保存下来,
那么当容器删除后,数据自然也就没有了。为了能保存数据在Docker中我们使用卷。
添加数据卷的方式
方式一:直接使用命令添加数据卷
docker run -it -v /宿主机目录:/容器内目录 镜像名 /bin/bash
通过以下命令可以查看容器卷是否挂载成功
docker inspect 容器ID
知道基本用法咱就来试试看吧!通过指令docker run -it -v /usr/local/myvolume:/usr/local/myvolume centos /bin/bash
创建一个目录卷,接下来我们先查看Docker容器(centos)内部的目录,发现确实多出来一个myvolume目录
主机上也对应的多出一个myvolume目录!
接下来我们使用docker inspect 容器ID
看看容器卷是否挂载成功。
Type是bind代表绑定成功,Source是宿主机的目录,Destination是容器内的目录,挂载到宿主机下,RW为true代表可读可写,Propagation为rprivate (表示原始挂载点或复制挂载点内的任何位置都不会在任何方向传播)。Mounts由一组键值对组成。
Key | Value |
---|---|
type | bind、volume、tmpfs,如不指定,默认是 volume |
source | Docker Host 上的一个文件或者目录 |
destination | 被挂载容器上的一个文件或者目录 |
rw | 表示read-wirte,true代表可读可写 |
propagation | rprivate、private、rshared、shared、rslave、slave |
接下来咱们在宿主机的myvolume
目录中创建一个test.txt
,添加文字update in host
[root@localhost ~]# touch /usr/local/myvolume/test.txt
[root@localhost ~]# vi /usr/local/myvolume/test.txt
[root@localhost ~]# cat /usr/local/myvolume/test.txt
update in host
按理说,容器内的容器卷的内容应该也会被改变。
#exec 在容器中打开新的终端 并且可以启动新的进程
#attch 直接进行容器终端,不会启动新的进程
[root@localhost ~]# docker attach 77463f965(容器ID)
[root@77463f965f46 /]# cat /usr/local/myvolume/test.txt
update in host
注意: 在以上的例子中,默认的只能在宿主机里面写数据。
解决办法: 在挂载目录后多加一个--privileged=true
参数即可。
docker run -it -v /usr/local/myvolume:/usr/local/myvolume --privileged=true centos /bin/bash
补充: 带权限的处理方式,使用此种方式之后,Mounts的挂载参数RW
会变成false
docker run -it -v /宿主机目录:/容器内目录:ro centos /bin/bash
方式二:使用Dockerfile添加
首先在宿主机的根目录下创建myvolume文件夹并进入,并在当前目录创建一个Dockerfile的文件,注意文件名一定要是Dockerfile
,因为Docker会自动查找名为Dockerfile
的文件,扫描里面的内容
[root@localhost local]# cd myvolume/
[root@localhost myvolume]# touch Dockerfile
[root@localhost myvolume]# ll
总用量 0
-rw-r--r--. 1 root root 0 8月 25 07:16 Dockerfile
接下来咱们编写Dockerfile
FROM centos
VOLUME ["/dataContainer1","/dataContainer2"]
CMD echo "finished,--------success1"
CMD /bin/bash
完成以上步骤之后,咱们build生成一个新的镜像
#注意后面有一个点哦,表示在当前路径下创建
docker build -f /myvolume/Dockerfile -t marco/centos .
创建完成之后使用docker image
查看镜像
紧接着使用以下指令启动容器
docker run -it --name='marcentos' marco/centos
查看容器里面有两个容器卷dataContainer1
,dataContainer2
但是现在有个问题… 宿主机中貌似没有与容器中这两个数据卷相对应的目录。所以说要想实现数据共享,首先我们得到找到和容器内部对接得宿主机目录是哪一个对吧?还记得之前查看容器内部细节的指令么?
docker inspect 8c22be3183e7
执行docker inspect
指令之后,找到Mounts挂载对象,之前我们说Source是主机里的数据卷存放的位置,Destination是容器里的数据卷存放的位置,因此我们很容易可以找到dataContainer1
和dataContainer2
在宿主机中与之对应的目录的位置。
"Mounts": [
{
"Type": "volume",
"Name": "d3273afd30e2ea7dbcaddf15afa081674002a16b231c4c31fc2916638958f37d",
"Source": "/var/lib/docker/volumes/d3273afd30e2ea7dbcaddf15afa081674002a16b231c4c31fc2916638958f37d/_data",
"Destination": "/dataContainer1",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
},
{
"Type": "volume",
"Name": "de3764ecd9252e13e24e24c6a9d9e650e10cf648d77dbc5d929a43ad333afd72",
"Source": "/var/lib/docker/volumes/de3764ecd9252e13e24e24c6a9d9e650e10cf648d77dbc5d929a43ad333afd72/_data",
"Destination": "/dataContainer2",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
]
咱们就以dataContainer1
对应的宿主机目录/var/lib/docker/volumes/d3273afd30e2ea7dbcaddf15afa081674002a16b231c4c31fc2916638958f37d/_data
来测试一下,看看是否能够实现数据联共享吧!
#首先切换到dataContainer1对应的宿主机目录下
[root@localhost lib]# cd /var/lib/docker/volumes/d3273afd30e2ea7dbcaddf15afa081674002a16b231c4c31fc2916638958f37d/_data
#在该目录中创建hello.txt
[root@localhost _data]# touch hello.txt
#切换到容器中
[root@localhost _data]# docker attach 8c22be3183e7
#查看dataContainer1中的是否有hello.txt文件
[root@8c22be3183e7 /]# ls -alh dataContainer1/
total 0
drwxr-xr-x. 2 root root 23 Aug 26 05:12 .
drwxr-xr-x. 1 root root 61 Aug 26 05:06 ..
-rw-r--r--. 1 root root 0 Aug 26 05:12 hello.txt
以上结果证实,通过Dockerfile也能生成容器数据卷,实现数据共享!
关于容器数据卷的小实验
接下来咱们做个小实验,带大家更深入的了解Dockerfile的底层机制。咱们还是以上面的marco/centos为镜像,启动一个容器,取名为dc1,dc1里面有dataContainer1
和dataContailer2
两个容器数据卷,在dataContailer1
里面添加dc1.txt
文件。
docker run -it --name="dc1" marco/centos
touch /dataContainer1/dc1.text
接着启动一个容器dc2 继承dc1,并在dataContailer1
里面添加dc2.txt
docker run -it --name="dc2" --volumes-from dc1 marco/centos
touch /dataContainer1/dc2.text
最后再启动一个容器dc3 继承dc2 在dataContailer1
里面添加dc3.txt
docker run -it --name="dc3" --volumes-from dc2 marco/centos
touch /dataContainer1/dc3.text
结果发现在dc3的dataContainer1
文件夹中可以看到dc1.txt
dc2.txt
dc3.txt
[root@09d28a2b9a03 /]# ls dataContainer1/
dc1.text dc2.txt dc3.txt
同样的dc1和dc2里面也可以看到dc1.txt
dc2.txt
dc3.txt
,说明这三个容器之间通过容器数据卷dataContainer1
已经实现互通了!
那么这里有个问题,大家可以试着思考看看,当我删除dc3容器之后,dc2和dc3里面可以看到哪些文件?
…
答案是dc1和dc2中可以看到dc1.text
dc2.txt
dc3.txt
… 有的朋友可能会有疑问了,我dc3已经删除了,并且dc3此前已经和两外两个容器实现共通了,那么dc3数据都丢失了,为什么dc1和dc2中还是可以看到这些文件呢?就算能看到,至少dc3.txt
是看不到的吧… 毕竟是在dc3中创建的文件…
[root@localhost myvolume]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7556ebcfc182 marco/centos "/bin/sh -c /bin/bash" 17 minutes ago Up 17 minutes dc3
09d28a2b9a03 marco/centos "/bin/sh -c /bin/bash" 18 minutes ago Up 18 minutes dc2
95fa663efaf9 marco/centos "/bin/sh -c /bin/bash" 21 minutes ago Up 21 minutes dc1
[root@localhost myvolume]# docker rm -f 7556ebcfc182
7556ebcfc182
[root@localhost myvolume]# docker attach 09d28a2b9a03
[root@09d28a2b9a03 /]# ls /dataContainer1/
dc1.text dc2.txt dc3.txt
[root@09d28a2b9a03 /]# docker attach 95fa663efaf9
bash: docker: command not found
[root@09d28a2b9a03 /]# [root@localhost myvolume]#
[root@localhost myvolume]# docker attach 95fa663efaf9
[root@95fa663efaf9 /]# ls /dataContainer1/
dc1.text dc2.txt dc3.txt
其实有以上的想法很正常,但是事实却拜在我们面前,怎么去解释呢?
其实原因很简单,大家还记得docker inspect 容器ID
指令查看容器内部细节的指令吧,咱们就查看Mounts挂载信息,大家应该也不是第一次看到挂载信息了,很明显,dc1和dc2所挂载的宿主机的目录都是/var/lib/docker/volumes/09a628fffacebd0ab89f4e11a9f33879f3efd2af28cbb81e82b058935b3e3922/_data
这说明,我们每次往容器的容器数据卷中添加或者删除文件时,宿主机的挂载目录中的数据也会随之变化,但是容器消失并不影响挂载目录中的数据,因此其他的公用同一个挂载目录的容器数据卷中的文件当然也不会改变!
docker inspect 95fa663efaf9 ##dc1的挂载信息
"Mounts": [
{
"Type": "volume",
"Name": "09a628fffacebd0ab89f4e11a9f33879f3efd2af28cbb81e82b058935b3e3922",
"Source": "/var/lib/docker/volumes/09a628fffacebd0ab89f4e11a9f33879f3efd2af28cbb81e82b058935b3e3922/_data",
"Destination": "/dataContainer1",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
},
]
docker inspect 09d28a2b9a03 ##dc2的挂载信息
"Mounts": [
{
"Name": "09a628fffacebd0ab89f4e11a9f33879f3efd2af28cbb81e82b058935b3e3922",
"Source": "/var/lib/docker/volumes/09a628fffacebd0ab89f4e11a9f33879f3efd2af28cbb81e82b058935b3e3922/_data",
"Destination": "/dataContainer1",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
]
并不是说容器消失,数据卷就不会消失,但只有一种情况,就是所有的共用同一个挂载目录的容器全部被删除,那么此时挂载目录的数据就会被清空
[root@localhost myvolume]# docker rm -f 09d28a2b9a03 #删除dc2容器
09d28a2b9a03
[root@localhost myvolume]# docker rm -f 95fa663efaf9 #删除dc1容器
95fa663efaf9
[root@localhost myvolume]# cd /var/lib/docker/volumes/4766e18a1905367019fd9dd5b2627258da4b5e463418d568aa4244f89c25dc1c/_data
[root@localhost _data]# ll
总用量 0 #挂载目录中已经没有文件了
这也说明数据卷容器的生命周期会一直持续到没有容器使用它为止!