容器网络发展史

提起 Docker 网络,不得不从容器战争说起。Docker 从 2013 年诞生,到后来逐渐成为了容器的代名词,然而 Docker 的野心也不止于此,它还想在更多的领域独占鳌头,比如制定容器的网络和存储标准。

于是 Docker 从 1.7 版本开始,便把网络和存储从 Docker 中正式以插件的形式剥离开来,并且分别为其定义了标准,Docker 定义的网络模型标准称之为 CNM (Container Network Model) 。

Docker 推出 CNM 的同时,CoreOS 推出了 CNI(Container Network Model)。起初,以 Kubernetes 为代表的容器编排阵营考虑过使用 CNM 作为容器的网络标准,但是后来由于很多技术和非技术原因,Kubernetes 决定支持 CoreOS 推出的容器网络标准 CNI。

从此,容器的网络标准便分为两大阵营,一个是以 Docker 公司为代表的 CNM,另一个便是以 Google、Kubernetes、CoreOS 为代表的 CNI 网络标准。

CNM

CNM (Container Network Model) 是 Docker 发布的容器网络标准,意在规范和指定容器网络发展标准,CNM 抽象了容器的网络接口 ,使得只要满足 CNM 接口的网络方案都可以接入到 Docker 容器网络,更好地满足了用户网络模型多样化的需求。
CNM 只是定义了网络标准,对于底层的具体实现并不太关心,这样便解耦了容器和网络,使得容器的网络模型更加灵活。
CNM 定义的网络标准包含三个重要元素。

  • 沙箱(Sandbox):沙箱代表了一系列网络堆栈的配置,其中包含路由信息、网络接口等网络资源的管理,沙箱的实现通常是 Linux 的 Net Namespace,但也可以通过其他技术来实现,比如 FreeBSD jail 等。
  • 接入点(Endpoint):接入点将沙箱连接到网络中,代表容器的网络接口,接入点的实现通常是 Linux 的 veth 设备对。
  • 网络(Network):网络是一组可以互相通信的接入点,它将多接入点组成一个子网,并且多个接入点之间可以相互通信。

CNM 的三个要素基本抽象了所有网络模型,使得网络模型的开发更加规范。
为了更好地构建容器网络标准,Docker 团队把网络功能从 Docker 中剥离出来,成为独立的项目 libnetwork,它通过插件的形式为 Docker 提供网络功能。Libnetwork 是开源的,使用 Golang 编写,它完全遵循 CNM 网络规范,是 CNM 的官方实现。Libnetwork 的工作流程也是完全围绕 CNM 的三个要素进行的,下面我们来详细了解一下 Libnetwork 是如何围绕 CNM 的三要素工作的。

Libnetwork 的工作流程

Libnetwork 是 Docker 启动容器时,用来为 Docker 容器提供网络接入功能的插件,它可以让 Docker 容器顺利接入网络,实现主机和容器网络的互通。

第一步:Docker 通过调用 libnetwork.New 函数来创建 NetworkController 实例。NetworkController 是一个接口类型,提供了各种接口,代码如下:

  1. 1. type NetworkController interface {
  2. 2. // 创建一个新的网络。 options 参数用于指定特性类型的网络选项。
  3. 3. NewNetwork(networkType, name string, id string, options ...NetworkOption) (Network, error)
  4. 4. // ... 此次省略部分接口
  5. 5. }

第二步:通过调用 NewNetwork 函数创建指定名称和类型的 Network,其中 Network 也是接口类型,代码如下:

  1. 1. type Network interface {
  2. 2. // 为该网络创建一个具有唯一指定名称的接入点(Endpoint)
  3. 3. CreateEndpoint(name string, options ...EndpointOption) (Endpoint, error)
  4. 4.
  5. 5. // 删除网络
  6. 6. Delete() error
  7. 7. // ... 此次省略部分接口
  8. 8. }

