《Docker 存储详解:从 Bind Mount 到 Data-packed Volume 实战》
Docker镜像
下面是镜像的常用操作子命令:
images 显示镜像列表
history 显示镜像构建历史
commit 从容器创建新镜像
build 从 Dockerfile 构建镜像
tag 给镜像打 tag
pull 从 registry 下载镜像
push 将 镜像 上传到 registry
rmi 删除 Docker host 中的镜像
search 搜索 Docker Hub 中的镜像
除了 rmi 和 search,其他命令都已经用过了。
rmi
rmi 只能删除 host 上的镜像,不会删除 registry 的镜像。
如果一个镜像对应了多个 tag,只有当最后一个 tag 被删除时,镜像才被真正删除。例如 host 中
busybox镜像有三个 tag:
[root@docker ~]# docker images busybox
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest 6fd955f66c23 16 months ago 4.26MB
[root@docker ~]# docker tag busybox:latest busybox:v1
[root@docker ~]# docker tag busybox:latest busybox:v2
[root@docker ~]# docker images busybox
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest 6fd955f66c23 16 months ago 4.26MB
busybox v1 6fd955f66c23 16 months ago 4.26MB
busybox v2 6fd955f66c23 16 months ago 4.26MB
保存本地镜像为文件-save
docker默认使用overlay2存储驱动存储镜像。
[root@docker ~]# docker info | grep 'Storage Driver'
Storage Driver: overlay2
镜像存储在本地/var/lib/docker/overlay2,通过文件系统层面拷贝image,操作复杂。可以使用save命令,将本地镜像保存为单个文件,并分享给他人使用。
[root@docker ~]# docker save --help
Usage: docker save [OPTIONS] IMAGE [IMAGE...]
Save one or more images to a tar archive (streamed to STDOUT by default)
Aliases:
docker image save, docker save
Options:
-o, --output string Write to a file, instead of STDOUT
示例:
[root@docker ~]# docker save httpd -o httpd.tar #httpd是镜像名 httpd.tar是
要保存的文件名
[root@docker ~]# docker save httpd hello-world -o images.tar #将httpd,helloworld镜像保存为images.tar
[root@docker ~]# ls
httpd.tar images.tar
将本地镜像文件导入本地-load
[root@docker ~]# docker load --help
Usage: docker load [OPTIONS]
Load an image from a tar archive or STDIN
Aliases:
docker image load, docker load
Options:
-i, --input string Read from tar archive file, instead of STDIN
-q, --quiet Suppress the load output
示例:删除本地镜像,并导入本地镜像文件
[root@docker ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
httpd latest 90f191b9781e 11 days ago 148MB
hello-world latest 74cc54e27dc4 6 months ago 10.1kB
[root@docker ~]# docker rmi httpd:latest hello-world:latest
Untagged: httpd:latest
Untagged:
httpd@sha256:f84fe51ff5d35124e024f51215b443b16c939b24eae747025a515200e71c7d07
Deleted: sha256:90f191b9781e01f5cd601af7b32d4ebb46770d1a98fa9170c328f9a78458b758
Deleted: sha256:8cbdaf91f3d9b2916bd85a36c608c8b652176b92266a6c51ead6c52f487b9eb5
Deleted: sha256:352b0ea3b9e318a2111200ed85540548a23873ce5ab5e6076b1dcef0110724ce
Deleted: sha256:7810a27010681e9307dae49c3673a021928add80b99e43937d79a3c5322cb862
Deleted: sha256:3c75e1b632ae15a9a14d7527fb134de5d45b2abcf3eef1cfaa057d702b957dba
Deleted: sha256:4eabd365f7baf2356d8d27adb4a182e45fca9dfc1d5533e93832c243135a5aa9
Deleted: sha256:1bb35e8b4de116e84b2ccf614cce4e309b6043bf2cd35543d8394edeaeb587e3
Untagged: hello-world:latest
Untagged: helloworld@sha256:ec153840d1e635ac434fab5e377081f17e0e15afab27beb3f726c3265039cfff
Deleted: sha256:74cc54e27dc41bb10dc4b2226072d469509f2f22f1a3ce74f4a59661a1d44602
Deleted: sha256:63a41026379f4391a306242eb0b9f26dc3550d863b7fdbb97d899f6eb89efe72
[root@docker ~]# docker images #本地没有镜像
REPOSITORY TAG IMAGE ID CREATED SIZE
删除本地正在使用的镜像需要选项-f
导入镜像
[root@docker ~]# docker load -i images.tar
1bb35e8b4de1: Loading layer 77.88MB/77.88MB
6c5ea8a96778: Loading layer 2.56kB/2.56kB
5f70bf18a086: Loading layer 1.024kB/1.024kB
785236f38f5b: Loading layer 11.41MB/11.41MB
5ca968893b91: Loading layer 62.99MB/62.99MB
b4558c4e936a: Loading layer 3.584kB/3.584kB
Loaded image: httpd:latest
63a41026379f: Loading layer 11.78kB/11.78kB
Loaded image: hello-world:latest
[root@docker ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
httpd latest 90f191b9781e 11 days ago 148MB
hello-world latest 74cc54e27dc4 6 months ago 10.1kB
说明:如果本地镜像名与导入的镜像重名,则本地的镜像会被覆盖。
[root@docker ~]# docker rm -f $(docker ps -aq) #删除所有容器
[root@docker ~]# docker rmi -f $(docker images -aq) #删除所有镜像
至此,Docker 镜像已经讨论完了,下节我们深入讨论容器。
容器
如何运行容器?
上一章我们学习了如何构建 Docker 镜像,并通过镜像运行容器。本章将深入讨论容器:学习容器的各种操作,容器各种状态之间如何转换,以及实现容器的底层技术。
运行容器
docker run=docker create + docker start
docker run 是启动容器的方法。在讨论 Dockerfile 时我们已经学习到,可用三种方式指定容器启动时
执行的命令:
- CMD 指令。
- ENTRYPOINT 指令。
- 在 docker run 命令行中指定。
例如下面的例子:
[root@docker ~]# docker create ubuntu #使用ubuntu镜像创建容器
eb1aa0ca86b2d49250ebe64913af50e88482ad68b9c3e61ef8f8da9c24b00f7a #新创建的容器长ID
[root@docker ~]# docker ps -a #create的容器状态时Created
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
eb1aa0ca86b2 ubuntu "/bin/bash" 2 seconds ago `Created`
quizzical_goldwasser
[root@docker ~]# docker start eb1aa0ca86b2 #启动容器,刚创建的容器ID
eb1aa0ca86b2
[root@docker ~]# docker ps -a #查看容器状态,启动了又退出了
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
eb1aa0ca86b2 ubuntu "/bin/bash" 23 seconds ago Exited (0) 3 seconds ago
quizzical_goldwasser
[root@docker ~]# docker run ubuntu pwd #使用ubuntu镜像创建容器并执行pwd命令
/
[root@docker ~]#
容器启动时执行 pwd ,返回的 / 是容器中的当前目录。 执行 docker ps 或 docker container ls
可以查看 Docker host 中当前运行的容器:
[root@docker ~]# docker ps #查看所有正在运行中的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
让容器长期运行
如何让容器保存运行呢?
因为容器的生命周期依赖于启动时执行的命令,只要该命令不结束,容器也就不会退出。
理解了这个原理,我们就可以通过执行一个长期运行的命令来保持容器的运行状态。例如执行下面的命令:
[root@docker ~]# docker run ubuntu /bin/bash -c "while true ; do sleep 1 ; echo
hahaha; done"
可见容器仍处于运行状态。不过这种方法有个缺点:它占用了一个终端。
我们可以加上参数 -d 以后台方式启动容器。
[root@docker ~]# docker run -d ubuntu /bin/bash -c "while true ; do sleep 1 ;
echo hahaha; done"
2a0bfa267fe146753b4fc8b23d55b08fbe3a5f9b5e093de6885133f6bbd20c56
[root@docker ~]#
容器启动后回到了 docker host 的终端。这里看到 docker 返回了一串字符,这是容器的 ID。通过
docker ps 查看容器:
现在我们有了两个正在运行的容器。这里注意一下容器的 CONTAINER ID 和 NAMES 这两个字段。
CONTAINER ID 是容器的 “短ID”,前面启动容器时返回的是 “长ID”。短ID是长ID的前12个字符。
NAMES 字段显示容器的名字,在启动容器时可以通过 --name 参数显示地为容器命名,如果不指定,
docker 会自动为容器分配名字。
对于容器的后续操作,我们需要通过 “长ID”、“短ID” 或者 “名称” 来指定要操作的容器。比如下面停止一个容器:
[root@docker ~]# docker stop 081be2bc2e1d
081be2bc2e1d
两种进入容器的方法
我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。有两种方法进入容
器:attach 和 exec。
docker attach
通过 docker attach 可以 attach 到容器启动命令的终端,例如:
[root@docker ~]# docker run -d ubuntu /bin/bash -c "while true ; do sleep 1 ;
echo I_am_in_container ; done"
dc508b94447f83b46080267580607569a187fcc7f780433f646e9d66949731a6
[root@docker ~]#
[root@docker ~]# docker attach
dc508b94447f83b46080267580607569a187fcc7f780433f646e9d66949731a6
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
这次我们通过 “长ID” attach 到了容器的启动命令终端,之后看到的是 echo 每隔一秒打印的信息。
注:可通过 Ctrl+p 然后 Ctrl+q 组合键退出 attach 终端。
docker exec
通过 docker exec 进入相同的容器:
说明如下:
① -it 以交互模式打开 pseudo-TTY,执行 bash,其结果就是打开了一个 bash 终端。
② 进入到容器中,容器的 hostname 就是其 “短ID”。
③ 可以像在普通 Linux 中一样执行命令。 ps -elf 显示了容器启动进程 while 以及当前的 bash 进
程。
④ 执行 exit 退出容器,回到 docker host。
docker exec -it bash|sh 是执行 exec 最常用的方式。
attach VS exec
attach 与 exec 主要区别如下:
- attach 直接进入容器 启动命令 的终端,不会启动新的进程。
- exec 则是在容器中打开新的终端,并且可以启动新的进程。
- 如果想直接在终端中查看启动命令的输出,用 attach;其他情况使用 exec。
当然,如果只是为了查看启动命令的输出,可以使用 docker logs 命令:
[root@docker ~]# docker logs -f dc508b94447f
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
-f 的作用与 tail -f 类似,能够持续打印输出。
下一节聊聊运行容器的最佳实践。
024 运行容器的最佳实践
按用途容器大致可分为两类:服务类容器和工具类的容器。
- 服务类容器以 daemon 的形式运行,对外提供服务。比如 web server,数据库等。通过 -d 以后
台方式启动这类容器是非常合适的。如果要排查问题,可以通过 exec -it 进入容器。
- 工具类容器通常给能我们提供一个临时的工作环境,通常以 run -it 方式运行,比如:
[root@docker ~]# docker run -it busybox
/ #
/ # wget www.baidu.com
Connecting to www.baidu.com (223.109.82.6:80)
saving to 'index.html'
index.html 100% |********************| 2381 0:00:00 ETA
'index.html' saved
/ #
/ # exit
[root@docker ~]#
运行 busybox, run -it 的作用是在容器启动后就直接进入。我们这里通过 wget 验证了在容器中访问
internet 的能力。执行 exit 退出终端,同时容器停止。
工具类容器多使用基础镜像,例如 busybox、debian、ubuntu 等。
容器运行小结
容器运行相关的知识点:
- 当 CMD 或 Entrypoint 或 docker run 命令行指定的命令运行结束时,容器停止。
- 通过 -d 参数在后台启动容器。
- 通过 exec -it 可进入容器并执行命令。
指定容器的三种方法:
- 短ID。
- 长ID。
- 容器名称。 可通过 --name 为容器命名。重命名容器可执行 docker rename 。
容器按用途可分为两类:
- 服务类的容器。
- 工具类的容器。
下一节讨论容器的其他操作,比如 stop, restart, pause, delete。
025 容器常用操作
前面讨论了如何运行容器,本节学习容器的其他常用操作。
stop/start/restart 容器
通过 docker stop 可以停止运行的容器。
容器在 docker host 中实际上是一个进程, docker stop 命令本质上是向该进程发送一个 SIGTERM 信
号。如果想快速停止容器,可使用 docker kill 命令,其作用是向容器进程发送 SIGKILL 信号。
docker start 会保留容器的第一次启动时的所有参数。
docker restart 可以重启容器,其作用就是依次执行 docker stop 和 docker start 。
容器可能会因某种错误而停止运行。对于服务类容器,我们通常希望在这种情况下容器能够自动重启。
启动容器时设置 --restart 就可以达到这个效果。
如果docker run -d httpd不加–restart=always参数,dock stop不会重启,attach进去ctrl_c(终止进
程)会chong’q。
–restart=always 意味着无论容器因何种原因退出(包括正常退出),就立即重启。该参数的形式还
可以是 --restart=on-failure:3 ,意思是如果启动进程退出代码非0,则重启容器,最多重启3次。
pause/unpause 容器
有时我们只是希望暂时让容器暂停工作一段时间,比如要对容器的文件系统打个快照,或者 dcoker host需要使用 CPU,这时可以执行 docker pause 。
一张图搞懂容器所有操作
前面我们已经讨论了容器的各种操作,对容器的生命周期有了大致的理解,下面这张状态机很好地总结了容器各种状态之间是如何转换的。
如果掌握了前面的知识,要看懂这张图应该不难。不过有两点还是需要补充一下:
- 可以先创建容器,稍后再启动。
退出包括正常退出或者非正常退出。这里举了两个例子:启动进程正常退出或发生 OOM,此时
docker 会根据 --restart 的策略判断是否需要重启容器。但如果容器是因为执行 docker stop或 docker kill 退出,则不会自动重启。
① docker create 创建的容器处于 Created 状态。
② docker start 将以后台方式启动容器。 docker run 命令实际上是 docker create 和
docker start 的组合。
- 只有当容器的启动进程 退出 时, --restart 才生效。
退出包括正常退出或者非正常退出。这里举了两个例子:启动进程正常退出或发生 OOM,此时
docker 会根据 --restart 的策略判断是否需要重启容器。但如果容器是因为执行 docker stop
或 docker kill 退出,则不会自动重启。
#基于httpd镜像创建容器httpd11,重启策略配置为always
[root@docker ~]# docker run -d --restart=always --name httpd11 httpd
7671267fabf6aafe49fe74256af8d0af07cd65e4af4222933385f3203bc0228e
#查看发现多了个httpd11容器
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
7671267fabf6 httpd "httpd-foreground" 3 seconds ago Up 2 seconds
80/tcp `httpd11`
27359e7cf87e httpd "httpd-foreground" About a minute ago Up About a
minute 80/tcp eager_northcutt
#登陆到容器httpd11,然后执行ctrl_c退出容器(会触发--restart=always参数)
[root@docker ~]# docker attach httpd11
^C[Mon Nov 17 07:48:31.582731 2025] [mpm_event:notice] [pid 1:tid 1] AH00491:
caught SIGTERM, shutting down
#发现httpd11重启了
[root@docker ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
7671267fabf6 httpd "httpd-foreground" 32 seconds ago Up 2 seconds
80/tcp httpd11
27359e7cf87e httpd "httpd-foreground" 2 minutes ago Up About a minute
80/tcp eager_northcutt
#管理员停止httpd11(不会触发重启)
[root@docker ~]# docker stop httpd11
httpd11
#发现httpd11没有c
[root@docker ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
7671267fabf6 httpd "httpd-foreground" 51 seconds ago Exited (0) 1
second ago httpd11
27359e7cf87e httpd "httpd-foreground" 2 minutes ago Up 2 minutes
80/tcp eager_northcutt
[root@docker ~]#
限制容器对内存的使用
cgroup简介
docker 通过 cgroup 来控制容器使用的资源配额,包括 CPU、内存、磁盘三大方面,基本覆盖了常见的资源配额和使用量控制。
cgroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如 cpu、memory、磁盘IO等等) 的机制,被 LXC、docker 等很多项目用于实现进程资源控制。
cgroup 将任意进程进行分组化管理的 Linux 内核功能。cgroup 本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O 或内存的分配控制等具体的资源管理功能是通过这个功能来实现的。这些具体
的资源管理功能称为 cgroup 子系统,有以下几大子系统实现:
blkio:设置限制每个块设备的输入输出控制。例如:磁盘,光盘以及 usb 等等。
cpu:使用调度程序为 cgroup 任务提供 cpu 的访问。
cpuacct:产生 cgroup 任务的 cpu 资源报告。
cpuset:如果是多核心的 cpu,这个子系统会为 cgroup 任务分配单独的 cpu 和内存。
devices:允许或拒绝 cgroup 任务对设备的访问。
freezer:暂停和恢复 cgroup 任务。
memory:设置每个 cgroup 的内存限制以及产生内存资源报告。
net_cls:标记每个网络包以供 cgroup 方便使用。
ns:命名空间子系统。
perf_event:增加了对每 group 的监测跟踪的能力,可以监测属于某个特定的 group 的所有线程
以及运行在特定CPU上的线程。
stress是什么?
是模拟压力测试的工具在机器上模拟cpu、内存等使用率来检测不同状态下的运行情况
目前 docker 只是用了其中一部分子系统,实现对资源配额和使用的控制。
可以使用 stress 工具来测试 CPU 和内存。使用下面的 Dockerfile 来创建一个基于 Ubuntu 的 stress 工具镜像。
Dockerfile
[root@docker dockerfile]# vim Dockerfile
# Version 1
FROM ubuntu
MAINTAINER gaoqd "6946630@qq.com"
RUN apt-get -y update && apt-get -y install stress
ENTRYPOINT ["/usr/bin/stress"] # 以服务或进程的形式运行
使用Dockerfile构建镜像ubuntu-with-stress
[root@docker ~]# docker build -t ubuntu-with-stress .
一个 docker host 上会运行若干容器,每个容器都需要 CPU、内存和 IO 资源。对于 KVM,VMware 等虚拟化技术,用户可以控制分配多少 CPU、内存资源给每个虚拟机。对于容器,Docker 也提供了类似的机制避免某个容器因占用太多资源而影响其他容器乃至整个 host 的性能。
内存限额
与操作系统类似,容器可使用的内存包括两部分:物理内存和 swap。 Docker 通过下面两组参数来控制
容器内存的使用量。
- -m 或 --memory :设置内存的使用限额,例如 100M, 2G。
- –memory-swap :设置 内存+swap的使用限额。
当我们执行如下命令:
docker run -m 200M --memory-swap=300M ubuntu
其含义是允许该容器最多使用 200M 的内存和 100M 的 swap。
正常情况下,–memory-swap 的值包含容器可用内存和可用swap。所以 -m 300m --memory-swap=1g的含义为:容器可用使用300M的物理内存,并且可以使用700M(1G-300)的swap
如果–memory-swap 设置为0 或者不设置,则容器可以使用的swap大小为-m值的两倍。
如果 --memory-swap 的值和-m 值相同,则容器不能使用swap
如果 --memory-swap值为-1。它表示容器程序使用的内存受限,而可以使用的swap空间不受限制
(宿主机有多少swap空间该容器就可以使用多少)
下面我们将使用ubuntu-with-stress镜像来学习如何为容器分配内存。该镜像可用于对容器执行压力测试。执行如下命令:
[root@docker ~]# docker run -it -m 200M --memory-swap=300M ubuntu-with-stress --
vm 1 --vm-bytes 280M -v
–vm 1 :启动 1 个内存工作线程。
–vm-bytes 280M :每个线程分配 280M 内存。
因为 280M 在可分配的范围(300M)内,所以工作线程能够正常工作,其过程是:
- 分配 280M 内存。
- 释放 280M 内存。
- 再分配 280M 内存。
- 再释放 280M 内存。
- 一直循环…
限制容器对CPU的使用
上节学习了如何限制容器对内存的使用,本节我们来看CPU。
默认设置下,所有容器可以平等地使用 host CPU 资源并且没有限制。
Docker 可以通过 -c 或 --cpu-shares 设置容器使用 CPU 的权重。如果不指定,默认值为 1024。
–cpu-shares 的值不能保证可以获得1个 vcpu 或者多少 GHz 的 CPU 资源,仅仅只是一个弹性的加权
值。
默认情况下,每个 docker 容器的 cpu 份额都是1024。单独一个容器的份额是没有意义的,只有在同时运行多个容器时,容器的 CPU 加权的效果才能体现出来。例如,两个容器A、B的 CPU 份额分别为1000和500,在 CPU 进行时间片分配的时候,容器 A 比容器 B 多一倍的机会获得 CPU 的时间片,但分配的
结果取决于当时主机和其他容器的运行状态,实际上也无法保证容器A一定能获得 CPU 时间片。比如容器A的进程一直是空闲的,那么容器B是可以获取比容器A更多的 CPU 时间片的。极端情况下,比如说主机上只运行了一个容器,即使它的 CPU 份额只有 50,它也可以独占整个主机的 CPU 资源。
cgroups 只在容器分配的资源紧缺时,也就是说在需要对容器使用的资源进行限制时,才会生效。因
此,无法单纯根据某个容器的 CPU 份额来确定有多少 CPU 资源分配给它,资源分配结果取决于同时运行的其他容器的 CPU 分配和容器中进程运行情况。
换句话说:通过 cpu share 可以设置容器使用 CPU 的优先级。
比如在 host 中启动了两个容器:
docker run --name “container_A” -c 1024 ubuntu
docker run --name “container_B” -c 512 ubuntu
container_A 的 cpu share 1024,是 container_B 的两倍。当两个容器都需要 CPU 资源时,
container_A 可以得到的 CPU 是 container_B 的两倍。
需要特别注意的是,这种按权重分配 CPU 只会发生在 CPU 资源紧张的情况下。如果 container_A 处于空闲状态,这时,为了充分利用 CPU 资源,container_B 也可以分配到全部·可用的 CPU。
下面我们继续用 ubuntu-with-stress 做实验。
- 启动 container_A,cpu share 为 1024:
[root@docker ~]# docker run --name "container_A" -it -c 1024 ubuntu-withstress --cpu 4 -v
stress: info: [1] dispatching hogs: 4 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 12000us
stress: dbug: [1] --> hogcpu worker 4 [7] forked
stress: dbug: [1] using backoff sleep of 9000us
stress: dbug: [1] --> hogcpu worker 3 [8] forked
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [9] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [10] forked
#再开一个窗口
#使用如下命令,创建容器,则最终生成的 cgroup 的 CPU 份额配置可以下面的文件中找到。
[root@docker ~]# cat /sys/fs/cgroup/cpu/docker/<容器长ID>/cpu.shares
1024
–cpu 用来设置工作线程的数量。因为当前 host 有 4颗 CPU,所以要4个工作线程才能将 CPU 压
满。如果 host 有多颗 CPU,则需要相应增加 --cpu 的数量。
- 再开一个窗口,启动 container_B,cpu share 为 512:
[root@docker ~]# docker run --name "container_B" -it -c 512 ubuntu-withstress --cpu 4 -v
stress: info: [1] dispatching hogs: 4 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 12000us
stress: dbug: [1] --> hogcpu worker 4 [7] forked
stress: dbug: [1] using backoff sleep of 9000us
stress: dbug: [1] --> hogcpu worker 3 [8] forked
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [9] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [10] forked
[root@docker ~]# cat /sys/fs/cgroup/cpu/docker/<容器长ID>/cpu.shares
512
- 在 host 中执行 top ,查看容器对 CPU 的使用情况:
因为我们host是4核所以开启了4个进程,为的就是充分让系统资源变得紧张,只有这样竞争资源,
我们设定的资源比例才可以显现出来,如果只运行一个进程,他们会自动分配到空闲的CPU,这样
比例就无法看出来。目前可以看到总比例是2:1。
container_A 消耗的 CPU 是 container_B 的两倍。
再开一个窗口
[root@docker docker]# docker stats
- 现在暂停 container_A:
[root@docker ~]# docker pause container_A
container_A
- top 显示 container_B 在 container_A 空闲的情况下能够用满整颗 CPU:
export和import容器
**export-**容器导出
将容器导出为一个tar包
[root@docker ~]# docker export --help
Usage: docker export [OPTIONS] CONTAINER
Export a container's filesystem as a tar archive
Aliases:
docker container export, docker export
Options:
-o, --output string Write to a file, instead of STDOUT
不管此时这个容器是否处于运行状态,都可以导出为文件。
示例:
# 创建容器httpd1用于测试
[root@docker ~]# docker run -d --name httpd1 httpd
e4f0a329c4df50ef0afb0bf21e22edc20e5a24c03ae64c6340aa2992d6e32525
[root@docker ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
e4f0a329c4df httpd "httpd-foreground" 3 minutes ago Up 3 minutes
80/tcp httpd1
[root@docker ~]# docker export httpd1 -o myhttpd.tar
[root@docker ~]# ls
myhttpd.tar
**import-**容器tar导入
import将export导出的tar包导入成为镜像
[root@docker ~]# docker import --help
Usage: docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]
Import the contents from a tarball to create a filesystem image
Aliases:
docker image import, docker import
Options:
-c, --change list Apply Dockerfile instruction to the created image
-m, --message string Set commit message for imported image
--platform string Set platform if server is multi-platform capable
示例:
[root@docker ~]# docker import myhttpd.tar
sha256:9ae4699fa217a7240c73a50e93a8c1d359d22cf60869d350c8aab8b3c02aabcf
[root@docker ~]# docker import myhttpd.tar myweb:v1
sha256:9fcb90561d2014130ef19f9dd8af5efd7a0a4a2154907bc818ac6b887f13755c
[root@docker ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myweb v1 9fcb90561d20 3 seconds ago 146MB
<none> <none> 9ae4699fa217 32 seconds ago 146MB
httpd latest 90f191b9781e 11 days ago 148MB
hello-world latest 74cc54e27dc4 6 months ago 10.1kB
[root@docker ~]#
docker save 和 docker export 对比:
-
docker save :将镜像保存为文件,save会保存该镜像的所有元数据和历史记录。
-
docker export :将容器导出为文件,文件会丢失所有元数据和历史记录,仅保存容器当时的状
态,再次导入会当作全新的镜像。
思考: docker export 导出的文件是否可以使用 docker load 导入?
[root@docker ~]# docker load -i myhttpd.tar
open /var/lib/docker/tmp/docker-import-3425397234/boot/json: no such file or
directory
实现容器的底层技术
为了更好地理解容器的特性,本节我们将讨论容器的底层实现技术。
cgroup 和 namespace 是最重要的两种技术。cgroup 实现资源限额, namespace 实现资源隔离。
cgroup
cgroup 全称 Control Group。Linux 操作系统通过 cgroup 可以设置进程使用 CPU、内存 和 IO 资源的
限额。相信你已经猜到了:前面我们看到的 --cpu-shares 、 -m 、 --device-write-bps 实际上就是
在配置 cgroup。
cgroup 到底长什么样子呢?我们可以在 /sys/fs/cgroup 中找到它。还是用例子来说明,启动一个容器,
设置 --cpu-shares=512 :
[root@docker docker]# docker run -it --cpu-shares 512 ubuntu-with-stress -c 1 -v
stress: info: [1] dispatching hogs: 1 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [7] forked
namespace
namespace 管理着 host 中全局唯一的资源,并可以让每个容器都觉得只有自己在使用它。Linux 使用了六种 namespace:Mount、UTS、IPC、PID、Network 和 User。
网络
none和host网络的适用场景
Docker安装时会自动在host上创建三个网络,可用docker network ls命令查看。
none网络
挂在none网络上的容器只有lo,无法与外界通信。适用于对安全性要求高且不需要联网的应用。
[root@docker ~]# docker run -it --network=none busybox
/ #
/ # ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
/ #
/ # hostname
4027da12afaf
host 网络
挂在host网络上的容器共享宿主机的network namespace。即容器的网络配置与host网络配置完全一
样。
[root@docker ~]# docker run -it --network=host busybox #容器网络与宿主机相同
/ #
/ # ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
link/ether 00:0c:29:33:15:f6 brd ff:ff:ff:ff:ff:ff
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue
link/ether 02:42:02:fd:82:b3 brd ff:ff:ff:ff:ff:ff
/ #
/ # hostname #容器主机与宿主机相同
docker
/ #
学容器必须懂brige网络
bridge网络是应用最广泛也是默认的网络。Docker安装时会创建一个命名为docker0的linux bridge。
容器创建时,docker会创建一对veth pair接口,一端在容器内即eth0,另一端在本地并被挂载到docker0网桥。
下面演示:
先配置yum源用于安装软件
[root@docker ~]# cd /etc/yum.repos.d/
[root@docker yum.repos.d]# vim cloud.repo
[centos-openstack-victoria]
name=CentOS 8 - OpenStack victoria
baseurl=https://mirrors.aliyun.com/centos-vault/8-stream/cloud/x86_64/openstackvictoria/
enabled=1
gpgcheck=0
[root@docker yum.repos.d]# yum install -y bridge-utils
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02420be905a5 no
当前 docker0 上没有任何其他网络设备,我们创建一个容器看看有什么变化。
[root@docker ~]# docker run -itd --name busybox1 busybox
5225d246f751dbfc4cdf745675d9c79d3de1b91dd4174268c35e671b9f1b0c80
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02420be905a5 no vethddb2744
一个新的网络接口 vethddb2744 被挂到了 docker0 上, vethddb2744 就是新创建容器的虚拟网卡。下面看一下容器的网络配置。
[root@docker ~]# docker exec -it busybox1 sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
# 容器里的网卡是24号网卡名字叫eth0,对面是25号网卡
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
我们还看到 eth0@if25 已经配置了 IP 172.17.0.2 ,为什么是这个网段呢?让我们通过 docker network inspect bridge 看一下 bridge 网络的配置信息:
[root@docker ~]# docker network inspect bridge
[
{
"Name": "bridge",
"Id": "9c8f4ff0c361c169536a912736d7a305b93abd9f3acc69fee066233260930a69",
"Created": "2025-07-31T13:31:35.155868533+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16", `#这是bridge网络分配的网
段`
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"5225d246f751dbfc4cdf745675d9c79d3de1b91dd4174268c35e671b9f1b0c80": {
"Name": "busybox1", `#容器busybox1`
"EndpointID":
"ecc590ff010723831b97c0439ef9a9ca13fda695185d846d25662caecb066b7f",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16", `分配给busybox1的
172.17.0.2`
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
原来 bridge 网络配置的 subnet 就是 172.17.0.0/16,并且网关是 172.17.0.1。这个网关在哪儿呢?大概你已经猜出来了,就是 docker0
[root@docker ~]# ip a | grep docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
group default
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
如何自定义容器网络?
除了 none, host, bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络。
Docker 提供三种 user-defined 网络驱动:bridge, overlay 和 macvlan。overlay 和 macvlan 用于创建跨主机的网络。
我们可通过 bridge 驱动创建类似前面默认的 bridge 网络,例如:
[root@docker ~]# docker network create --driver bridge my_net
89f7bc11b602e84452ae01786113ac196a535f7296b9989ad941b3a48a5e6d04
查看一下当前 host 的网络结构变化
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no
#my_net,看bridge name和刚才创建的my_net id一样
docker0 8000.02420be905a5 no vethddb2744
新增了一个网桥 br-89f7bc11b602 ,这里 br-89f7bc11b602 正好是新建 bridge 网络 my_net 的短id。执行 docker network inspect 查看一下 my_net 的配置信息:
[root@docker ~]# docker network ls
[root@docker ~]# docker network inspect my_net
[
{
"Name": "my_net",
"Id": "89f7bc11b602e84452ae01786113ac196a535f7296b9989ad941b3a48a5e6d04",
"Created": "2025-07-31T15:57:17.487644996+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16", `#这里 172.18.0.0/16 是 Docker
自动分配的 IP 网段。``
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
这里 172.18.0.0/16 是 Docker 自动分配的 IP 网段。
我们可以自己指定 IP 网段吗?
答案是:可以。
只需在创建网段时指定 --subnet 和 --gateway 参数:
[root@docker ~]# docker network create --driver bridge --subnet 172.22.16.0/24 --
gateway 172.22.16.1 my_net2
ec761bc51778f67c2245af72fd969f00cd517ee617a10a70dc01904f9c10279d
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no #my_net
br-ec761bc51778 8000.02427fa54b88 no #my_net2
docker0 8000.02420be905a5 no vethddb2744
[root@docker ~]# docker network inspect my_net2
[
{
"Name": "my_net2",
"Id": "ec761bc51778f67c2245af72fd969f00cd517ee617a10a70dc01904f9c10279d",
"Created": "2025-07-31T16:00:06.021244096+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.22.16.0/24", #确实是指定的网段
"Gateway": "172.22.16.1" #确实是指定的网关
地址
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
这里我们创建了新的 bridge 网络 my_net2 ,网段为 172.22.16.0/24,网关为 172.22.16.1。与前面一样,网关在 my_net2 对应的网桥 br-ec761bc51778 上:
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no #my_net
br-ec761bc51778 8000.02427fa54b88 no #my_net2
docker0 8000.02420be905a5 no vethddb2744
#同时宿主机上出现了与网桥my_net,my_net2同名的网卡
#查看my_net2网卡
[root@docker ~]# ip a | grep br-ec761bc51778
27: br-ec761bc51778: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue
state DOWN group default
inet 172.22.16.1/24 brd 172.22.16.255 scope global br-ec761bc51778
容器要使用新的网络,需要在启动时通过 --network 指定:
[root@docker ~]# docker run -it --network=my_net2 --name busybox2 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
28: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:16:10:02 brd ff:ff:ff:ff:ff:ff
inet 172.22.16.2/24 brd 172.22.16.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ctrl+P,ctrl+q退出容器
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no
br-ec761bc51778 8000.02427fa54b88 no veth02dd704
#my_net2 上新增的接口veth02dd704连接容器busybox2
docker0 8000.02420be905a5 no vethddb2744
容器分配到的 IP 为 172.22.16.2。
到目前为止,容器的 IP 都是 docker 自动从 subnet 中分配,我们能否指定一个静态 IP 呢?
答案是:可以,通过 --ip 指定。
[root@docker ~]# docker run -it --network=my_net2 --ip 172.22.16.8 --name
busybox3 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
30: eth0@if31: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:16:10:08 brd ff:ff:ff:ff:ff:ff
inet 172.22.16.8/24 brd 172.22.16.255 scope global eth0 #确实是指定的
IP
valid_lft forever preferred_lft forever
/ # ctrl+P,ctrl+q退出容器
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no #my_net
br-ec761bc51778 8000.02427fa54b88 no veth02dd704
#my_net2
veth5fbb7f6
#veth5fbb7f6是连接busybox3
docker0 8000.02420be905a5 no vethddb2744
注:只有使用 --subnet 创建的网络才能指定静态 IP。
my_net 创建时没有指定 --subnet ,如果指定静态 IP 报错如下:
[root@docker ~]# docker run -it --network=my_net --ip 172.18.0.8 busybox
docker: Error response from daemon: invalid config for network my_net: invalid
endpoint settings:
user specified IP address is supported only when connecting to networks with user
configured subnets.
See 'docker run --help'.
理解容器之间的连通性
busybox2、busybox3 容器都挂在 my_net2 上,应该能够互通,我们验证一下:
# 登陆busybox2 ping busybox3
[root@docker ~]# docker exec -it busybox2 sh
/ # ifconfig eth0
eth0 Link encap:Ethernet HWaddr 02:42:AC:16:10:02
inet addr:172.22.16.2 Bcast:172.22.16.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:18 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1436 (1.4 KiB) TX bytes:0 (0.0 B)
/ # ping -c 3 172.22.16.8
PING 172.22.16.8 (172.22.16.8): 56 data bytes
64 bytes from 172.22.16.8: seq=0 ttl=64 time=0.197 ms
64 bytes from 172.22.16.8: seq=1 ttl=64 time=0.137 ms
64 bytes from 172.22.16.8: seq=2 ttl=64 time=0.158 ms
--- 172.22.16.8 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.137/0.164/0.197 ms
/ #
# ping my_net2网关地址
/ # ping -c 3 172.22.16.1
PING 172.22.16.1 (172.22.16.1): 56 data bytes
64 bytes from 172.22.16.1: seq=0 ttl=64 time=0.149 ms
64 bytes from 172.22.16.1: seq=1 ttl=64 time=0.116 ms
64 bytes from 172.22.16.1: seq=2 ttl=64 time=0.182 ms
--- 172.22.16.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.116/0.149/0.182 ms
可见同一网络中的容器、网关之间都是可以通信的。
my_net2 与默认 bridge 网络(docker0)能通信吗?
从拓扑图可知,两个网络属于不同的网桥,应该不能通信,我们通过实验验证一下,让 busybox2 容器
ping buxybox1 容器:
/ # ping -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
--- 172.17.0.2 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss
/ #
确实 ping 不通,符合预期。
“等等!不同的网络如果加上路由应该就可以通信了吧?”
这是一个非常非常好的想法。
确实,如果 host 上对每个网络的都有一条路由,同时操作系统上打开了 ip forwarding,host 就成了一
个路由器,挂接在不同网桥上的网络就能够相互通信。下面我们来看看 docker host 满不满足这些条件呢?ip r 查看 host 上的路由表:
[root@docker ~]# ip r
default via 192.168.108.2 dev ens160 proto static metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
172.18.0.0/16 dev br-a6eff42dc3c6 proto kernel scope link src 172.18.0.1 linkdown
172.22.16.0/24 dev br-c0fc7bdf8143 proto kernel scope link src 172.22.16.1
192.168.108.0/24 dev ens160 proto kernel scope link src 192.168.108.30 metric 100
[root@docker ~]#
172.17.0.0/16 和 172.22.16.0/24 两个网络的路由都定义好了。再看看 ip forwarding:
[root@docker ~]# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
ip forwarding 也已经启用了。
条件都满足,为什么不能通行呢?
我们还得看看 iptables:
[root@docker ~]# iptables-save
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*filter
:INPUT ACCEPT [69184:9702316]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [113709:49882234]
:DOCKER - [0:0]
:DOCKER-ISOLATION-STAGE-1 - [0:0]
:DOCKER-ISOLATION-STAGE-2 - [0:0]
:DOCKER-USER - [0:0]
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o br-ec761bc51778 -m conntrack --ctstate RELATED,ESTABLISHED -j
ACCEPT
-A FORWARD -o br-ec761bc51778 -j DOCKER
-A FORWARD -i br-ec761bc51778 ! -o br-ec761bc51778 -j ACCEPT
-A FORWARD -i br-ec761bc51778 -o br-ec761bc51778 -j ACCEPT
-A FORWARD -o br-89f7bc11b602 -m conntrack --ctstate RELATED,ESTABLISHED -j
ACCEPT
-A FORWARD -o br-89f7bc11b602 -j DOCKER
-A FORWARD -i br-89f7bc11b602 ! -o br-89f7bc11b602 -j ACCEPT
-A FORWARD -i br-89f7bc11b602 -o br-89f7bc11b602 -j ACCEPT
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i br-ec761bc51778 ! -o br-ec761bc51778 -j DOCKERISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-89f7bc11b602 ! -o br-89f7bc11b602 -j DOCKERISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o br-ec761bc51778 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-89f7bc11b602 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
COMMIT
# Completed on Thu Jul 31 17:09:34 2025
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*security
:INPUT ACCEPT [69166:9698194]
:FORWARD ACCEPT [34:2856]
:OUTPUT ACCEPT [113709:49882234]
COMMIT
# Completed on Thu Jul 31 17:09:34 2025
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*raw
:PREROUTING ACCEPT [69554:9733396]
:OUTPUT ACCEPT [113709:49882234]
COMMIT
# Completed on Thu Jul 31 17:09:34 2025
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*mangle
:PREROUTING ACCEPT [69554:9733396]
:INPUT ACCEPT [69184:9702316]
:FORWARD ACCEPT [370:31080]
:OUTPUT ACCEPT [113709:49882234]
:POSTROUTING ACCEPT [113743:49885090]
COMMIT
# Completed on Thu Jul 31 17:09:34 2025
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*nat
:PREROUTING ACCEPT [367:33182]
:INPUT ACCEPT [9:500]
:POSTROUTING ACCEPT [294:23341]
:OUTPUT ACCEPT [291:23089]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.22.16.0/24 ! -o br-ec761bc51778 -j MASQUERADE
-A POSTROUTING -s 172.18.0.0/16 ! -o br-89f7bc11b602 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i br-ec761bc51778 -j RETURN
-A DOCKER -i br-89f7bc11b602 -j RETURN
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Thu Jul 31 17:09:34 2025
原因就在这里了:iptables DROP 掉了网桥 docker0 与 br-ec761bc51778(my_net2) 之间双向的流量。
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no
br-ec761bc51778(mynet2) 8000.02427fa54b88 no veth02dd704
veth5fbb7f6
docker0 8000.02420be905a5 no vethddb2744
容器中增加了一个网卡 eth1,分配了 my_net2 的 IP 172.22.16.3。现在 busybox2 应该能够访问 busybox1 了,验证一下:
[root@docker ~]# docker exec -it busybox2 sh
/ # ping -c 3 172.22.16.3
PING 172.22.16.3 (172.22.16.3): 56 data bytes
64 bytes from 172.22.16.3: seq=0 ttl=64 time=0.199 ms
64 bytes from 172.22.16.3: seq=1 ttl=64 time=0.134 ms
64 bytes from 172.22.16.3: seq=2 ttl=64 time=0.141 ms
--- 172.22.16.3 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.134/0.158/0.199 ms
/ #
容器通信的三种方式
容器之间可通过 IP,Docker DNS Server 或 joined 容器三种方式通信。
IP 通信
从上一节的例子可以得出这样一个结论:两个容器要能通信,必须要有属于同一个网络的网卡。
满足这个条件后,容器就可以通过 IP 交互了。具体做法是在容器创建时通过 --network 指定相应的网
络,或者通过 docker network connect 将现有容器加入到指定网络。可参考上一节 busybox 的例
子,这里不再赘述。
Docker DNS Server
通过 IP 访问容器虽然满足了通信的需求,但还是不够灵活。因为我们在部署应用之前可能无法确定 IP,部署之后再指定要访问的 IP 会比较麻烦。对于这个问题,可以通过 docker 自带的 DNS 服务解决。从 Docker 1.10 版本开始,docker daemon 实现了一个内嵌的 DNS server,使容器可以直接通过“容器名”通信。方法很简单,只要在启动时用 --name 为容器命名就可以了。
下面启动两个容器 bbox1 和 bbox2:
[root@docker ~]# docker run -it --network my_net2 --name bbox1 busybox
/ #
ctrl_p,ctrl_q退出容器
[root@docker ~]# docker run -it --network my_net2 --name bbox2 busybox
/ #
ctrl_p,ctrl_q退出容器
然后,bbox2 就可以直接 ping 到 bbox1 了:
[root@docker ~]# docker exec -it bbox2 sh
/ #
/ # ping -c 3 bbox1
PING bbox1 (172.22.16.2): 56 data bytes
64 bytes from 172.22.16.2: seq=0 ttl=64 time=0.177 ms
64 bytes from 172.22.16.2: seq=1 ttl=64 time=0.161 ms
64 bytes from 172.22.16.2: seq=2 ttl=64 time=0.187 ms
--- bbox1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.161/0.175/0.187 ms
使用 docker DNS 有个限制:只能在 user-defined 网络中使用。也就是说,默认的 bridge 网络是无法使用 DNS 的。下面验证一下:
创建 bbox3 和 bbox4,均连接到 bridge 网络
[root@docker ~]# docker run -it --name bbox3 busybox
/ #
ctrl_p,ctrl_q退出容器
[root@docker ~]# docker run -it --name bbox4 busybox
/ #
ctrl_p,ctrl_q退出容器
bbox4 无法 ping 到 bbox3。
[root@docker ~]# docker exec -it bbox4 sh
/ # ping -c 3 bbox3
ping: bad address 'bbox3'
/ #
joined 容器
joined 容器是另一种实现容器间通信的方式。
joined 容器非常特别,它可以使两个或多个容器共享一个网络栈,共享网卡和配置信息,joined 容器之间可以通过 127.0.0.1 直接通信。请看下面的例子:
先创建一个 httpd 容器,名字为 web1。
[root@docker ~]# docker run -d -it --name web1 httpd
f7641c43eb7021724e869f77bd4541e909ebf0968fc35e6a43e8e674d046ef3f
下面我们查看一下 web1 的网络
[root@docker ~]# docker exec -it web1 bash
root@1d5181ce7a85:/usr/local/apache2# hostname -I
172.17.0.4
# 也可以直接通过hostname -I观察IP地址
然后创建 busybox 容器并通过 --network=container:web1 指定 jointed 容器为 web1:
请注意 busybox 容器中的网络配置信息
[root@docker ~]# docker run -it --network container:web1 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ #
看!busybox 和 web1 的网卡 mac 地址与 IP 完全一样,它们共享了相同的网络栈。busybox 可以直接用 127.0.0.1 访问 web1 的 http 服务。
/ # wget 127.0.0.1
Connecting to 127.0.0.1 (127.0.0.1:80)
saving to 'index.html'
index.html 100% |***************************| 45 0:00:00 ETA
'index.html' saved
/ #
/ # cat index.html
<html><body><h1>It works!</h1></body></html>
/ #
oined 容器非常适合以下场景:
- 不同容器中的程序希望通过 loopback 高效快速地通信,比如 web server 与 app server。
- 希望监控其他容器的网络流量,比如运行在独立容器中的网络监控程序。
容器之间的通信我们已经搞清楚了,接下来要考虑的是容器如何与外部世界通信?这将是下一节的主
题。
容器如何访问外部世界
前面我们已经解决了容器间通信的问题,接下来讨论容器如何与外部世界通信。这里涉及两个方向:
- 容器访问外部世界
- 外部世界访问容器
容器访问外部世界
在我们当前的实验环境下,docker host 是可以访问aql
[root@docker ~]# ping -c 3 www.baidu.com
PING www.baidu.com (223.109.82.6) 56(84) bytes of data.
64 bytes from 223.109.82.6 (223.109.82.6): icmp_seq=1 ttl=128 time=67.8 ms
64 bytes from 223.109.82.6 (223.109.82.6): icmp_seq=2 ttl=128 time=13.0 ms
64 bytes from 223.109.82.6 (223.109.82.6): icmp_seq=3 ttl=128 time=12.5 ms
--- www.baidu.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 12.454/31.074/67.754/25.937 ms
我们看一下容器是否也能访问外网呢?
[root@docker ~]# docker run -it --name test1 busybox
/ #
/ # ping -c 3 www.baidu.com
PING www.baidu.com (223.109.82.6): 56 data bytes
64 bytes from 223.109.82.6: seq=0 ttl=127 time=21.240 ms
64 bytes from 223.109.82.6: seq=1 ttl=127 time=14.987 ms
64 bytes from 223.109.82.6: seq=2 ttl=127 time=14.651 ms
--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 14.651/16.959/21.240 ms
可见,容器默认就能访问外网。
请注意:这里外网指的是容器网络以外的网络环境,并非特指 internet。
现象很简单,但更重要的:我们应该理解现象下的本质。
在上面的例子中,busybox 位于 docker0 这个私有 bridge 网络中(172.17.0.0/16),当 busybox 从容器向外 ping 时,数据包是怎样到达 www.baidu.com 的呢?
这里的关键就是 NAT。我们查看一下 docker host 上的 iptables 规则:
[root@docker ~]# iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P POSTROUTING ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.19.0.0/16 ! -o br-6936dc39839e -j MASQUERADE
-A POSTROUTING -s 172.22.16.0/24 ! -o br-c0fc7bdf8143 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i docker0 -j RETURN
-A DOCKER -i br-6936dc39839e -j RETURN
-A DOCKER -i br-c0fc7bdf8143 -j RETURN
[root@docker ~]#
在 NAT 表中,有这么一条规则:
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
其含义是:如果网桥 docker0 收到来自 172.17.0.0/16 网段的外出包,把它交给 MASQUERADE 处理。
而 MASQUERADE 的处理方式是将包的源地址替换成 host 的地址发送出去,即做了一次网络地址转换(NAT)。
下面我们通过 tcpdump 查看地址是如何转换的。先查看 docker host 的路由表:
[root@docker ~]# ip r
default via 192.168.108.2 dev ens160 proto static metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
172.19.0.0/16 dev br-6936dc39839e proto kernel scope link src 172.19.0.1 linkdown
172.22.16.0/24 dev br-c0fc7bdf8143 proto kernel scope link src 172.22.16.1
linkdown
192.168.108.0/24 dev ens160 proto kernel scope link src 192.168.108.30 metric 100
默认路由通过 ens160 发出去,所以我们要同时监控 ens160 和 docker0 上的 icmp(ping)数据包。
使用tcpdump观察现象
[root@docker ~]# yum install -y tcpdump
[root@docker ~]# tcpdump -i docker0 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
#再开一个窗口
[root@docker ~]# tcpdump -i ens160 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes
再开一个窗口, busybox ping www.baidu.com
[root@docker ~]# docker run -it busybox
/ #
/ # ping -c 3 www.baidu.com
PING www.baidu.com (223.109.82.41): 56 data bytes
64 bytes from 223.109.82.41: seq=0 ttl=127 time=12.503 ms
64 bytes from 223.109.82.41: seq=1 ttl=127 time=13.380 ms
64 bytes from 223.109.82.41: seq=2 ttl=127 time=11.507 ms
--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 11.507/12.463/13.380 ms
/ #
tcpdump 输出如下:
查看docker0抓包窗口
[root@docker ~]# tcpdump -i docker0 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
15:23:42.854951 IP 172.17.0.2 > 223.109.82.41: ICMP echo request, id 7, seq 0,
length 64
15:23:42.870473 IP 223.109.82.41 > 172.17.0.2: ICMP echo reply, id 7, seq 0,
length 64
15:23:43.857551 IP 172.17.0.2 > 223.109.82.41: ICMP echo request, id 7, seq 1,
length 64
15:23:43.880065 IP 223.109.82.41 > 172.17.0.2: ICMP echo reply, id 7, seq 1,
length 64
15:23:44.869116 IP 172.17.0.2 > 223.109.82.41: ICMP echo request, id 7, seq 2,
length 64
15:23:44.883777 IP 223.109.82.41 > 172.17.0.2: ICMP echo reply, id 7, seq 2,
length 64
docker0 收到 busybox 的 ping 包,源地址为容器 IP 172.17.0.2,这没问题,交给 MASQUERADE 处
理。这时,在 ens160 上我们看到了变化:
[root@docker ~]# tcpdump -i ens160 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes
15:25:41.886472 IP 192.168.108.30 > 223.109.82.41: ICMP echo request, id 8, seq
0, length 64
15:25:41.906738 IP 223.109.82.41 > 192.168.108.30: ICMP echo reply, id 8, seq 0,
length 64
15:25:42.887752 IP 192.168.108.30 > 223.109.82.41: ICMP echo request, id 8, seq
1, length 64
15:25:42.899613 IP 223.109.82.41 > 192.168.108.30: ICMP echo reply, id 8, seq 1,
length 64
15:25:43.889467 IP 192.168.108.30 > 223.109.82.41: ICMP echo request, id 8, seq
2, length 64
15:25:43.904277 IP 223.109.82.41 > 192.168.108.30: ICMP echo reply, id 8, seq 2,
length 64
外部世界如何访问容器
上节我们学习了容器如何访问外部网络,今天讨论另一个方向:外部网络如何访问到容器?
答案是:端口映射。
docker 可将容器对外提供服务的端口映射到 host 的某个端口,外网通过该端口访问容器。容器启动时通过 -p 参数映射端口:
容器启动后,可通过 docker ps 或者 docker port 查看到 host 映射的端口。在上面的例子中,httpd
容器的 80 端口被映射到 host 32768 上,这样就可以通过 :<32768> 访问容器的 web 服务了。
[root@docker ~]# curl 192.168.108.30:32768
<html><body><h1>It works!</h1></body></html>
除了映射动态端口,也可在 -p 中指定映射到 host 某个特定端口,例如可将 80 端口映射到 host 的
8080 端口:
[root@docker ~]# docker run -d -p 8080:80 httpd
25b04cb7f6d6c012c609251a425475fcf7da03c93feb20c1d951eb45cb1ef79b
[root@docker ~]#
[root@docker ~]# curl 192.168.108.30:8080
<html><body><h1>It works!</h1></body></html>
安装tomcat
[root@docker ~]# docker search tomcat
NAME DESCRIPTION STARS
OFFICIAL
tomcat Apache Tomcat is an open source implementati… 3751
[OK]
bitnami/tomcat Bitnami container image for Tomcat 52
bitnamicharts/tomcat Bitnami Helm chart for Apache Tomcat 0
rootpublic/tomcat 0
chainguard/tomcat Build, ship and run secure software with Cha… 0
islandora/tomcat Base tomcat image used for java services. 0
vulhub/tomcat 0
openeuler/tomcat 0
tomcat/tomcat01 0
jelastic/tomcat An image of the Tomcat Java application serv… 4
rightctrl/tomcat CentOS , Oracle Java, tomcat application ssl… 7
amd64/tomcat Apache Tomcat is an open source implementati… 10
softwareplant/tomcat Tomcat images for jira-cloud testing 0
arm64v8/tomcat Apache Tomcat is an open source implementati… 13
appsvc/tomcat 1
s390x/tomcat Apache Tomcat is an open source implementati… 0
ppc64le/tomcat Apache Tomcat is an open source implementati… 1
arm32v7/tomcat Apache Tomcat is an open source implementati… 12
awscory/tomcat tomcat 0
devbeta/tomcat 0
tutum/tomcat Base docker image to run a Tomcat applicatio… 11
qasymphony/tomcat Tomcat images 1
hegand/tomcat docker-tomcat 0
techangels/tomcat 0
cloudesire/tomcat Tomcat server, 6/7/8 14
从docker hub上拉取tomcat镜像到本地
[root@docker ~]# docker pull tomcat
docker images查看是否有拉取到的tomcat
[root@docker ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
httpd latest 90f191b9781e 2 weeks ago 148MB
tomcat latest 9ca267cc83c7 3 weeks ago 468MB
hello-world latest 74cc54e27dc4 6 months ago 10.1kB
使用tomcat镜像创建容器实例(也叫运行镜像)
[root@docker ~]# docker run -itd -p 8080:8080 tomcat
92d78922526ba2a54ef63c48d2fec80b10997027de93457a4e2d56d7ea7448e2
把webapps.dist目录换成webapps
#查看刚才创建的tomcat容器ID
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
NAMES
92d78922526b tomcat "catalina.sh run" 2 minutes ago Up 2 minutes
0.0.0.0:8080->8080/tcp, :::8080->8080/tcp fervent_brahmagupta
#进入tomcat容器
[root@docker ~]# docker exec -it 92d bash
#查看webapps文件夹为空
root@92d78922526b:/usr/local/tomcat# ls webapps
root@92d78922526b:/usr/local/tomcat#
#文件在webapps.dist
root@92d78922526b:/usr/local/tomcat# ls webapps.dist/
docs examples host-manager manager ROOT
#用webapps.dist替换webapps
root@92d78922526b:/usr/local/tomcat# mv webapps.dist/* webapps
本章小结
在这一章我们首先学习了 Docker 的三种网络:none, host 和 bridge 并讨论了它们的不同使用场景;然
后我们实践了创建自定义网络;最后详细讨论了如何实现容器与容器之间,容器与外部网络之间的通
信。
本章重点关注的是单个主机内的容器网络,下一节开始学习 Docker 存储
存储
Docker的两类存储资源
我们从本章开始讨论 Docker 存储。
Docker 为容器提供了两种存放数据的资源:
- 由 storage driver 管理的镜像层和容器层。
- Data Volume。
Docker 安装时会根据当前系统的配置选择默认的 driver。默认 driver 具有最好的稳定性,因为默认
driver 在发行版上经过了严格的测试。
运行 docker info 查看CentOS的默认 driver:
[root@docker ~]# docker info
Client: Docker Engine - Community
Version: 26.1.3
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.14.0
Path: /usr/libexec/docker/cli-plugins/docker-buildx
compose: Docker Compose (Docker Inc.)
Version: v2.27.0
Path: /usr/libexec/docker/cli-plugins/docker-compose
Server:
Containers: 2
Running: 0
Paused: 0
Stopped: 2
Images: 16
Server Version: 26.1.3
Storage Driver: overlay2
Backing Filesystem: xfs
Supports d_type: true
Using metacopy: false
Native Overlay Diff: true
userxattr: false
Logging Driver: json-file
Cgroup Driver: cgroupfs
Cgroup Version: 1
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 8b3b7ca2e5ce38e8f31a34f35b2b68ceb8470d89
runc version: v1.1.12-0-g51d5e94
init version: de40ad0
Security Options:
seccomp
Profile: builtin
Kernel Version: 4.18.0-553.6.1.el8.x86_64
Operating System: CentOS Stream 8
OSType: linux
Architecture: x86_64
CPUs: 4
Total Memory: 15.36GiB
Name: docker
ID: fa9330b1-aebb-4889-a2d6-bc7181d443b2
Docker Root Dir: /var/lib/docker
Debug Mode: false
Experimental: false
Insecure Registries:
127.0.0.0/8
Data Volume之bind mount
始讨论 Data Volume。
Data Volume 本质上是 Docker Host 文件系统中的目录或文件,能够直接被 mount 到容器的文件系统中。Data Volume 有以下特点:
- Data Volume 是目录或文件,而非没有格式化的磁盘(块设备)。
- 容器可以读写 volume 中的数据。
- volume 数据可以被永久的保存,即使使用它的容器已经销毁。
好,现在我们有数据层(镜像层和容器层)和 volume 都可以用来存放数据,具体使用的时候要怎样选择呢?考虑下面几个场景:
- Database 软件 vs Database 数据
- Web 应用 vs 应用产生的日志
- 数据分析软件 vs input/output 数据
- Apache Server vs 静态 HTML 文件
相信大家会做出这样的选择:
- 前者放在数据层中。因为这部分内容是无状态的,应该作为镜像的一部分。
- 后者放在 Data Volume 中。这是需要持久化的数据,并且应该与镜像分开存放。
还有个大家可能会关心的问题:如何设置 voluem 的容量?
因为 volume 实际上是 docker host 文件系统的一部分,所以 volume 的容量取决于文件系统当前未使用的空间,目前还没有方法设置 volume 的容量。
bind mount
bind mount 是将 host 上已存在的目录或文件 mount 到容器。
例如 docker host 上有目录 $HOME/htdocs:
[root@docker ~]# pwd
/root
[root@docker ~]# mkdir htdocs
[root@docker ~]# cd htdocs/
[root@docker htdocs]# touch index.html
[root@docker htdocs]# vim index.html
<html><body><h1>This is a file in host file system !</h1></body></html>
[root@docker htdocs]# cd ..
[root@docker ~]# cat htdocs/index.html
<html><body><h1>This is a file in host file system !</h1></body></html>
[root@docker ~]#
通过 -v 将其 mount 到 httpd 容器:
[root@docker ~]# docker run -d -p 80:80 -v ~/htdocs:/usr/local/apache2/htdocs
httpd
883f8a22b2be796928d16ad131f6cba922492e325fdadf4275ca9f59eae5ed12
-v 的格式为 : 。/usr/local/apache2/htdocs 就是 apache server 存放静态文件的地方。由于 /usr/local/apache2/htdocs 已经存在,原有数据会被隐藏起来,取而代之的是host $HOME/htdocs/ 中的数据,这与 linux mount 命令的行为是一致的。
[root@docker ~]# curl 127.0.0.1:80
<html><body><h1>This is a file in host file system !</h1></body></html>
curl 显示当前主页确实是 $HOME/htdocs/index.html 中的内容。更新一下,看是否能生效:
[root@docker ~]# echo "updated index page!" > ~/htdocs/index.html
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
updated index page!
host 中的修改确实生效了,bind mount 可以让 host 与容器共享数据。这在管理上是非常方便的。
下面我们将容器销毁,看看对 bind mount 有什么影响:
[root@docker ~]# docker stop 883f8a22b2be
883f8a22b2be
[root@docker ~]# docker rm 883f8a22b2be
883f8a22b2be
[root@docker ~]# cat ~/htdocs/index.html
updated index page!
[root@docker ~]#
可见,即使容器没有了,bind mount 也还在。这也合理,bind mount 是 host 文件系统中的数据,只是借给容器用用,哪能随便就删了啊。
另外,bind mount 时还可以指定数据的读写权限,默认是可读可写,可指定为只读:
[root@docker ~]# docker run -d -p 80:80 -v ~/htdocs:/usr/local/apache2/htdocs:ro
httpd
bf1f526ddde7a7babb29adc091eef039628ba2eb288e6db33c9ccd946cc5d5f1
[root@docker ~]# docker exec -it bf1f526ddde7 bash
root@bf1f526ddde7:/usr/local/apache2# echo "do some changes" > htdocs/index.html
bash: htdocs/index.html: Read-only file system
ro 设置了只读权限,在容器中是无法对 bind mount 数据进行修改的。只有 host 有权修改数据,提高了安全性。
除了 bind mount 目录,还可以单独指定一个文件:
# 删除上一个容器不然80端口冲突
[root@docker ~]# docker rm - f bf1f526ddde7
#运行新容器用单独文件绑定
[root@docker ~]# docker run -d -p 80:80 -v
~/htdocs/index.html:/usr/local/apache2/htdocs/new_index.html httpd
643e8820e98a17b977a984903ddfa6f37dcae3d705d45b624d0fce5c94fecb0c
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
<html><body><h1>It works!</h1></body></html>
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80/new_index.html
updated index page!
[root@docker ~]#
Data Volume之docker volume
docker managed volume 与 bind mount 在使用上的最大区别是不需要指定 mount 源,指明 mountpoint 就行了。还是以 httpd 容器为例
[root@docker ~]# docker run -d -p 80:80 -v /usr/local/apache2/htdocs httpd
c75f36c4bf32e39f2d078d41956be85522c0630cf91498123dcd83ba159ce873
我们通过 -v 告诉 docker 需要一个 data volume,并将其 mount 到 /usr/local/apache2/htdocs。那么这个 data volume 具体在哪儿呢?
这个答案可以在容器的配置信息中找到,执行 docker inspect 命令:
[root@docker ~]# docker inspect c75f36c4bf32 #docker inspect后面跟的是容器ID
...
"Mounts": [
{
"Type": "volume",
"Name":
"1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635e569d16",
"Source":
"/var/lib/docker/volumes/1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a63
5e569d16/_data",
"Destination": "/usr/local/apache2/htdocs",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
...
docker inspect 的输出很多,我们感兴趣的是 Mounts 这部分,这里会显示容器当前使用的所有 data
volume,包括 bind mount 和 docker managed volume。
Source 就是该 volume 在 host 上的目录。
原来,每当容器申请 mount docker manged volume 时,docker 都会在 /var/lib/docker/volumes
下生成一个目录(例子中是
"/var/lib/docker/volumes/1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635e56
9d16/_data ),这个目录就是 mount 源。
下面继续研究这个 volume,看看里面有些什么东西:
[root@docker ~]# ls -l
/var/lib/docker/volumes/1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635
e569d16/_data
total 4
-rw-r--r-- 1 501 ftp 26 Sep 17 21:08 index.html
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
<html><body><h1>It works!</h1></body></html>
volume 的内容跟容器原有 /usr/local/apache2/htdocs 完全一样,这是怎么回事呢?
这是因为:如果 mount point 指向的是已有目录,原有数据会被复制到 volume 中。
但要明确一点:此时的 /usr/local/apache2/htdocs 已经不再是由 storage driver 管理的层数据了,它已经是一个 data volume。我们可以像 bind mount 一样对数据进行操作,例如更新数据:
[root@docker ~]# echo "update volume from host !" >
/var/lib/docker/volumes/1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635
e569d16/_data/index.html
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
update volume from host !
简单回顾一下 docker managed volume 的创建过程:
- 容器启动时,简单的告诉 docker “我需要一个 volume 存放数据,帮我 mount 到目录 /abc”。
- docker 在 /var/lib/docker/volumes 中生成一个随机目录作为 mount 源。
- 如果 /abc 已经存在,则将数据复制到 mount 源,
- 将 volume mount 到 /abc
除了通过 docker inspect 查看 volume,我们也可以用 docker volume 命令:
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local 1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635e569d16
[root@docker ~]#
[root@docker ~]# docker volume inspect
1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635e569d16
[
{
"CreatedAt": "2024-09-17T21:04:33+08:00",
"Driver": "local",
"Labels": {
"com.docker.volume.anonymous": ""
},
"Mountpoint":
"/var/lib/docker/volumes/1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a63
5e569d16/_da
"Name":
"1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635e569d16",
"Options": null,
"Scope": "local"
}
]
[root@docker ~]#
如何共享数据
数据共享是 volume 的关键特性,本节我们详细讨论通过 volume 如何在容器与 host 之间,容器与容器
之间共享数据。
容器与 host 共享数据
我们有两种类型的 data volume,它们均可实现在容器与 host 之间共享数据,但方式有所区别。
对于 bind mount 是非常明确的:直接将要共享的目录 mount 到容器。具体请参考前面 httpd 的例子,不再赘述。
docker managed volume 就要麻烦点。由于 volume 位于 host 中的目录,是在容器启动时才生成,所
以需要将共享数据拷贝到 volume 中。请看下面的例子:
[root@docker ~]# docker run -d -p 80:80 -v /usr/local/apache2/htdocs httpd
889b9d5309bd12499f8e4ee78a491d9ba7acef5660be00f30cda6569c9974c6f
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
<html><body><h1>It works!</h1></body></html>
[root@docker ~]#
[root@docker ~]# docker cp ~/htdocs/index.html
889b9d5309bd:/usr/local/apache2/htdocs #将host os的/root/htdocs/index.html拷贝到容
器中的/usr/local/apache2/htdocs目录下
Successfully copied 2.05kB to 889b9d5309bd:/usr/local/apache2/htdocs
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
updated index page!
[root@docker ~]#
容器之间共享数据
第一种方法是将共享数据放在 bind mount 中,然后将其 mount 到多个容器。还是以 httpd 为例,不过
这次的场景复杂些,我们要创建由三个 httpd 容器组成的 web server 集群,它们使用相同的 html 文
件,操作如下:
- 将 $HOME/htdocs mount 到三个 httpd 容器。
[root@docker ~]# docker run --name web1 -d -p 80 -v
~/htdocs:/usr/local/apache2/htdocs httpd
994e75ef90a80365d7ea7733c727c3b1d24e5423d59a74185304462b7137d774
[root@docker ~]#
[root@docker ~]# docker run --name web2 -d -p 80 -v
~/htdocs:/usr/local/apache2/htdocs httpd
28ff7c72d18b30123ff7e383db211890a0007e48817cdc73e7f50e9f2233eee6
[root@docker ~]#
[root@docker ~]# docker run --name web3 -d -p 80 -v
~/htdocs:/usr/local/apache2/htdocs httpd
160d823d985bad0ce7c27a0d301956f3c7ab4e9ca34c5be2601b3374fa30f6f1
[root@docker ~]#
- 查看当前主页内容。
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
160d823d985b httpd "httpd-foreground" 13 seconds ago Up 12 seconds
0.0.0.0:32770->80/tcp, :::32770->80/tcp web3
28ff7c72d18b httpd "httpd-foreground" 19 seconds ago Up 18 seconds
0.0.0.0:32769->80/tcp, :::32769->80/tcp web2
994e75ef90a8 httpd "httpd-foreground" 29 seconds ago Up 27 seconds
0.0.0.0:32768->80/tcp, :::32768->80/tcp web1
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:32770
#这个32770端口是host os映射到web3的duan'k
updated index page!
[root@docker ~]# curl 127.0.0.1:32769
updated index page!
[root@docker ~]# curl 127.0.0.1:32768
updated index page!
[root@docker ~]#
- 修改 volume 中的主页文件,再次查看并确认所有容器都使用了新的主页。
[root@docker ~]# echo "This is a new index page for web cluster" >
~/htdocs/index.html
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:32770
This is a new index page for web cluster
[root@docker ~]# curl 127.0.0.1:32769
This is a new index page for web cluster
[root@docker ~]# curl 127.0.0.1:32768
This is a new index page for web cluster
[root@docker ~]#
用volume container共享数据
volume container 是专门为其他容器提供 volume 的容器。它提供的卷可以是 bind mount,也可以是docker managed volume。下面我们创建一个 volume container:
[root@docker ~]# docker create --name vc_data \
-v ~/htdocs/:/usr/local/apache2/htdocs \
-v /other/userful/tools \
busybox
bb4a33bf34aa7817ea752db83079aff15a5dfd77c23363f2e8dd3096d1cb910f
我们将容器命名为 vc_data (vc 是 volume container 的缩写)。注意这里执行的是 docker create
命令,这是因为 volume container 的作用只是提供数据,它本身不需要处于运行状态。容器 mount 了
两个 volume:
- bind mount,存放 web server 的静态文件。
- docker managed volume,存放一些实用工具(当然现在是空的,这里只是做个示例)。
通过 docker inspect 可以查看到这两个 volume。
[root@docker ~]# docker inspect vc_data
...
"Mounts": [
{
"Type": "volume",
"Name":
"4500300655eb8e32a5d88174395a61ea394e17266fbde60f224d815a7a9557e0",
"Source":
"/var/lib/docker/volumes/4500300655eb8e32a5d88174395a61ea394e17266fbde60f224d815a
7a9557e0/_data",
"Destination": "/other/userful/tools",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
},
{
"Type": "bind",
"Source": "/root/htdocs",
"Destination": "/usr/local/apache2/htdocs",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
]
其他容器可以通过 --volumes-from 使用 vc_data 这个 volume container:
[root@docker ~]# docker run --name web1 -d -p 80 --volumes-from vc_data httpd
362f039b562e4bf0020c38839a69bfd62733f27fda5481eb5b58178f41318ffe
[root@docker ~]#
[root@docker ~]# docker run --name web2 -d -p 80 --volumes-from vc_data httpd
217c1a243bb9a1f32ccae9ae2792e3720263d36402bbac58f43cfb068bc21e66
[root@docker ~]#
[root@docker ~]# docker run --name web3 -d -p 80 --volumes-from vc_data httpd
ce00b5fa458fc1af01e82ce8b43e8c072c858a669c1298add8053cbc7ac9f49b
[root@docker ~]#
三个 httpd 容器都使用了 vc_data,看看它们现在都有哪些 volume,以 web1 为例:
[root@docker ~]# docker inspect web1
...
"Mounts": [
{
"Type": "bind",
"Source": "/root/htdocs",
"Destination": "/usr/local/apache2/htdocs",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
},
{
"Type": "volume",
"Name":
"4500300655eb8e32a5d88174395a61ea394e17266fbde60f224d815a7a9557e0",
"Source":
"/var/lib/docker/volumes/4500300655eb8e32a5d88174395a61ea394e17266fbde60f224d815a
7a9557e0/_data",
"Destination": "/other/userful/tools",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
...
web1 容器使用的就是 vc_data 的 volume,而且连 mount point 都是一样的。验证一下数据共享的效果:
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
ce00b5fa458f httpd "httpd-foreground" 2 minutes ago Up 2 minutes
0.0.0.0:32773->80/tcp, :::32773->80/tcp web3
217c1a243bb9 httpd "httpd-foreground" 2 minutes ago Up 2 minutes
0.0.0.0:32772->80/tcp, :::32772->80/tcp web2
362f039b562e httpd "httpd-foreground" 2 minutes ago Up 2 minutes
0.0.0.0:32771->80/tcp, :::32771->80/tcp web1
[root@docker ~]#
[root@docker ~]# echo "This content is from a volume container!" >
~/htdocs/index.html
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:32773
This content is from a volume container!
[root@docker ~]# curl 127.0.0.1:32772
This content is from a volume container!
[root@docker ~]# curl 127.0.0.1:32771
This content is from a volume container!
可见,三个容器已经成功共享了 volume container 中的 volume。
下面我们讨论一下 volume container 的特点:
-
与 bind mount 相比,不必为每一个容器指定 host path,所有 path 都在 volume container 中定义好了,容器只需与 volume container 关联,实现了容器与 host 的解耦。
-
使用 volume container 的容器其 mount point 是一致的,有利于配置的规范和标准化,但也带来一定的局限,使用时需要综合考虑。
另一种在容器之间共享数据的方式是 data-packed volume container,下一节讨论。
data-packed volume container
在上一节的例子中 volume container 的数据归根到底还是在 host 里,有没有办法将数据完全放到
volume container 中,同时又能与其他容器共享呢?
当然可以,通常我们称这种容器为 data-packed volume container。其原理是将数据打包到镜像中,然
后通过 docker managed volume 共享。
我们用下面的 Dockfile 构建镜像:
[root@docker ~]# vim Dockerfile
FROM busybox:latest
ADD htdocs /usr/local/apache2/htdocs
VOLUME /usr/local/apache2/htdocs
ADD 将静态文件添加到容器目录 /usr/local/apache2/htdocs。
VOLUME 的作用与 -v 等效,用来创建 docker managed volume,mount point 为
/usr/local/apache2/htdocs,因为这个目录就是 ADD 添加的目录,所以会将已有数据拷贝到 volume
中。
修改文本内容
[root@docker ~]# echo "This content is from a data packed volume container!" >
htdocs/index.html
build 新镜像 datapacked:
[root@docker ~]# docker build -t datapacked .
用新镜像创建 data-packed volume container:
[root@docker ~]# docker create --name vc_data datapacked
55dc035deeb31907830ea45f7af5e209c7e48078d59732798c529e7445d2a196
因为在 Dockerfile 中已经使用了 VOLUME 指令,这里就不需要指定 volume 的 mount point 了。启动httpd 容器并使用 data-packed volume container:
[root@docker ~]# docker run -d -p 80:80 --volumes-from vc_data httpd
98db78fc661441ec7ce0a2b76d62675fccf3389b7768cc9744b852645323c33b
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
This content is from a data packed volume container!
容器能够正确读取 volume 中的数据。data-packed volume container 是自包含的,不依赖 host 提供
数据,具有很强的移植性,非常适合 只使用 静态数据的场景,比如应用的配置信息、web server 的静态
文件等。
容器数据共享就讨论到这里,下一节我们学习如何对 data volume 的生命周期进行管理
volum生命周期管理
Data Volume 中存放的是重要的应用数据,如何管理 volume 对应用至关重要。前面我们主要关注的是
volume 的创建、共享和使用,本节将讨论如何备份、恢复、迁移和销毁 volume。
备份
因为 volume 实际上是 host 文件系统中的目录和文件,所以 volume 的备份实际上是对文件系统的备
份。
还记得前面我们是如何搭建本地 Registry 的吗?
[root@docker ~]# docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry
registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
1cc3d825d8b2: Pull complete
85ab09421e5a: Pull complete
40960af72c1c: Pull complete
e7bb1dbb377e: Pull complete
a538cc9b1ae3: Pull complete
Digest: sha256:ac0192b549007e22998eb74e8d8488dcfe70f1489520c3b144a6047ac5efbe90
Status: Downloaded newer image for registry:2
a8214201171f28a13803c647ef84acdc982465c230cb5c6a034bd05574948408
[root@docker ~]#
所有的本地镜像都存在 host 的 /myregistry 目录中,我们要做的就是定期备份这个目录。
恢复
volume 的恢复也很简单,如果数据损坏了,直接用之前备份的数据拷贝到 /myregistry 就可以了。
迁移
如果我们想使用更新版本的 Registry,这就涉及到数据迁移,方法是:
- docker stop 当前 Registry 容器。
- 启动新版本容器并 mount 原有 volume。
docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry registry:latest
当然,在启用新容器前要确保新版本的默认数据路径是否发生变化。
销毁
可以删除不再需要的 volume,但一定要确保知道自己正在做什么,volume 删除后数据是找不回来的。
docker 不会销毁 bind mount,删除数据的工作只能由 host 负责。对于 docker managed volume,在
执行 docker rm 删除容器时可以带上 -v 参数,docker 会将容器使用到的 volume 一并删除,但前提
是没有其他容器 mount 该 volume,目的是保护数据,非常合理。
如果删除容器时没有带 -v 呢?这样就会产生孤儿 volume,好在 docker 提供了 volume 子命令可以对
docker managed volume 进行维护。请看下面的例子:
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
[root@docker ~]#
[root@docker ~]# docker run --name bbox -v /test/data busybox
[root@docker ~]#
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local 8645b8e2edb91820799e6e8807d8b3476f1c5cec40beb14240b0e5143050e5e1
容器 bbox 使用的 docker managed volume 可以通过 docker volume ls 查看到。删除 bbox:
[root@docker ~]# docker rm bbox
bbox
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local 8645b8e2edb91820799e6e8807d8b3476f1c5cec40beb14240b0e5143050e5e1
[root@docker ~]#
因为没有使用 -v ,volume 遗留了下来。对于这样的孤儿 volume,可以用 docker volume rm 删除:
[root@docker ~]# docker volume rm
8645b8e2edb91820799e6e8807d8b3476f1c5cec40beb14240b0e5143050e5e1
8645b8e2edb91820799e6e8807d8b3476f1c5cec40beb14240b0e5143050e5e1
[root@docker ~]#
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
[root@docker ~]#
如果想批量删除孤儿 volume,可以执行:
docker volume rm $(docker volume ls -q)
docker volume prune
小结
本章我们学习了以下内容:
- docker 为容器提供了两种存储资源:数据层和 Data Volume。
- 数据层包括镜像层和容器层,由 storage driver 管理。
- Data Volume 有两种类型:bind mount 和 docker managed volume。
- bind mount 可实现容器与 host 之间,容器与容器之间共享数据。
- volume container 是一种具有更好移植性的容器间数据共享方案,特别是 data-packed volume
container。
- 最后我们学习了如何备份、恢复、迁移和销毁 Data Volume。
更多推荐



所有评论(0)