0%

关于 Docker 中镜像与存储卷的概念和详细使用方法

关于 Docker 系列其他文章的传送门

  1. 《 简单了解 Linux 容器是什么 以及 Docker 常用命令操作说明 》

  2. 《 Docker 中 4 种网络模型的概念和实现 》

  3. 《 关于 Docker 中镜像与存储卷的概念和详细使用方法 》     您当前所在位置

  4. 《 使用 Dockerfile 定制镜像 搭建 php 站点 》

  5. 《 使用 Harbor 创建本地 Docker 仓库 》



images

一、聊聊 Docker 镜像

容器技术的火热,离不开 docker 的推动,但是容器技术并不是 docker 公司发明的,这家公司最大的创新其实是镜像技术,而镜像技术其实早就已经存在,但是 docker 却创造了一种叫分层镜像的机制,这种分层镜像技术,就好像一把助燃剂,让容器技术如干柴烈火般的燃烧起来

在 docker 出现之前,人们想要使用容器,需要操作 namespace、cgroup、lxc 等机制,过程及其繁琐,而且对比虚拟机来说,安装速度并没有多大优势,而 docker 出现后,人们只需要从指定的镜像仓库下载镜像,在本地启动即可使用容器
这种镜像非常精简,只有程序本身和其依赖的文件,启动速度极快,究其根本还是因为镜像分层的技术加持,那什么是镜像分层呢?

镜像分层的原理

docker分层

什么是分层呢,如下图所示:
layer

一个 docker 镜像由多个可读的镜像层组成,然后运行的容器会在这个 docker 的镜像上面多加一层可写的容器层,任何的对文件的更改都只存在此容器层。因此任何对容器的操作均不会影响到镜像

如何实现

至于容器如何获取镜像层文件而又不影响到是镜像层的呢? docker 是这样实现的…

如果需要获取某个文件,那么容器曾会从上到下去下一层的镜像层去获取文件,如果该层文件不存在,那么就会去下一镜像层去寻找,直到最后一层。 对于用户而言,用户面向的是一个叠加后的文件系统

kernel

docker 就是利用之前提到过的一个叫做 copy-on-write (CoW) 的策略来保证 base 镜像的安全性,以及更高的性能和空间利用率。,而任何对于文件的操作都会记录在容器层,例如说修改文件,容器层会把在镜像层找到的文件拷贝到容器层然后进行修改,删除文件则会在容器层内记录删除文件的记录

cow

联合文件系统(UnionFS)

分层镜像这种特殊的机制,构建于一个叫联合文件系统(UnionFS)之上,这是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下 (unite several directories into a single virtual filesystem)

联合文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于镜像(base 镜像),可以制作各种具体的应用镜像

另外,不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率