第三步:通过调用 CreateEndpoint 来创建接入点(Endpoint)。在 CreateEndpoint 函数中为容器分配了 IP 和网卡接口。其中 Endpoint 也是接口类型,代码如下:

  1. 1. // Endpoint 表示网络和沙箱之间的逻辑连接。
  2. 2. type Endpoint interface {
  3. 3. // 将沙箱连接到接入点,并将为接入点分配的网络资源填充到沙箱中。
  4. 4. // the network resources allocated for the endpoint.
  5. 5. Join(sandbox Sandbox, options ...EndpointOption) error
  6. 6. // 删除接入点
  7. 7. Delete(force bool) error
  8. 8. // ... 此次省略部分接口
  9. 9. }

第四步:调用 NewSandbox 来创建容器沙箱,主要是初始化 Namespace 相关的资源。

第五步:调用 Endpoint 的 Join 函数将沙箱和网络接入点关联起来,此时容器就加入了 Docker 网络并具备了网络访问能力。
Libnetwork 基于以上工作流程可以构建出多种网络模式,以满足我们的在不同场景下的需求,下面详细介绍Libnetwork 提供的常见的四种网络模式。

Libnetwork 常见网络模式

Libnetwork 比较典型的网络模式主要有四种,这四种网络模式基本满足了单机容器的所有场景。

  1. null 空网络模式:可以构建一个没有网络接入的容器环境,以保障数据安全。
  2. bridge 桥接模式:可以打通容器与容器间网络通信的需求。
  3. host 主机网络模式:可以让容器内的进程共享主机网络,从而监听或修改主机网络。
  4. container 网络模式:可以将两个容器放在同一个网络命名空间内,让两个业务通过 localhost 即可实现访问。

下面我们对 libnetwork 的四种网络模式逐一讲解:

(1)null 空网络模式

有时候,我们需要处理一些保密数据,出于安全考虑,需要一个隔离的网络环境执行一些纯计算任务。这时候 null 网络模式就派上用场了,这时候容器就像一个没有联网的电脑,处于一个相对较安全的环境,确保数据不被他人从网络窃取。
使用 Docker 创建 null 空网络模式的容器时,容器拥有自己独立的 Net Namespace,但是此时的容器并没有任何网络配置。在这种模式下,Docker 除了为容器创建了 Net Namespace 外,没有创建任何网卡接口、IP 地址、路由等网络配置。
使用 docker run 命令启动时,添加 —net=none 参数启动一个空网络模式的容器,命令如下:

  1. 1. $ docker run --net=none -it busybox
  2. 2. / #

容器启动后,使用 ifconfig 命令查看一下容器内网络配置信息:

  1. 1. / # ifconfig
  2. 2. lo Link encap:Local Loopback
  3. 3. inet addr:127.0.0.1 Mask:255.0.0.0
  4. 4. UP LOOPBACK RUNNING MTU:65536 Metric:1
  5. 5. RX packets:0 errors:0 dropped:0 overruns:0 frame:0
  6. 6. TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  7. 7. collisions:0 txqueuelen:1000
  8. 8. RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

可以看到容器内除了 Net Namespace 自带的 lo 网卡并没有创建任何虚拟网卡,然后再使用 route -n 命令查看一下容器内的路由信息:

  1. 1. / # route -n
  2. 2. Kernel IP routing table
  3. 3. Destination Gateway Genmask Flags Metric Ref Use Iface

可以看到,容器内也并没有配置任何路由信息。

(2)bridge 桥接模式

Docker 的 bridge 网络是启动容器时默认的网络模式,使用 bridge 网络可以实现容器与容器的互通,可以从一个容器直接通过容器 IP 访问到另外一个容器。同时使用 bridge 网络可以实现主机与容器的互通,我们在容器内启动的业务,可以从主机直接请求。
在介绍 Docker 的 bridge 桥接模式前,需要先了解一下 Linux 的 veth 和 bridge 相关的技术,因为 Docker 的 bridge 模式正是由这两种技术实现的。

  • Linux veth

