底层原理不懂就上手,上手出了问题就懵逼,最近在对接阿里云时遇到Docker存储驱动的神坑,爬了几天爬不出来,最后发现是节点中Docker存储驱动的问题,由此引发此次学习,避免类似问题再次懵逼。
关于镜像images,核心首先必须明确一点,镜像都是只读的,如果需要进行写操作,必须在该镜像上创建一个新的镜像层(我们所有的写操作其实都是在一个可写的镜像层上操作的)。同时镜像也是共享的,那些依赖于同一个 image 的多个容器,并不会将单独复制需要的镜像到自己的容器中进行启动,那样会浪费巨大的空间,实际节点上只会有一个镜像,多个容器是共享这一个镜像的。存储驱动可以将容器中的数据进行持久化同时避免性能问题。存储驱动允许我们在容器的可写层创建数据,这些数据在容器销毁后也就没有了,且这些文件的读写速度都比本地文件系统的性能低。
默认情况下,容器内所有创建的文件都存储在一个可写层,所以这些文件仅仅存活于容器运行时,一旦容器被销毁这些文件也会被销毁。容器的可写层和宿主机器紧密耦合,很难将可写层的文件或数据迁移到非宿主机器外的地方。要将数据写入容器的可写层必须要有一个存储驱动(storage driver)来管理文件系统,存储驱动程序使用Linux内核提供的联合文件系统,这种方式与使用直接写入主机文件系统的数据卷相比降低了性能。Docker提供几种种持久化容器中数据或文件的方式:
volumes
:Volumes是持久化Docker中数据最好的方式,存在于宿主机器文件系统的一部分(Linux中位于/var/lib/docker/volumes/
目录下的xx.db
文件),由Docker自己管理,非Docker进程去修改这部分的文件;bind mounts
:这种方式可以将容器中的数据持久化到宿主机器的任何位置(文件或目录),任何进程都可以进行修改tmpfs mount
:Linux 上专用,tmpfs挂载仅存储在主机系统的内存中,永远不会写入主机系统的文件系统;named pipe
:windowns 上专用,同上;关于上述的几种方式的区别,官网形象的图文解释:
Volumes(数据卷) 由 Docker 自行创建和管理,可以使用docker volume create
显式的创建一个Volume,当然如果不人为创建,Docker会在创建容器或服务时自行创建。在使用docker volume create xxx
创建 Volume 时,Docker会在宿主机器的/var/lib/docker/volumes/
目录下创建xxx
文件夹用于容器数据的持久化。当将xxx
的Volume挂载到容器上时,此目录就是容器中对应的目录。这种方式和bind mounts
很相似,但 Volumes 方式是和宿主机器核心功能隔离并且是由Docker自己管理的。注意同一个Volume是可以同时挂载到多个容器内的,当没有使用该存储卷的容器时,该存储卷并不会自动销毁(因为主要是解决容器可写层数据持久化的问题,当然不会自动销毁),如果需要删除某个存储卷需要手动调用docker volume prune
命令,这个命令将会移除所有宿主机器本地的Volumes(谨慎操作)。
当挂载一个 Volume 时,该存储卷可以进行命名或直接匿名,匿名存储卷首次挂载到容器时并不会显式的给它一个名字,而是Docker给它们一个随机名字,且保证该名字是Docker主机中唯一的,匿名和命名的存储卷唯一的区别就是它们名字。
Volumes 是支持 volume driver 的,它允许使用远程主机的存储卷或者其他云供应商提供的数据卷或者其他的存储形式。
下面是一个示例:
# 创建数据卷
docker volume create testvolumes
# 直接拉一个ubuntu镜像来实验
docker pull ubuntu
# 通过 '-v/--volume 存储卷' 的方式即可挂载 /var/lib/docker/volumes/testvolumes:
docker run -it -v testvolumes ubuntu:latest
# 在容器中的 /testvolumes/_data 目录下创建空文件 addDatas,退出容器并查看宿主机器的 /var/lib/docker/volumes/testvolumes/_data 创建成功
touch addDatas
Volum应用场景包含:
/var/lib/docker/volumes/
)即可;Volume 方式有点注意点:
数据卷驱动,当创建数据卷docker volume create xxx
或启动容器挂载一个还未创建的数据卷时,此时可以指定一个数据卷驱动。下面是一个示例
数据卷 Volume 支持--mount
和--volume/-v
的语法,当和服务(即swarm services)一起使用数据卷时,仅支持--mount
参数,所以官方也是一直推荐使用--mount
,完整的示例:
# 1. 单个容器的示例
# 创建数据卷
docker volume create my-vol
# 查看数据卷列表
docker volume ls
# 使用ubuntu镜像创建一个容器devtest,将数据卷my-vol挂载到目录/app下,并且该数据卷是只读的
docker run -d --name devtest --mount src=my-vol,dst=/app,readonly ubuntu:latest
# 检查容器信息(注意查看 Mounts 字段是否和期望的一样)
docker volume inspect devtest
# 移除数据卷
docker container stop devtest
docker container rm devtest
docker volume rm my-vol
# 2. 容器服务的示例
# 将当前节点进行 swarm 初始化,并且成为master
docker swarm init
# 创建副本为4的service,service仅支持--mount,不支持-v/--volume的形式
docker service create -d --replicas=4 --name devtest-service --mount src=my-vol,dst=/app/test ubuntu:latest
# 查看service的状态
docker service ps devtest-service
# 移除所有 devtest-service 的task
docker service rm devtest-service
Bind mounts (绑定加载)的方式在Dokcer早期版本就提供了,相对于 Volumes 方式功能有限。Bind mounts 就是将宿主机器中一个文件或者目录挂载到容器中,挂载时必须以该文件或目录的绝对路径指定,而且指定的宿主机器上的路径不一定非要在宿主机器上存在,Docker在加载时会去按需创建,而且绑定加载的性能非常好,但它依赖于宿主机器上特定的目录结构。所以目前为官网还是推荐使用数据卷(Volumes)的形式来作存储驱动。此外,bind mounts 不能使用使用Dokcer命令行直接管理这些绑定加载(bind mounts)。
此外 bind mounts 方式必须对一些敏感文件(比如host文件)有权限,可以通过运行在容器中的进程修改(增长改查)系统host文件,这个功能可能会引发安全问题,因为此时Dokcer对宿主机器上非Docker进程甚至系统级别的文件都可以随意更改。
下面是一个示例:
# 通过 `-v/--volume 宿主机目录:容器目录` 的形式将宿主机器的 /root/helm 目录挂载到容器的 /usr/local/helm 目录
docker run --volume /root/helm:/usr/local/helm -it ubuntu:latest
绑定挂载适用的场景有:
/etc/resolv.conf
挂载到容器内从而给容器提供的默认DNS策略;target
目录绑定挂载到容器中,这样就不需要要在容器内再次构建了; 绑定挂载和数据卷同时有一个注意点,如果将一个非空宿主机目录挂载到容器中已存在的文件或者目录,那么容器内的这些文件或目录将会被宿主机的文件或者目录将会被遮挡,就像你把文件存在宿主机的/mnt
目录,然后将一个USB驱动也挂载到/mnt
,那此时/mnt
目录中的内容将会被USB驱动遮挡直到USB驱动取消挂载,被遮挡的文件或目录并不会被移除或修改,只是使用绑定挂载或者数据卷形式时这些文件是不可得的;
tmpfs
挂载不会持久化在磁盘上持久化,也不会存储在宿主机器上,可以在容器的生命声明周期内使用,用于存储不需要持久化或者一些敏感信息,典型应用在内部,群集服务使用tmpfs
挂载将机密安装到服务的容器中。使用--tmpfs
参数进行挂载。
tmpfs
挂载适用于那些不需要在宿主机器或容器中持久化数据的场景,比如可能为了安全原因或者是为了在application 需要写大量非持久化数据时保证容器的性能(我觉得这个原因靠谱点)。
npipe
挂载可以用于宿主机器和Docker容器的通信,典型应用就是在容器中运行第三方工具时使用named pipe
连接 Docker Engine 的 API。
注:上述4种挂载方式在语法上不太一样,使用-v/--volume
来使用,在17.06+可以不区分挂载方式直接使用--mount
进行挂载(本来这个参数是用于 swarm services,而单个容器使用-v/--volume
),因为之前使用的 -v/--volume
参数名和数据卷Volume的形式一样,很容易误导开发者是转专用于数据挂载方式的参数,而且对于参数的定义较为严格,使用--mount
参数语义更加清晰,诸如:docker run -it --mount source=testvolumes,target=/usr/local/helm ubuntu:latest
(前提是建立自己的Volume)。
-v/--volume
命令实际是有三个参数的,之间以:
分隔,而且必须以规定的顺序出现,三个参数的具体含义为:
,
分隔的参数; --mount
命令参数是由多个
形式的键值对组成,之间以,
分隔,不要求顺序,主要参数的键值对有:
type=xxx
:type
参数用来表示挂载的方式,value可以是volume
、bind
或者tmpfs
;source/src=xxx
:source
或者src
参数表示数据卷(有名字的,匿名数据卷该参数省略),value为数据卷的名字;target/destination/dst=xxx
:target
或者destination
或者dst
参数表示内容器的挂载路径,value即为容器内的目录;readonly
:表示绑定挂载是否只读bind mount
,如果出现(该字段非键值对),那容器内对该文件/目录只具备读的能力;volume-opt
:表示除上述参数之外的其他参数,可以出现多个volume-opt
参数,每个都以键值对的形式出现; 如果数据卷驱动可以接受多个以,
分隔的数组,那在传参时必须在CSV编译器检查语法时正常通过,为此用双引号""
将volume-opt
这个参数对象包裹起来,然后整个--mount
参数对象使用单引号''
包裹,示例:
$ docker service create \
--mount 'type=volume,src=,dst=,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:,"volume-opt=o=addr=,vers=4,soft,timeo=180,bg,tcp,rw"'
--name myservice \
<IMAGE>