Docker 中使用的 AUFS(AnotherUnionFS)就是一种联合文件系统。 AUFS 支持为每一个成员目录(类似 Git 的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限, 同时 AUFS 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改 (不影响只读部分的)

Docker 目前支持的联合文件系统包括 OverlayFS, AUFS, Btrfs, VFS, ZFS 和 Device Mapper

各 Linux 发行版 Docker 推荐使用的存储驱动如下表。

Linux 发行版 Docker 推荐使用的存储驱动
Docker CE on Ubuntu overlay2 (16.04 +)
Docker CE on Debian overlay2 (Debian Stretch), aufs, devicemapper
Docker CE on CentOS overlay2
Docker CE on Fedora overlay2

在可能的情况下,推荐使用 overlay2 存储驱动,overlay2 是目前 Docker 默认的存储驱动,以前则是 aufs。你可以通过配置来使用以上提到的其他类型的存储驱动

关于 base 镜像

base 镜像有两层含义:

  1. 不依赖其他镜像,从 scratch 构建
  2. 其他镜像可以之为基础进行扩展

所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等

base 镜像提供的是最小安装的 Linux 发行版

我们大部分镜像都将是基于 base 镜像构建的。所以,通常使用的是官方发布的 base 镜像。可以在 docker hub 里找到

比如 centos: https://hub.docker.com/_/centos

我们可以自己构建 docker base 镜像,也可以直接使用已有的 base 镜像。比如 centos, 我们可以直接从 docker hub 上拉取

拉取

1
docker pull centos

查看

1
2
3
docker images centos
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 1e1148e4cc2c 2 months ago 202MB

可以看到最新的 centos 镜像只有 200mb ,是不是觉得太小了?这是因为 docker 镜像在运行的时候直接使用 docker 宿主机器的 kernel

众所周知:
Linux 操作系统由 内核空间用户空间 组成
内核空间是 kernel ,用户空间是 rootfs ,不同 Linux 发行版的区别主要是 rootfs
比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大

所以 Docker 可以同时支持多种 Linux 镜像,模拟出多种操作系统环境

综述

可能会有人问为什么要这么去做呢?我觉得有两大好处:

  1. 基本上每个软件都是基于某个镜像去运行的,因此一旦某个底层环境出了问题,就不需要去修改全部基于该镜像的软件的镜像,只需要修改底层环境的镜像
  2. 这个好处也是最大好处,就是可以共享资源,其他相同环境的软件镜像都共同去享用同一个环境镜像,而不需要每个软件镜像要去创建一个底层环境

镜像的使用

常用参数

1
2
3
4
5
6
7
8
9
10
11
12
13
image: # 镜像类分组
ls 列出镜像 = docker images
pull 从远程下载镜像到本地
push 上传镜像
rm 删除镜像 = rmi 大杂烩命令
tag 给镜像打标签
inspect 查看镜像详细信息

container # 容器类分组
commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] 把容器做成镜像
-p 制作镜像时暂停容器
-a 指定镜像的作者
-c 改变底层默认运行的程序

示例:

1
2
3
docker container commit -p -a "monster <admin@monster.com>" -c 'CMD ["/bin/sh","-c","/bin/sh"]' centos_monster centos:sh
# 把一个叫 centos_monster 的容器(容器名,不是镜像名)容器打包成叫 centos:sh 的镜像
# 先暂停容器,指定作者是 monster,将默认要运行的命令 (bash) 改为 sh

搜索镜像

下载镜像时,可以使用官方的镜像仓库 Docker Hub,或者国内的镜像加速器,如阿里云(需要注册账号)DaoCloud

这张图是关于 DockerHub 上镜像的说明

hub_1

hub_2

也可以使用 docker 客户端搜索

1
2
3
4
5
6
7
# 语法
docker search IMAGES_NAME:TAG

# 示例
docker search centos:7
或者
docker search centos:6

search

下载镜像

使用参数 pull
pull

查看本地已有镜像

1
2
3
4
5
root@node1 ~] docker images
#仓库名 #标签 #短 ID #创建时间 #大小
REPOSITORY TAG IMAGE ID CREATED SIZE
centos 7 9f38484d220f 4 months ago 202MB
centos latest 9f38484d220f 4 months ago 202MB

更改镜像标签

1
2
3
4
5
6
root@node1 [10:35:10 PM] [~] -> docker image tag centos:7 centos:monster #把 centos:7 改成 centos:monster
root@node1 [10:35:27 PM] [~] -> docker images
# 可以看到 ID 是一样的,只有 tag 不一样
REPOSITORY TAG IMAGE ID CREATED SIZE
centos 7 9f38484d220f 4 months ago 202MB
centos monster 9f38484d220f 4 months ago 202MB

指定镜像启动容器

优先使用本地镜像,没有的话从指定互联网镜像仓库下载到本地并启动

这里使用刚才改好的镜像

1
2
                                          #容器名            #镜像名
root@node1 ~] docker run -it --rm --name centos_monster centos:monster

打包镜像

1. 做记号

在刚才启动的容器内创建 1120 的文件夹做记号