veth 是 Linux 中的虚拟设备接口,veth 都是成对出现的,它在容器中,通常充当一个桥梁。veth 可以用来连接虚拟网络设备, veth 可以用来连通两个 Net Namespace,从而使得两个 Net Namespace 之间可以互相访问。

  • Linux bridge

Linux bridge 是一个虚拟设备,是用来连接网络的设备,相当于物理网络环境中的交换机。Linux bridge 可以用来转发两个 Net Namespace 内的流量。

  • veth 与 bridge 的关系

image.png
我们可以看到,bridge 就像一台交换机,而 veth 就像一根网线,通过交换机和网线可以把两个不同 Net Namespace 的容器连通,使得它们可以互相通信。
Docker 的 bridge 模式也是这种原理。Docker 启动时,libnetwork 会在主机上创建 docker0 网桥,docker0 网桥就相当于图中的交换机,而 Docker 创建出的 brige 模式的容器则都会连接 docker0 上,从而实现网络互通。
bridge 桥接模式是 Docker 的默认网络模式,当我们创建容器时不指定任何网络模式,Docker 启动容器默认的网络模式为 bridge。
**

(3)host 主机网络模式

容器内的网络并不是希望永远跟主机是隔离的,有些基础业务需要创建或更新主机的网络配置,程序必须以主机网络模式运行才能够修改主机网络,这时候就需要用到 Docker 的 host 主机网络模式。
使用 host 主机网络模式时:

  • libnetwork 不会为容器创建新的网络配置和 Net Namespace。
  • Docker 容器中的进程直接共享主机的网络配置,可以直接使用主机的网络信息,此时,在容器内监听的端口,也将直接占用到主机的端口。
  • 除了网络共享主机的网络外,其他的包括进程、文件系统、主机名等都是与主机隔离的。

host 主机网络模式通常适用于想要使用主机网络,但又不想把运行环境直接安装到主机上的场景中。例如我想在主机上运行一个 busybox 服务,但又不想直接把 busybox 安装到主机上污染主机环境,此时我可以使用以下命令启动一个主机网络模式的 busybox 镜像:

  1. 1. $ docker run -it --net=host busybox
  2. 2. / #

然后使用ip a 命令查看一下容器内的网络环境:

  1. 1. / # ip a
  2. 2. 1: lo: <LOOPBACK,UP,LOWER\_UP> mtu 65536 qdisc noqueue qlen 1000
  3. 3. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  4. 4. inet 127.0.0.1/8 scope host lo
  5. 5. valid\_lft forever preferred\_lft forever
  6. 6. inet6 ::1/128 scope host
  7. 7. valid\_lft forever preferred\_lft forever
  8. 8. 2: eth0: <BROADCAST,MULTICAST,UP,LOWER\_UP> mtu 1500 qdisc pfifo\_fast qlen 1000
  9. 9. link/ether 02:11:b0:14:01:0c brd ff:ff:ff:ff:ff:ff
  10. 10. inet 172.20.1.11/24 brd 172.20.1.255 scope global dynamic eth0
  11. 11. valid\_lft 85785286sec preferred\_lft 85785286sec
  12. 12. inet6 fe80::11:b0ff:fe14:10c/64 scope link
  13. 13. valid\_lft forever preferred\_lft forever
  14. 14. 3: docker0: \<NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue
  15. 15. link/ether 02:42:82:8d:a0:df brd ff:ff:ff:ff:ff:ff
  16. 16. inet 172.17.0.1/16 scope global docker0
  17. 17. valid\_lft forever preferred\_lft forever
  18. 18. inet6 fe80::42:82ff:fe8d:a0df/64 scope link
  19. 19. valid\_lft forever preferred\_lft forever

可以看到容器内的网络环境与主机完全一致。

(4)container 网络模式

