文章目录
- Docker 进阶实战:数据管理、网络
- 数据管理
- Volume
- 创建数据卷
- 挂载数据卷
- 共享数据卷
- 删除数据卷
- Bind mounts
- tmpfs mounts
- 网络
- 端口映射
- 容器互联
- Docker 内部网络
- Docker link
- Docker Networking
Docker 进阶实战:数据管理、网络
数据管理
默认情况下,容器中数据的读写发生在容器的存储层,当容器被删除时其上的数据将会丢失。为了实现数据的持久化存储,Docker 为我们提供了以下几种方案:
- Volumes
- Bind mounts
- tmpfs mounts
如下图,无论是哪种方式,其实对于容器来说它们都没有任何区别,都是存储在宿主机上的一个文件或者目录中,唯一的不同就是存储的位置或是文件系统、内存、Docker area。
Volume
卷(volume)是在一个或者多个容器内被选定的目录,可以绕过分层的联合文件系统(Union File System),为 Docker 提供持久数据或者共享数据,它具有以下特点:
- 对卷的修改会直接生效,并绕过镜像。
- 当提交或者创建镜像时,卷不被包含在镜像里。
- 卷可以在容器间共享。
- 容器停止,卷里的内容依旧存在。
创建数据卷
我们可以使用 docker volume create
命令来创建一个数据卷,例如:
docker volume create test
接着可以使用 docker volume inspect
命令查看该数据卷的信息:
[
{
"CreatedAt": "2022-07-31T22:04:29+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/test/_data",
"Name": "test",
"Options": {},
"Scope": "local"
}
]
Mountpoint 中的路径 /var/lib/docker/volumes/test/_data
就是卷在宿主机上存储数据的目录。
挂载数据卷
那我们该怎么为容器挂载数据卷呢?可以在启动/创建容器的时候加上 -v
参数来挂载数据卷,例如:
docker run -d -v test:/data --name webserver orekilee/demo:nginx
这里我们将数据卷 test 挂载到了容器的 /data 目录下。
需要注意的是,即使我们在上一步没有创建数据卷,这里 docker 也会自动的去帮我们创建。如果我们没有指定数据卷的名字,docker 就会随机生成一个 id 来作为代替。
共享数据卷
如果想要让几个容器之间共享数据,可以在启动容器的时候用 --volumes-from
选项指定想要共享数据卷的容器,例如:
docker run -d --volumes-from webserver --name webserver1 orekilee/demo:nginx
此时我们新创建的容器 webserver1 就可以共享 webserver 所使用的所有数据卷,并且它们挂载的目录都是相同的。
删除数据卷
由于数据卷是用来为容器持久化数据的,所以其生命周期独立于容器之外,即使我们将容器销毁,docker 也并不会将它删除,而由于 docker 并不具备垃圾回收机制,这些无用的数据卷就会浪费我们的空间。
因此当一个数据卷没用之后,可以使用 docker volume rm
命令将其删除。
docker volume rm test
Bind mounts
除了挂载 docker area 中的数据卷,我们也可以将宿主机上的目录挂载到容器中,例如:
docker run -d -v /data/test:/data --name webserver2 orekilee/demo:nginx
上面的命令将宿主机中的 /data/test
(必须使用绝对路径)目录作为一个卷挂载到容器中的 /data
。如果宿主机中不存在该目录,将会直接创建一个。容器可以通过访问 /data
来直接操作宿主机中 /data/test
中的数据。
tmpfs mounts
与前面提到的两种方式不同,tmpfs mounts 所挂载的数据是临时存储于宿主机内存中的,即非持久化存储。当容器停止时,其中的数据也会随之删除。
我们可以在启动容器时使用 --tmpfs
参数来将数据挂载到内存中,例如:
docker run -d --tmpfs /data --name webserver3 orekilee/demo:nginx
此时我们可以使用 docker inspect
命令来查看其 mount 信息:
"Mounts": [
{
"Type": "tmpfs",
"Source": "",
"Destination": "/app",
"Mode": "",
"RW": true,
"Propagation": ""
}
]
本节提到的三种方式都可以使用 –mount 选项来完成,其可以通过指定类型 type=[volume、bind、tmpfs] 以及宿主机目录 source=[宿主机目录] target=[容器挂载目录] 来完成,例如:
–mount type=bind,source=/data,target=/data
由于其语法冗余,本文就没有过多介绍,想要了解 –mount 与 -v、–tmpfs 区别的小伙伴可以自行查看 docker 官方文档。
网络
端口映射
当我们在容器中部署一些服务时(例如 MySQL、Tomcat、Nginx 等),如果想让外部也能够访问到这些应用,就需要在创建容器时加上 -p
或者 -P
(不指定端口,随机映射到一个开放的端口上)参数来指定端口映射:
# -p格式有以下三种 ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort
docker run -p 8080:80 -d --name webserver1 orekilee/demo:nginx
这里我们将本地宿主机的 8080 端口绑定到了容器的 80 端口上,此时我们可以通过浏览器访问宿主机的 8080 端口,就可以访问到 nginx 服务,如下图:
如果我们使用 -P
来随机映射,可以使用 docker port
命令来查看其对应的端口号:
# 使用-P随机映射一个端口
docker run -P -d --name webserver222 orekilee/demo:nginx
# 查看映射的端口号
docker port webserver222
80/tcp -> 0.0.0.0:49158
那么它是如何实现的呢?其实很简单,只是在本地 iptable 的 nat 表中添加了相应的规则,我们可以使用 iptables -t nat -nL 2
命令来查看刚刚配置的几个端口:
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.8:80
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.9:80
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:49158 to:172.17.0.10:80
容器互联
在 Docker 中,如果想要让各个容器之间通过内网进行网络连接,通常有以下三种方法:
- docker 内部网络
- Docker link
- Docker Networking
Docker 内部网络
docker 为了能够连接容器与本地宿主网络,构建了一个虚拟的以太网桥 docker0,它会为每一个 docker 容器分配一个 ip 地址,我们可以通过 ip a
命令来查看它的接口信息:
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:c9:f4:67:6d brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:c9ff:fef4:676d/64 scope link
valid_lft forever preferred_lft forever
5: veth6e15dc8@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether fa:c2:79:89:57:bb brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::f8c2:79ff:fe89:57bb/64 scope link
valid_lft forever preferred_lft forever
7: vethaeec631@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether a6:08:87:69:93:be brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::a408:87ff:fe69:93be/64 scope link
valid_lft forever preferred_lft forever
在这里我们发现当前所运行的容器都有一个对应的 veth 接口,那它有什么作用呢?
这里就先简单说明一下它的作用(原理在后续的章节会写),veth 其实是一个虚拟出来的网络接口,我们可以将其比作一个全双工的管道。管道的一端连接着 docker0,另一端则连接着我们的容器,两者可以通过这个管道来进行通信。通过为每一个容器创建一个这样的管道,docker 就构建出了一个虚拟的子网,这个子网由宿主机和所有的Docker容器共享。
内部网络作为 docker 默认的方案,虽然看起来不错,但是其有一个致命的问题:每当容器重启时,ip 地址就会重新分配,如果我们在服务中将 ip 写死,则会导致服务不可用,需要重新配置。
Docker link
在 docker 1.9 之前,我们通常使用 Docker link 来解决上面的问题。它的使用方法非常简单,只需要在启动容器的时候,加上一个 –link
的参数即可。
这里我两个容器为例子:
# 首先启动一个容器
docker run -d --name webserver1 orekilee/demo:nginx
# 接着启动第二个容器,将其link到第一个容器上容器上
docker run -d --name webserver2 --link webserver1 orekilee/demo:nginx
此时,我们进入 webserver2 中,直接通过 ping 容器名来尝试连接 webserver1:
PING webserver1 (172.17.0.8) 56(84) bytes of data.
64 bytes from webserver1 (172.17.0.8): icmp_seq=1 ttl=64 time=0.065 ms
64 bytes from webserver1 (172.17.0.8): icmp_seq=2 ttl=64 time=0.066 ms
64 bytes from webserver1 (172.17.0.8): icmp_seq=3 ttl=64 time=0.068 ms
那么它是如何做到的呢?首先我们查看一下 /etc/hosts
文件:
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 nginx1 91c559efadf8
172.17.0.3 d0e819e71e15
我们发现在 hosts 文件中,docker 直接为其加上了 ip 地址与容器名的映射。
接着使用 env
查看环境变量:
LANG=en_US.UTF-8
HOSTNAME=8f1d5dc8d58a
WEBSERVER1_PORT_80_TCP_PORT=80
WEBSERVER1_NAME=/webserver2/webserver1
which_declare=declare -f
PWD=/
HOME=/root
WEBSERVER1_PORT_80_TCP_ADDR=172.17.0.8
TERM=xterm
WEBSERVER1_PORT=tcp://172.17.0.8:80
WEBSERVER1_PORT_80_TCP=tcp://172.17.0.8:80
WEBSERVER1_PORT_80_TCP_PROTO=tcp
SHLVL=1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LESSOPEN=||/usr/bin/lesspipe.sh %s
BASH_FUNC_which%%=() { ( alias;
eval ${which_declare} ) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot "$@"
}
_=/usr/bin/env
在环境变量中,同样也写入了 webserver1 的连接信息。
随着容器的重启,这些网络信息也会自动进行更新。相比较于 docker 内部网络,我们在代码中不必去写死 ip 地址,而是可以直接使用容器名来进行通信。
除此之外,通过把容器 link 在一起,可以让客户容器直接访问任意服务容器的公开端口(只有通过 --link
才能连接到这个端口),容器的端口不需要对本地宿主机公开。使得我们可以限制容器化应用程序被攻击面,减少应用暴露的网络。
注意:Docker link 只能工作于同一台 Docker 宿主机中,不能链接位于不同 Docker 宿主机上的容器。
Docker Networking
在 docker 1.9 版本中推出了 Docker Networking,其允许用户创建自己的网络,容器可以通过这个网络互相通信,同时容器可以跨越不同的宿主机来通信,且网络配置可以更灵活地定制。
相比较于 Docker link,它有以下两个优点:
- Docker Networking 可以将容器连接到不同宿主机上的容器。
- 通过 Docker Networking 连接的容器可以在无需更新连接的情况下,对停止、启动或者重启容器。而使用 Docker link,则可能需要更新一些配置,或者重启相应的容器来维护 Docker 容器之间的链接。
- Docker Networking 不必事先创建容器再去连接它。同样,也不必关心容器的运行顺序。
除了默认的运行在单个主机上的 bridge 桥接网络,还有允许跨多台宿主机进行通信的 overlay 网络,只需要在创建时加上
-d overlay
参数即可。
接下来我们就看看它的用法,首先我们先创建一个桥接网络 test:
docker network create test
那我们该如何该如何让容器加入这个网络呢?只需要在创建容器的时候加上 --net
参数即可:
docker run -d --net=test --name webserver1 orekilee/demo:nginx
docker run -d --net=test --name webserver2 orekilee/demo:nginx
接着使用 docker network inspect
命令查看这个网络的信息:
[
{
"Name": "test",
"Id": "4480ede291b6db5ead834dfcda0c35a4130028694a0151b35b9d4108d8f0a2d2",
"Created": "2022-07-31T18:30:05.518031987+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"0e859754b01b674cc28b2a14e9ca25fd7bbbb27b5ef4f17b821408616561c8b9": {
"Name": "webserver1",
"EndpointID": "b4d3491f6abbe80c61f79588251b9c5ed36bae40238f6dd9a76d5ca91be40201",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
},
"28b8cf143b840eaea720dca4591804d22b983ca0e6a19a365ff16338226669fc": {
"Name": "webserver2",
"EndpointID": "18b08b0441674ec966640adfd1410e7a31cd28e59746604f23e816df6ca818b6",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
此时我们看到我们的容器已经加入该网络,并且其 ip 地址,端口号都被记录其中。
接着我们来到 webserver1 上,尝试去连接 webserver2:
PING webserver2 (172.18.0.3) 56(84) bytes of data.
64 bytes from webserver2.test (172.18.0.3): icmp_seq=1 ttl=64 time=0.071 ms
64 bytes from webserver2.test (172.18.0.3): icmp_seq=2 ttl=64 time=0.071 ms
64 bytes from webserver2.test (172.18.0.3): icmp_seq=3 ttl=64 time=0.068 ms
此时我们发现,它们已经能够达成互联,并且与 docker link 相同,它们的 hosts 信息也会随着重启而自动更新。
除了在创建的时候指定 network,我们能不能让已经运行的容器加入 network 呢?
这时就需要使用 docker network connect
来将运行中的容器加入到已有的网络:
docker run -d --name webserver3 orekilee/demo:nginx
docker network connect test webserver3
此时再查看 docker network inspect
,就可以发现容器已经加入:
[
{
"Name": "test",
"Id": "4480ede291b6db5ead834dfcda0c35a4130028694a0151b35b9d4108d8f0a2d2",
"Created": "2022-07-31T18:30:05.518031987+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"0e859754b01b674cc28b2a14e9ca25fd7bbbb27b5ef4f17b821408616561c8b9": {
"Name": "webserver1",
"EndpointID": "b4d3491f6abbe80c61f79588251b9c5ed36bae40238f6dd9a76d5ca91be40201",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
},
"28b8cf143b840eaea720dca4591804d22b983ca0e6a19a365ff16338226669fc": {
"Name": "webserver2",
"EndpointID": "18b08b0441674ec966640adfd1410e7a31cd28e59746604f23e816df6ca818b6",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
},
"bf0b77de8064434af64598a51ed62ca1915b47cc218ed3f354149b5a916b64d7": {
"Name": "webserver3",
"EndpointID": "3271ea7fb1b88ef9d2a80f284ac21f0408078b811ce0544e9770e2fbba14dd46",
"MacAddress": "02:42:ac:12:00:04",
"IPv4Address": "172.18.0.4/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
同理,我们可以使用 docker network disconnect
命令将容器从网络中删除。