1
2
3
4
[root@49364c3bf90e /] mkdir {11..20}
[root@49364c3bf90e /] ls
11 13 15 17 19 anaconda-post.log dev home lib64 mnt proc run srv tmp var
12 14 16 18 20 bin etc lib media opt root sbin sys usr
2. 使用快捷键剥离容器 shell
  • ctrl + p + ctrl + q
3. 查看容器的状态是 UP
1
2
3
root@node1 [10:45:18 PM] [~] -> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
49364c3bf90e centos:monster "/bin/bash" 4 minutes ago Up 4 minutes monster
4. 把容器打包成镜像
1
2
3
docker container commit -p -a "monster <admin@monster.com>" -c 'CMD ["/bin/sh","-c","/bin/sh"]' centos_monster centos:sh
# 把一个叫 centos_monster 的容器(容器名,不是镜像名)容器打包成叫 centos:sh 的镜像
# 先暂停容器,指定作者是 monster,将默认要运行的命令 (bash) 改为 sh
5. 查看镜像

刚才打包的镜像 centos:sh

1
2
3
root@node1 [11:11:36 PM] [~] -> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos sh 7b7666dabd04 11 seconds ago 202MB
6. 启动容器

指定从 centos:sh 启动容器

发现 shell 已经不是 bash 了,之前创建的 11 到 20 文件夹也存在,说明是刚才制作的镜像

1
2
3
4
5
6
root@node1 [11:11:39 PM] [~] -> docker run -it --rm centos:sh

sh-4.2# ls
11 13 15 17 19 anaconda-post.log dev home lib64 mnt proc run srv tmp var
12 14 16 18 20 bin etc lib media opt root sbin sys usr
sh-4.2#

上传镜像到 DockerHub

我们把刚才修改过的镜像上传到 DockerHub 仓库,需要先注册

如果是阿里云的话,方法类似,但是上传时候的地址是私有的,登录阿里云官网查看详细说明(需注册)

1. 创建一个仓库

repository

2. 更改镜像的 TAG

必须是这种格式 HubID/IMAGENAME:TAG

1
2
3
4
5
6
7
root@node1 [11:52:13 PM] [~] -> docker image tag centos:sh monster0303/centos:sh
root@node1 [11:52:13 PM] [~] -> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos sh 7b7666dabd04 40 minutes ago 202MB
monster0303/centos sh 7b7666dabd04 40 minutes ago 202MB
centos latest 9f38484d220f 4 months ago 202MB
centos monster 9f38484d220f 4 months ago 202MB
3. 登录 DockerHub
1
2
3
4
5
6
7
8
9
root@node1 [11:57:24 PM] [~] -> docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: monster0303
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded # 登录成功
4. 上传镜像
1
2
3
4
5
root@node1 [11:59:05 PM] [~] -> docker push monster0303/centos:sh
The push refers to repository [docker.io/monster0303/centos]
9045b15099e1: Pushed
d69483a6face: Mounted from library/centos
sh: digest: sha256:1c8ca46a1ba00d8002cde76740ed1de3235cb57386d0fc2e9995d87caf104af6 size: 736
5. 查看

DockerHub 上已经可以看到了,大功告成!!
push

二、存储卷的引入

1. 为什么有存储卷

Docker 的镜像由多个只读层叠加而成,启动容器时,Docker 会加载只读镜像层并在镜像栈顶部添加一个读写层

如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本仍然存在,只是已经被读写层中该文件的副本所隐藏,此即 写时复制 (COW) 机制

关闭并重启容器,其数据不受影响,但删除 Docker 容器,则其更改将会全部丢失

这样存在的问题:

  • 存储于联合文件系统中,不易于宿主机访问;
  • 容器间数据共享不便
  • 删除容器其数据会丢失
  • 无法迁移

2. 解决方案

既然镜像存在这么严重的问题,那在生产环境怎么保证数据的稳定性呢?以后用编排系统动态管理容器时怎么办呢?