container 网络模式允许一个容器共享另一个容器的网络命名空间。当两个容器需要共享网络,但其他资源仍然需要隔离时就可以使用 container 网络模式,例如开发了一个 http 服务,但又想使用 nginx 的一些特性,让 nginx 代理外部的请求然后转发给自己的业务,这时使用 container 网络模式将自己开发的服务和 nginx 服务部署到同一个网络命名空间中。
下面举例说明。首先使用以下命令启动一个 busybox1 容器:
复制代码

  1. 1. $ docker run -d --name=busybox1 busybox sleep 3600

然后使用 docker exec 命令进入到 centos 容器中查看一下网络配置:

  1. 1. $ docker exec -it busybox1 sh
  2. 2. / # ifconfig
  3. 3. eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
  4. 4. inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
  5. 5. UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
  6. 6. RX packets:11 errors:0 dropped:0 overruns:0 frame:0
  7. 7. TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  8. 8. collisions:0 txqueuelen:0
  9. 9. RX bytes:906 (906.0 B) TX bytes:0 (0.0 B)
  10. 10.
  11. 11. lo Link encap:Local Loopback
  12. 12. inet addr:127.0.0.1 Mask:255.0.0.0
  13. 13. UP LOOPBACK RUNNING MTU:65536 Metric:1
  14. 14. RX packets:0 errors:0 dropped:0 overruns:0 frame:0
  15. 15. TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  16. 16. collisions:0 txqueuelen:1000
  17. 17. RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

可以看到 busybox1 的 IP 地址为 172.17.0.2。
然后新打开一个命令行窗口,再启动一个 busybox2 容器,通过 container 网络模式连接到 busybox1 的网络,命令如下:

  1. 1. $ docker run -it --net=container:busybox1 --name=busybox2 busybox sh
  2. 2. / #

在 busybox2 容器内同样使用 ifconfig 命令查看一下容器内的网络配置:

  1. 1. / # ifconfig
  2. 2. eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
  3. 3. inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
  4. 4. UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
  5. 5. RX packets:14 errors:0 dropped:0 overruns:0 frame:0
  6. 6. TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  7. 7. collisions:0 txqueuelen:0
  8. 8. RX bytes:1116 (1.0 KiB) TX bytes:0 (0.0 B)
  9. 9.
  10. 10. lo Link encap:Local Loopback
  11. 11. inet addr:127.0.0.1 Mask:255.0.0.0
  12. 12. UP LOOPBACK RUNNING MTU:65536 Metric:1
  13. 13. RX packets:0 errors:0 dropped:0 overruns:0 frame:0
  14. 14. TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  15. 15. collisions:0 txqueuelen:1000
  16. 16. RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

可以看到 busybox2 容器的网络 IP 也为 172.17.0.2,与 busybox1 的网络一致。

上面有说到 Libnetwork 的工作流程是完全围绕 CNM 的三个要素进行的,CNM 制定标准之初不仅仅是为了单台主机上的容器互通,更多的是为了定义跨主机之间的容器通信标准。但是后来由于 Kubernetes 逐渐成为了容器编排的标准,而 Kubernetes 最终选择了 CNI 作为容器网络的定义标准(具体原因可以参考这里),很遗憾 CNM 最终没有成为跨主机容器通信的标准,但是CNM 却为推动容器网络标准做出了重大贡献,且 Libnetwork 也是 Docker 的默认网络实现,提供了单独使用 Docker 容器时的多种网络接入功能。

为什么容器需要持久化存储

容器按照业务类型,总体可以分为两类:

  • 无状态的(数据不需要被持久化)
  • 有状态的(数据需要被持久化)

