Dokcer为容器提供了两种存放数据的资源:storage driver(管理镜像层和容器层) 和 Data Volume。
1.1 storage driver 1. 前言问题:假设一个宿主机里面同时启动了4个Nginx容器,一个nginx运行时完整环境有100MB,那么4个Nginx容器会占用多少的磁盘空间呢?
4*100?如果是这样计算的话,那就说明这个四个Nginx容器都占用了独立的空间,每启动了一个Nginx容器都会启用一个小型的Linux系统,这时如果同样的一个软件,我们启动成百上千个,那么每一个都占用独立的空间,这样算下来就会占用很大的磁盘空间。
实际上Docker在底层考虑了这个问题(起了很多个容器他的空间该怎么占用),这就涉及到了Docker的存储原理。
容器和镜像的关系
容器由最上面一个可写的容器层和多个只读的镜像层组成,添加新数据或者修改现有数据的所有写入容器都存储在容器可写层中。当容器被删除时,可写层也被删除,底层的image保持不变。
因为每个容器都有自己的可写容器层,所有的变化都存储在容器层中,所以多个容器可以共享同一个底层镜像的访问,同时又能达到数据隔离的效果。(先知道这么一个概念,后文会有详解)
总结:镜像就是固化的文件,容器是基于镜像层在加一个读写层,所有的内容改变都在容器里面
那么这些文件Docker是怎么来管理的呢?Docker在底层使用自己的存储驱动(storage driver)程序来管理镜像层和可写容器层的数据。它支持多种storage driver,有AUFS、Device Mapper、Btrfs、OverlayFS(现在官方推荐使用overlay2)、VFS和ZFS。他们有各自的特性,但所有的驱动程序都采用的是写时复制技术。(后文会有详解)
2. overlay2 工作原理Docker安装的时候会根据当前系统的配置选择默认的driver
我这里使用的存储驱动是overlay2,底层文件系统是xfs,各层数据是存放在/var/lib/docker/overlay2下面的。
-
overlay2和早期使用的AUFS类似,不过它比AUFS性能更好,详细可见官网。
从kernel 3.18进入主流Linux内核。设计简单,速度快,比AUFS和Device mapper速度快。在某些情况下,也比Btrfs速度快。是Docker存储方式选择的未来。因为OverlayFS只有两层,不是多层,所以OverlayFS “copy-up”操作快于AUFS。以此可以减少操作延时。
-
overlay2将所有的目录称之为层,它是镜像和容器分层的基础。
-
overlay2将目录下一层叫做LowerDir,上一层叫做UpperDir,将这两层联合挂载后的结果叫做MergedDir
(1) 镜像怎么存储上面这个图展示了Docker结构和OverlayFS结构的映射关系,镜像层对应着lowerdir,容器层对应着upperdir。我们的容器可写层、镜像层都一起被挂载到merged目录下。
Docker镜像是分层存储的,每一层包含了一些文件变动,靠上的层覆盖靠下层的同名文件,最后可以通过overlay2这种层叠文件系统把所有层的文件挂载到一个目录下。
(2)容器怎么存储
首先拉取一个Nginx镜像,然后通过docker image inspect nginx命令查看Nginx镜像的详情,每个镜像都会有一个Data信息,这个信息指示了镜像是怎么存的。
"LowerDir": /var/lib/docker/overlay2/081cbcee5bfaea8c1587c77a566166d637a17302e2d066624c40feb96c151141/diff /var/lib/docker/overlay2/c1a9a9924e2c8e7c5e8a3d6a8c68c820c433f358541def64a0c04b1b91087a19/diff /var/lib/docker/overlay2/5ecf70b2d77ca7d707540e841b96e5801ace7b1f5d2c0e8e5e1/diff /var/lib/docker/overlay2/1771c0e02ea8ef74ab2c870841108272cf5f3fab59ea6104532c974ff8cf1736/diff /var/lib/docker/overlay2/ebe32d1f496e88cf2ad3eedf2a56770266c6655ed53d1f8e982a9039f8019f2d/diff "MergedDir": /var/lib/docker/overlay2/b9a9442d04da0a31d118d20df604ba4175c779dfadea9dc8fb73e44e6a62997d/merged "UpperDir": /var/lib/docker/overlay2/b9a9442d04da0a31d118d20df604ba4175c779dfadea9dc8fb73e44e6a62997d/diff "WorkDir": /var/lib/docker/overlay2/b9a9442d04da0a31d118d20df604ba4175c779dfadea9dc8fb73e44e6a62997d/work其中 MergedDir 代表当前镜像层在 overlay2 存储下的目录,LowerDir 代表当前镜像的父层关系,使用冒号分隔,冒号最后代表该镜像的最底层。
接着进入到overlay2目录下,可以看到有六个目录,这就说明这个Nginx镜像被分为了6层。
接着我们进入到任意一个镜像层看看里面的结构
镜像层的link文件内容为该镜像层的短 ID,diff文件夹为该镜像层的改动内容,lower文件内容为该层的所有父层镜像的短 ID。
至于最下面的l目录里,它存放的是一堆软连接,根据这个短ID可以软连接到了对应的镜像层的diff文件夹下
这时候我们再回过头来看看上面LowerDir中冒号最后的镜像层,也就是我们Nginx镜像父层的最底层是什么样的
可以看到它里面没有了lower文件,这是因为他就是最底层了,等于是根镜像了。同时,diff文件夹下的文件,这不就是我们熟悉的Linux文件目录结构么。这时我们也就能想到了,这个Nginx镜像的Dockerfile文件,肯定是FROM的一个有Linux操作系统的镜像。然后要在这个Linux系统上装Nginx,会逐层修改系统,Dockerfile的每一个命令都可能引起了系统的变化,它的每一个变化都会记录一层diff文件。
启动一个Nginx容器,以便观察Docker创建的容器可写层,并查看它的配置信息
上面的LowerDir就是容器所依赖的镜像层目录
接着我们再来查看一下overlay2目录,可以发现下面增加了容器层相关的目录
这个带有init后缀的层是容器启动的一个临时层,从字面意思看它大概就是容器的初始化层。docker commit提交为镜像时是不会提交init层的
进入容器层目录,看看里面的结构
link和lower文件与镜像层的功能一致,link文件内容为该容器层的短 ID,lower 文件为该层的所有父层镜像的短 ID 。diff目录为容器的读写层,容器内修改的文件都会在 diff中出现,merged 目录为分层文件联合挂载后的结果,也是容器内的工作目录。
当我们在容器中创建一个文件的时候,在容器层的diff目录下也就可以看到这个文件
这时我们在看看merged目录,可以看到他将lowerdir和upperdir的文件合并在了一起
这些目录完成了镜像分层的组织,docker底层的storage driver完成了以上的目录组织结构
总体来说,overlay2 是这样储存文件的:overlay2将镜像层和容器层都放在单独的目录,并且有唯一 ID,每一层仅存储发生变化的文件,最终使用联合挂载技术将容器层和镜像层的所有文件统一挂载到容器中,使得容器中看到完整的系统文件。
接下来我们来验证一下文章最开始提到的问题
我们可以在根据Nginx镜像多启动几个容器:
接着进入到这些容器的LowerDir下面容器所依赖的最底层镜像层目录,通过ls -i命令查看所有文件夹的唯一inode(inode是一个文件的唯一标识)
我们可以看到他们的inode是一样的,也就说明了这三个容器共用的同一个镜像文件。
因此,就算Nginx镜像有100MB,启动了100个,它也不会占用10G的磁盘空间。一个容器其实就占用了几个k的大小(容器可写层的大小)
到这里大家可能会跟我有同样的一个问题,那多个容器共用了一个镜像的文件系统,如果一个容器对文件系统进行了修改,会不会影响到镜像呢?导致其他的容器也被改变?那就请看下面的解释。
3. 容器与镜像的写时复制技术前面我们说到了,一个镜像的多个容器用到的文件系统就是镜像的文件系统
为了保证容器的修改不会互相影响,Docker采用了写时复制技术。
Copy-on-Write特性:
当容器启动时,一个新的可写层被加载到镜像的顶部。这一层被称之为“容器层”,容器层之下的都叫做“镜像层”。
所有对容器的改动,无论添加、删除,还是修改文件都只会发生在容器层中。只有容器层是可以写的,容器层下面的所有镜像层都是只读的。
我们在容器中进行操作时:
- 添加文件。在容器中创建文件时,新文件被添加到容器层中。
- 读取文件。在容器中读取某个文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。
- **修改文件。**在容器中修改已存在的文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改。
- 删除文件。在容器中删除文件时,Docker也是从上往下依次在各镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。
只有当需要修改时才复制一份数据,这种特性被称作Copy-on-Write。可见,容器层保存的镜像变化的部分,不会对镜像本身进行任何修改。
写时复制不仅节省空间,而且还减少了容器启动时间。当你创建一个容器(或者来自同一个镜像的多个容器)时,Docker 只需要创建可写容器层即可。
验证写时复制技术的过程
-
我们进入到镜像中,找到Nginx目录,然后查看它的配置文件nginx.conf的文件唯一标识inode
-
这时我们进入到容器内部去查看这个配置文件nginx.conf。我们知道,当我们在容器中只读这个nginx.conf文件时他是不会将这个文件复制到容器的可写层的。所以,这里的配置文件nginx.conf的inode和我们在镜像中直接看到的是一样的。说明这时他还是从镜像层去读取的这个文件。
-
但如果要是改了这个配置文件的内容,它就会复制了一份新的文件到上层路径,再来进行的修改,用的不再是镜像的那个配置文件了。
-
这时我们进入到容器的可写层,可以看到它的inode发生了变化
这个文件也是我们在容器中修改的文件
再去看看镜像层的nginx文件,是没有发生变化的
-
总结:当容器需要修改文件时,他会采用写时复制技术,将镜像的文件复制到自己容器的上层路径下,然后修改。如果下次要用这个文件,如果在容器层有就用容器层的,如果没有就看底层目录镜像层的。
容器无论怎么修改,底层镜像的内容永远不会变,一旦容器移除了,容器修改的所有配置全部丢失,并不会映射到镜像里面,所以就需要docker commit来提交容器变化为一个新的镜像。
1.2 容器如何挂载Volume对于某些容器,直接将数据放在由storage driver维护的层中是很好的选择,比如那些无状态应用。无状态意味着容器没有需要持久化的数据,随时可以从镜像直接创建。但对于另一类应用就不太合适了,他们有持久化数据的需求,容器启动时需要加载已有的数据,容器销毁时希望保留产生的新数据。(这时就要用到Docker的Volume技术了)
如果用docker启动一个mysql,默认什么都不做,那么会导致什么样的问题?
如果用docker启动一个mysql,那mysql中存的所有的数据都在mysql的临时层,这里的临时文件又没有给linux文件系统中放,所以如果这个mysql被干掉了,那么这个mysql中的数据就都丢失了。
那么怎么解决这个问题呢?
- 文件挂载
- docker commit提交改变(会同时提交mysql保存的所有数据),但是如果mysql的数据有100个G,那就会导致这个镜像有100G,肯定是不合理的
每一个容器里面的内容支持三种挂载方式:
-
Volumes(卷):存储在主机文件系统的一部分中,该文件系统(Docker area)由Docker管理(在Linux上是“ / var / lib / docker / volumes /”)。 非Docker进程不应修改文件系统的这一部分。 卷是在Docker中持久存储数据的最佳方法。
docker自动在外部创建文件夹挂载容器内部指定的文件夹内容【Dockerfile文件中 VOLUME指令的作用】
-
Bind mounts(绑定挂载):可以在任何地方存储在主机系统上。 它们甚至可能是重要的系统文件或目录。 Docker主机或Docker容器上的非Docker进程可以随时对其进行修改。
自己在外部创建文件夹,手动挂载到liunx的宿主机上
-
tmpfs mounts(临时挂载):仅存储在主机系统的内存中,并且永远不会写入主机系统的文件系统
可以把数据挂载到内存中(几乎不用)
-v 宿主机的绝对路径 : Docker容器内部的绝对路径(叫挂载,有空挂载问题)
eg:docker run -d -P --name=mynginx01 -v /root/html:/usr/share/nginx/html nginx
接着访问ngxin,发现访问的页面展示如下:
如果不挂载docker run -d -P --name=mynginx02 nginx,访问nginx的默认页会是怎样的呢?如下:
上面的这个现象就是-v挂载方式的一个坑!!!——空挂载(https://docs.docker.com/storage/ 最后一段话)
- 我们把宿主机上的/root/html目录挂载到了容器的/usr/share/nginx/html,挂载的原理是宿主机路径和容器路径下的文件是同步相同的,但是我们linux主机上的html目录是个空目录,导致映射到nginx中也是一个空目录,所以就导致了空映射。
-v 不以/开头的路径 : Docker容器内部绝对路径(叫绑定,Docker会自动管理,不加 / 后Docker不会把他当作目录,而是当作卷)
eg:docker run -d -P --name=mynginxtest3 -v html:/usr/share/nginx/html nginx
采用这种方式挂载,访问ngxin默认页,发现可以成功访问默认欢迎页
总结:(nginx测试html挂载几种不同情况)
卷又分为:具名卷和匿名卷
- 不挂载,效果:访问默认欢迎页
- -v /root/html:/usr/share/nginx/html,效果:访问forbidden
- -v html:/usr/share/nginx/html,效果:访问默认欢迎页
为啥会有不同效果呢?
-v html:/usr/share/nginx/html,这种方式,docker inspect容器的时候可以看到
"Mounts": [ { "Type": "volume", // 表示这是个卷 "Name": "html", // 名字是命令中指定的html "Source": "/var/lib/docker/volumes/html/_data", // 宿主机的目录,进去后发现nginx容器中的两个文件都在(50x.html、index.html) "Destination": "/usr/share/nginx/html", // 容器内部的目录 "Driver": "local", "Mode": "z", "RW": true, // 读写模式 "Propagation": "" } ]因此将这种方式叫做:绑定。也就是通过Docker自动管理的方式,这种不加 / 后,Docker不会把他当作目录,而是当作卷。
可以通过docker volume命令对卷目录进行管理和操作。 (卷目录的根路径为:/var/lib/docker/volumes)
因此-v html:/usr/share/nginx/html命令其实做了两件事
首先在docker底层创建一个指定名字(eg:html)的卷(具名卷)
把这个卷和容器的内部目录绑定
容器启动以后,容器目录里面的内容就会在卷里存着(通过ls -i发现,他们存的是同一个文件)
在宿主机上查看文件的inode
在容器内部查看文件的inode
- -v html:/usr/share/nginx/html:这种方式叫做具名卷,与之对应的就是匿名卷
- -v /usr/share/nginx/html:这钟卷的名字会由docker来指定一个uuid的名字
Docker官方建议:
- 如果是自己开发测试:用-v绝对路径的方式
- 如果是生产环境,建议用卷
- 除非特殊需要挂载主机路径的则操作绝对路径挂载
1. Docker提供的几种原生网络Docker安装时会自动在host上创建三个网络
-
none网络
也就是没有网络,挂在这个网络下的容器除了lo,没有其他任何网卡。(用处不大)
使用场景:
- 对安全性要求高且不需要联网的应用可以用,比如某个容器的唯一途径是生成随机密码,就可以放到none网络中避免密码被窃取
-
host网络
连接到host网络的容器共享宿主机的网络容器,容器的网络配置和宿主机完全一样。
使用场景:
- 直接使用宿主机的网络的最大好处就是性能,如果容器对网络传输效率有较高的要求,则可以使用host网络。但是它带来的缺点就是要考虑端口冲突问题。宿主机上已经使用的端口就不能在用了,同时如果有多个容器都使用的host网络,则他们的端口也是不能重复的。
-
bridge网络(容器启动默认选择的网络)
通过下面这个网络拓扑结构图,我们了解一下bridge网络的原理
这是我们的宿主机,Docker安装会创建一个名为docker0的Linux bridge。如果不指定—network的话,那所有创建的容器都默认会挂到docker0上。
-
没安装docker时虚拟机上的网络配置
-
安装docker后
当我们创建了一个Tomcat容器,然后通过brctl show命令可以看到它下面挂了一个网卡veth1da2f3f(没创建的时候是不会挂任何网卡的)
然后我们进入到容器内部看看这个容器的网络配置,发现有一个网卡eth0@if13
那为什么不是同一个虚拟网卡呢?其实这个容器内部的网卡eth0@if13和容器外部的网卡veth1da2f3f是一对,他们两个之间建立连接。这里的连接其实就是网卡的挂载,也就是说给veth1da2f3f的请求,他会转交给eth0@if13,我们把他俩称为veth pair。
这个veth pair是一种成对出现的特殊的网络设备,可以把他想象成由一根虚拟网线连接起来的一对网卡,网卡的一头(eth0@if13)在容器中,另一头(veth1da2f3f)挂载网桥docker0上,它的效果就是将eth0@if13挂在了docker0上。
我们还可以看到eth0@if6已经配置了IP172.17.0.2,那这个网段是怎么来的呢?
因为我们创建容器时默认使用的是bridge网络(启动容器时没有指定—network就默认用的是bridge网络),我们来通过docker network inspect bridge命令看看这个网络的具体信息
可以看到这个网络配置的子网就是172.17.0.0/16,并且网关是170.17.0.1,这个网关就在对应的docker0网桥上。
-
-
user-defined网络
除了none、bridge、host这三个自动创建的网络,用户也可以根据业务需要创建用户自定义网络。
Docker提供三种用户自定义网络驱动:bridge、overlay和macvlan。overlay和macvlan用于创建跨主机的网络。
- 自定义网络:docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet
- 容器要使用上面自定义的新网络时,需要在启动的时候指定:docker run -it --name=b1 --network=mynet busybox
目前,我们创建的容器的IP都是Docker自动从subnet中分配的,我们能否指定一个静态IP呢?
可以,docker run -it --name=b1 --network=mynet --ip 192.168.0.5 busybox
- 注意:只有使用--subnet创建的网络才能指定静态IP(如果通过docker network create --driver bridge mynet的方式创建的网络是不行的)
当两个容器使用同一网络,他们之间是可以ping通的,因为挂在同一个网桥下。
比如我们创建两个使用默认网络的容器,然后在两个容器中进行相互ping,看能否访问成功
-
在t1容器中ping a2
-
在a2容器中ping t1
那么如果使用两个不同网络启动的容器间可以ping通吗?答案是不行的,因为他们都没有在一个网桥下。
那么应该怎么让两个不同网络的容器也能ping通呢?
我们可以使用跨网络连接别人的方式:docker network connect mynet tomcat,表示把tomcat加入到mynet网络中(这里tomcat启动时使用的是默认的bridge网络,将它加入到mynet网络中,就可以与通过mynet网络启动的容器进行通信)
- 这其实是在tomcat容器中加了一个虚拟网卡,分配了mynet网络的IP,这样就可以让mynet网络的容器访问到tomcat了。
-
--link:
docker run -d -p --name=t1 --link=r1 tomcat
这种方式容器启动的时候,会查询–link的容器(r1)的ip并写死到当前容器的/etc/hosts下面;而且是单向的,对方并不知道我的存在的。
这种方式存在的问题:r1的ip地址变了是不会更新hosts文件的
-
自定义桥接网络
通过IP访问容器不够灵活,因为在部署应用前它的IP是不确定的,部署之后再指定要访问的IP就会很麻烦。
采用自定义网络后,每个容器间可以就通过容器名访问。从Docker1.10版本开始,docker deamon实现了一个内嵌的DNS server,使容器可以直接通过“容器名”通信。
- 注意:docker DNS有个限制,就是只能在自定义网络中使用。
- 容器访问外网
注意:这里的外网只的是容器网络以外的网络环境,并非特指Internet。
当我们宿主机可以访问外网时,我们的容器也就能访问外网。为什么呢???
首先我们可以查看一下宿主机上的iptables规则
我们可以看到,在NAT表中,有一条规则,它的含义是:如果网桥docker0收到来自172.17.0.0/16网段的外出包,把它交给MASQUERADE处理,而MASQUERADE的处理方式是将包的源地址替换成host的地址发出去,也就是做了一次网络地址转换(NAT)。
接着我们通过tcpdump查看地址是如何转换的,先查看宿主机的路由表
默认路由是通过eth0发出去的,所以我们要同时监控eth0和docker0上icmp数据包。
当我们在我们创建的busybox容器中ping baidu.com时,tcpdump输出如下:
docker0收到busybox的ping包,源地址是容器的IP:172.17.0.3,是来自172.17.0.0/16网段的包没问题,然后将他交个MASQUERADE处理,MASQUERADE将包的源地址替换成host的地址发出去,我们可以监控eth0上的icmp数据包看看
我们可以发现源地址变成了eth0的IP:10.0.2.15。下面这个图就是上述过程的全过程:
- 外网访问容器
那么外网怎么访问我们的容器呢?通过端口映射!
docker可以将容器对外提供服务的端口映射到宿主机的某个端口上,外网通过
: 访问到容器。 -
这个端口映射是在容器启动的时候指定的,有两种方式
-
docker run -d -P --name=n1 nginx:随机指定端口,运行容器后可以通过docker ps查看这个容器的端口映射信息。
-
docker run -d -p 8888:80 --name=n2 nginx:指定端口映射,即将容器的80端口映射到宿主机的8888端口。
-
我们每一个映射的端口,host都会启动一个docker-proxy进程来监听这个端口,处理访问容器的流量
-