解决方案就是:卷( volume )

卷是容器上的一个或多个 目录,此类目录可绕过联合文件系统,与宿主机上的某目录 绑定 (关联)

volume

Docker 有两种类型的卷,每种类型都在容器中存在一个挂载点,但其在宿主机上的位置有所不同

类型 含义 区别
Bind mount volume 指向主机文件系统上用户指定位置的卷 在宿主机上的目录是用户指定的,在容器上的目录也是指定的
Docker-managed volume Docker 守护进程在主机文件系统中 Docker 拥有的一部分中创建托管卷 容器上的目录是用户指定的,但是在宿主机上的目录是在固定的目录下自动生成的

3. 卷参数

1
2
3
4
5
6
docker run
--volume CONTAINER_DIR # 指定容器内目录,宿主机随机目录
--volume HOST_DIR:CONTAINER_DIR # 指定容器和宿主机目录
--volume-from CONTAINER # 从其他容器克隆卷

docker container inspect -f {{.顶级.一级.二级.四级}} CONTAINER_NAME # 根据指定格式过滤并显示

4. 示例

先创建容器 c1,指定容器内目录挂载点为 /www_data,宿主机目录随机

1
root@node1 [06:47:35 PM] [~] -> docker run --name c1 -it --volume /www_data centos

查看容器内 / 目录下确实创建了 www_data 目录

1
2
[root@862a1d8070e3 /]# ls /
anaconda-post.log bin dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var www_data

接下来在挂载目录中写一个 test.txt 文件

1
[root@862a1d8070e3 /]# echo "test page " > www_data/test.txt

回到宿主机,查看挂载点在哪个目录下

1
2
3
4
root@node1 [06:51:38 PM] [~] -> docker container inspect -f {{.Mounts}} c1
[{volume e9d29b27d26fd5a7573b0da088ac42ad9d80e2742c5ecee2519e8a12980d852f /var/lib/docker/volumes/e9d29b27d26fd5a7573b0da088ac42ad9d80e2742c5ecee2519e8a12980d852f/_data /www_data local true }]
↑ ↑
# 宿主机挂载点 # 容器内挂载点

在挂载点内,就能看到容器内创建的 test.txt 文件了

1
2
root@node1 [06:47:35 PM] [~] -> cat /var/lib/docker/volumes/e9d29b27d26fd5a7573b0da088ac42ad9d80e2742c5ecee2519e8a12980d852f/_data/test.txt
test page

指定宿主机挂载目录也是类似的效果,而 “从其他容器克隆卷” 的选项,就是会从指定的容器卷克隆一份出来作为自己的卷用,可以理解成虚拟机里的模板,从模板创建虚拟机类似

5. jq

Docker 中的信息大部分是使用 JONS 格式存储,不方便阅读,比如刚才查看 Mounts 时,阅读体验并不友好,为了便于阅读,可以使用 jq 格式化输出

jq 格式化输出 JONS 的文档

1
jq '.'  # 不做任何更改,直接美化输出

需要注意的是,jq 不支持过滤后的 JONS,可能因为无法判断语法了

所以可以先格式化输出 再过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
root@node1 [06:47:35 PM] [~] -> docker inspect 862a1d8070e3 | jq '.'| grep -i mounts -A 10
"Mounts": [
{
"Type": "volume",
"Name": "e9d29b27d26fd5a7573b0da088ac42ad9d80e2742c5ecee2519e8a12980d852f",
"Source": "/var/lib/docker/volumes/e9d29b27d26fd5a7573b0da088ac42ad9d80e2742c5ecee2519e8a12980d852f/_data",
"Destination": "/www_data",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}

参考文章
https://yeasy.gitbooks.io/docker_practice/underly/ufs.html
http://blog.sbb.fun/blog/docker/docker3.html
https://www.cnblogs.com/woshimrf/p/docker-container-lawyer.html
https://www.itcodemonkey.com/article/13255.html