显然,容器更擅长无状态应用。因为未持久化数据的容器根目录的生命周期与容器的生命周期一样,容器文件系统的本质是在镜像层上面创建的读写层,运行中的容器对任何文件的修改都存在于该读写层,当容器被删除时,容器中的读写层也会随之消失。
虽然容器希望所有的业务都尽量保持无状态,这样容器就可以开箱即用,并且可以任意调度,但实际业务总是有各种需要数据持久化的场景,比如 MySQL、Kafka 等有状态的业务。因此为了解决有状态业务的需求,Docker 提出了卷(Volume)的概念。
什么是卷?卷的本质是文件或者目录,它可以绕过默认的联合文件系统,直接以文件或目录的形式存在于宿主机上。卷的概念不仅解决了数据持久化的问题,还解决了容器间共享数据的问题。使用卷可以将容器内的目录或文件持久化,当容器重启后保证数据不丢失,例如可以使用卷将 MySQL 的目录持久化,实现容器重启数据库数据不丢失。
Docker 提供了卷(Volume)的功能,使用docker volume命令可以实现对卷的创建、查看和删除等操作。下面我们来详细了解一下这些命令。

Docker 卷的操作

创建数据卷

使用docker volume create命令可以创建一个数据卷。
使用以下命令创建一个名为 myvolume 的数据卷:

  1. 1. $ docker volume create myvolume

在这里要说明下,默认情况下 ,Docker 创建的数据卷为 local 模式,仅能提供本主机的容器访问。如果想要实现远程访问,需要借助网络存储来实现。Docker 的 local 存储模式并未提供配额管理,因此在生产环境中需要手动维护磁盘存储空间。
除了使用docker volume create的方式创建卷,还可以在 Docker 启动时使用 -v 的方式指定容器内需要被持久化的路径,Docker 会自动创建卷,并且绑定到容器中,使用命令如下:

  1. 1. $ docker run -d --name=nginx-volume -v /usr/share/nginx/html nginx

使用以上命令,启动了一个 nginx 容器,-v参数使得 Docker 自动生成一个卷并且绑定到容器的 /usr/share/nginx/html 目录中。
可以使用docker volume ls命令来查看下主机上的卷:

  1. 1. $ docker volume ls
  2. 2. DRIVER VOLUME NAME
  3. 3. local eaa8a223eb61a2091bf5cd5247c1b28ac287450a086d6eee9632d9d1b9f69171

可以看到,Docker 自动为我们创建了一个名称为随机 ID 的卷。

查看数据卷

已经创建的数据卷可以使用 docker volume ls 命令查看。

  1. 1. $ docker volume ls
  2. 2. DRIVER VOLUME NAME
  3. 3. local myvolume

通过输出可以看到 myvolume 卷已经创建成功。
如果想要查看某个数据卷的详细信息,可以使用docker volume inspect命令。例如,我想查看 myvolume 的详细信息,命令如下:
复制代码

  1. 1. $ docker volume inspect myvolume
  2. 2. [
  3. 3. {
  4. 4. "CreatedAt": "2020-09-08T09:10:50Z",
  5. 5. "Driver": "local",
  6. 6. "Labels": {},
  7. 7. "Mountpoint": "/var/lib/docker/volumes/myvolume/_data",
  8. 8. "Name": "myvolume",
  9. 9. "Options": {},
  10. 10. "Scope": "local"
  11. 11. }
  12. 12. ]

通过docker volume inspect命令可以看到卷的创建日期、命令、挂载路径信息。

使用数据卷

使用docker volume创建的卷在容器启动时,添加 —mount 参数指定卷的名称即可使用。
这里使用上一步创建的卷来启动一个 nginx 容器,并将 /usr/share/nginx/html 目录与卷关联,命令如下:

  1. 1. $ docker run -d --name=nginx --mount source=myvolume,target=/usr/share/nginx/html nginx

使用 Docker 的卷可以实现指定目录的文件持久化,下面我们进入容器中并且修改 index.html 文件内容,命令如下:

  1. 1. $ docker exec -it nginx bash
  2. 2. ## 使用以下内容直接替换 /usr/share/nginx/html/index.html 文件
  3. 3. root@719d3c32e211:/# cat <<EOF >/usr/share/nginx/html/index.html
  4. 4. <!DOCTYPE html>
  5. 5. <html>
  6. 6. <head>
  7. 7. <title>Hello, Docker Volume!</title>
  8. 8. <style>
  9. 9. body {
  10. 10. width: 35em;
  11. 11. margin: 0 auto;
  12. 12. font-family: Tahoma, Verdana, Arial, sans-serif;
  13. 13. }
  14. 14. </style>
  15. 15. </head>
  16. 16. <body>
  17. 17. <h1>Hello, Docker Volume!</h1>
  18. 18. </body>
  19. 19. </html>
  20. 20. EOF

此时使用docker rm命令将运行中的 nginx 容器彻底删除。

  1. 1. $ docker rm -f nginx

旧的 nginx 容器删除后,再使用docker run命令启动一个新的容器,并且挂载 myvolume 卷,命令如下。

  1. 1. $ docker run -d --name=nginx --mount source=myvolume,target=/usr/share/nginx/html nginx

新容器启动后,进入容器查看一下 index.html 文件内容:

  1. 1. $ docker exec -it nginx bash
  2. 2. root@7ffac645f431:/# cat /usr/share/nginx/html/index.html
  3. 3. <!DOCTYPE html>
  4. 4. <html>
  5. 5. <head>
  6. 6. <title>Hello, Docker Volume!</title>
  7. 7. <style>
  8. 8. body {
  9. 9. width: 35em;
  10. 10. margin: 0 auto;
  11. 11. font-family: Tahoma, Verdana, Arial, sans-serif;
  12. 12. }
  13. 13. </style>
  14. 14. </head>
  15. 15. <body>
  16. 16. <h1>Hello, Docker Volume!</h1>
  17. 17. </body>
  18. 18. </html>

可以看到,此时 index.html 文件内容依旧是之前写入的内容。可见,使用 Docker 卷后我们的数据并没有随着容器的删除而消失。

删除数据卷

容器的删除并不会自动删除已经创建的数据卷,因此不再使用的数据卷需要我们手动删除,删除的命令为 docker volume rm 。例如,我们想要删除上面创建 myvolume 数据卷,可以使用以下命令:

  1. 1. $ docker volume rm myvolume

这里需要注意,正在被使用中的数据卷无法删除,如果你想要删除正在使用中的数据卷,需要先删除所有关联的容器。
有时候,两个容器之间会有共享数据的需求,很典型的一个场景就是容器内产生的日志需要一个专门的日志采集程序去采集日志内容,例如需要使用 Filebeat (一种日志采集工具)采集 nginx 容器内的日志,需要使用卷来共享一个日志目录,从而使得 Filebeat 和 nginx 容器都可以访问到这个目录,这时就需要用到容器之间共享数据卷的方式。

容器与容器之间数据共享

那如何实现容器与容器之间数据共享呢?下面我举例说明。
首先使用docker volume create命令创建一个共享日志的数据卷。

  1. 1. $ docker volume create log-vol

启动一个生产日志的容器(下面用 producer 窗口来表示):

  1. 1. $ docker run --mount source=log-vol,target=/tmp/log --name=log-producer -it busybox

然后新打开一个命令行窗口,启动一个消费者容器(下面用 consumer 窗口来表示):

  1. 1. docker run -it --name consumer --volumes-from log-producer busybox

使用volumes-from参数可以在启动新的容器时来挂载已经存在的容器的卷,volumes-from参数后面跟已经启动的容器名称。
下面我们切换到 producer 窗口,使用以下命令创建一个 mylog.log 文件并写入 “Hello,My log.” 的内容:

  1. 1. / # cat <<EOF >/tmp/log/mylog.log
  2. 2. Hello, My log.
  3. 3. EOF

然后我们切换到 consumer 窗口,查看一下相关内容:

  1. 1. / # cat /tmp/log/mylog.log
  2. 2. Hello, My log.

可以看到我们从 producer 容器写入的文件内容会自动出现在 consumer 容器中,证明我们成功实现了两个容器间的数据共享。
总结一下,首先使用 docker volume create 命令创建了 log-vol 卷来作为共享目录,log-producer 容器向该卷写入数据,consumer 容器从该卷读取数据。这就像主机上的两个进程,一个向主机目录写数据,一个从主机目录读数据,利用主机的目录,实现了容器之间的数据共享。

主机与容器之间数据共享

Docker 卷的目录默认在 /var/lib/docker 下,当我们想把主机的其他目录映射到容器内时,就需要用到主机与容器之间数据共享的方式了,例如把 MySQL 容器中的 /var/lib/mysql 目录映射到主机的 /var/lib/mysql 目录中,就可以使用主机与容器之间数据共享的方式来实现。
要实现主机与容器之间数据共享,其实很简单,只需要在启动容器的时候添加-v参数即可, 使用格式为:-v HOST_PATH:CONTIANAER_PATH
例如,想挂载主机的 /data 目录到容器中的 /usr/local/data 中,可以使用以下命令来启动容器:

  1. 1. $ docker run -v /data:/usr/local/data -it busybox

容器启动后,便可以在容器内的 /usr/local/data 访问到主机 /data 目录的内容了,并且容器重启后,/data 目录下的数据也不会丢失。

Docker 卷的实现原理

在了解 Docker 卷的原理之前,先来回顾一下镜像和容器的文件系统原理。

镜像和容器的文件系统原理: 镜像是由多层文件系统组成的,当想要启动一个容器时,Docker 会在镜像上层创建一个可读写层,容器中的文件都工作在这个读写层中,当容器删除时,与容器相关的工作文件将全部丢失。

Docker 容器的文件系统不是一个真正的文件系统,而是通过联合文件系统实现的一个伪文件系统,而 Docker 卷则是直接利用主机的某个文件或者目录,它可以绕过联合文件系统,直接挂载主机上的文件或目录到容器中,这就是它的工作原理。
下面,通过一个实例来说明卷的工作原理。首先,创建一个名称为 volume-data 的卷:

  1. 1. $ docker volume create volume-data

使用 ls 命令查看一下 /var/lib/docker/volumes 目录下的内容:

  1. 1. $ sudo ls -l /var/lib/docker/volumes
  2. 2. drwxr-xr-x. 3 root root 19 Sep 8 10:59 volume-data

然后再看下 volume-data 目录下有什么内容:

  1. 1. $ sudo ls -l /var/lib/docker/volumes/volume-data
  2. 2. total 0
  3. 3. drwxr-xr-x. 2 root root 6 Sep 8 10:59 _data

可以看到创建的卷出现在了 /var/lib/docker/volumes 目录下,并且 volume-data 目录下还创建了一个 _data 目录。
实际上,在创建 Docker 卷时,Docker 会把卷的数据全部放在 /var/lib/docker/volumes 目录下,并且在每个对应的卷的目录下创建一个 _data 目录,然后把 _data 目录绑定到容器中。因此在容器中挂载卷的目录下操作文件,实际上是在操作主机上的 _data 目录。为了证实,来实际演示下。
首先,启动一个容器,并且绑定 volume-data 卷到容器内的 /data 目录下:

  1. 1. $ docker run -it --mount source=volume-data,target=/data busybox
  2. 2. / #

我们进入到容器的 /data 目录,创建一个 data.log 文件:

  1. 1. / # cd data/
  2. 2. /data # touch data.log

然后新打开一个命令行窗口,查看一下主机上的文件内容:

  1. 1. $ sudo ls -l /var/lib/docker/volumes/volume-data/_data
  2. 2. total 0
  3. 3. -rw-r--r--. 1 root root 0 Sep 8 11:15 data.log

可以看到主机上的 _data 目录下也出现了 data.log 文件。这说明,在容器内操作卷挂载的目录就是直接操作主机上的 _data 目录,符合上面的说法。
综上,Docker 卷的实现原理是在主机的 /var/lib/docker/volumes 目录下,根据卷的名称创建相应的目录,然后在每个卷的目录下创建 _data 目录,在容器启动时如果使用 —mount 参数,Docker 会把主机上的目录直接映射到容器的指定目录下,实现数据持久化。