第2章 容器
01 如何运行容器?
运行容器
docker run=docker create + docker start

docker run 是启动容器的方法。在讨论 Dockerfile 时我们已经学习到,可用三种方式指定容器启动时执行的命令:
- CMD 指令。
- ENTRYPOINT 指令。
- 在
docker run命令行中指定。
例如下面的例子:
bash
[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
shell
[root@docker ~]# docker run ubuntu pwd #使用ubuntu镜像创建容器并执行pwd命令
/
[root@docker ~]#
容器启动时执行 pwd,返回的 / 是容器中的当前目录。 执行 docker ps 或 docker container ls 可以查看 Docker host 中当前运行的容器:
bash
[root@docker ~]# docker ps #查看所有正在运行中的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
咦,怎么没有容器?用 docker ps -a 或 docker container ls -a 看看。

-a 会显示所有状态的容器,可以看到,之前的容器已经退出了,状态为Exited。
这种"一闪而过"的容器通常不是我们想要的结果,我们希望容器能够保持 runing 状态,这样才能被我们使用。
让容器长期运行
如何让容器保存运行呢?
因为容器的生命周期依赖于启动时执行的命令,只要该命令不结束,容器也就不会退出。
理解了这个原理,我们就可以通过执行一个长期运行的命令来保持容器的运行状态。例如执行下面的命令:
bash
[root@docker ~]# docker run ubuntu /bin/bash -c "while true ; do sleep 1 ; echo hahaha; done"
while 语句让 bash 不会退出。我们可以打开另一个终端查看容器的状态:

可见容器仍处于运行状态。不过这种方法有个缺点:它占用了一个终端。
我们可以加上参数 -d 以后台方式启动容器。
bash
[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" 或者 "名称" 来指定要操作的容器。比如下面停止一个容器:
bash
[root@docker ~]# docker stop 081be2bc2e1d
081be2bc2e1d
这里我们就是通过 "短ID" 指定了要停止的容器。
通过 while 启动的容器虽然能够保持运行,但实际上没有干什么有意义的事情。容器常见的用途是运行后台服务,例如前面我们已经看到的 http server:

这一次我们用 --name 指定了容器的名字。 我们还看到容器运行的命令是httpd-foreground,通过 docker history 可知这个命令是通过 CMD 指定的。

我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。下一节学习如何进入容器内部。
02 两种进入容器的方法
我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。有两种方法进入容器:attach 和 exec。
docker attach
通过 docker attach 可以 attach 到容器启动命令的终端,例如:
bash
[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 <container> bash|sh 是执行 exec 最常用的方式。
attach VS exec
attach 与 exec 主要区别如下:
- attach 直接进入容器 启动命令 的终端,不会启动新的进程。
- exec 则是在容器中打开新的终端,并且可以启动新的进程。
- 如果想直接在终端中查看启动命令的输出,用 attach;其他情况使用 exec。
当然,如果只是为了查看启动命令的输出,可以使用 docker logs 命令:
bash
[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 类似,能够持续打印输出。
下一节聊聊运行容器的最佳实践。
03 运行容器的最佳实践
按用途容器大致可分为两类:服务类容器和工具类的容器。
- 服务类容器以 daemon 的形式运行,对外提供服务。比如 web server,数据库等。通过
-d以后台方式启动这类容器是非常合适的。如果要排查问题,可以通过exec -it进入容器。 - 工具类容器通常给能我们提供一个临时的工作环境,通常以
run -it方式运行,比如:
bash
[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。
04 容器常用操作
前面讨论了如何运行容器,本节学习容器的其他常用操作。
stop/start/restart 容器
通过 docker stop 可以停止运行的容器。

容器在 docker host 中实际上是一个进程,docker stop 命令本质上是向该进程发送一个 SIGTERM 信号。如果想快速停止容器,可使用 docker kill 命令,其作用是向容器进程发送 SIGKILL 信号。

对于处于停止状态的容器,可以通过 docker start 重新启动。

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。

处于暂停状态的容器不会占用 CPU 资源,直到通过 docker unpause 恢复运行。

删除容器
使用 docker 一段时间后,host 上可能会有大量已经退出了的容器。

这些容器依然会占用 host 的文件系统资源,如果确认不会再重启此类容器,可以通过 docker rm 删除。
bash
[root@docker ~]# docker rm 35af7150bd17 9769bb915803
35af7150bd17
9769bb915803
docker rm 一次可以指定多个容器,如果希望批量删除所有已经退出的容器,可以执行如下命令:
docker rm -f $(docker ps -aq -f status=exited)

bash
# !!!慎用,删除所有状态容器
[root@docker ~]# docker rm -f $(docker ps -aq)
顺便说一句:docker rm 是删除容器,而 docker rmi 是删除镜像。
一下学了这么多操作,很有必要做个总结。下一节我们会用一张图来描述容器的状态机。
05 一张图搞懂容器所有操作
前面我们已经讨论了容器的各种操作,对容器的生命周期有了大致的理解,下面这张状态机很好地总结了容器各种状态之间是如何转换的。

如果掌握了前面的知识,要看懂这张图应该不难。不过有两点还是需要补充一下:
-
可以先创建容器,稍后再启动。

①
docker create创建的容器处于 Created 状态。②
docker start将以后台方式启动容器。docker run命令实际上是docker create和docker start的组合。 -
只有当容器的启动进程 退出 时,
--restart才生效。

退出包括正常退出或者非正常退出。这里举了两个例子:启动进程正常退出或发生 OOM,此时 docker 会根据
--restart的策略判断是否需要重启容器。但如果容器是因为执行docker stop或docker kill退出,则不会自动重启。
bash
#基于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 ~]#
好了,容器操作就讨论到这里,下一节我们将学习如何限制容器对资源的使用。
06 限制容器对内存的使用
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
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
bash
[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镜像来学习如何为容器分配内存。该镜像可用于对容器执行压力测试。执行如下命令:
bash
[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 内存。
- 一直循环...
如果让工作线程分配的内存超过 300M,结果如下:

分配的内存超过限额,stress 线程报错,容器退出。
如果在启动容器时只指定 -m 而不指定 --memory-swap,那么 --memory-swap 默认为 -m 的两倍,比如:
docker run -it -m 200M ubuntu-with-stress
容器最多使用 200M 物理内存和 200M swap。
内存限额就讨论到这里,下一节我们学习如何限制容器对 CPU 资源的使用。
07 限制容器对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:
bash[root@docker ~]# docker run --name "container_A" -it -c 1024 ubuntu-with-stress --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:
bash[root@docker ~]# docker run --name "container_B" -it -c 512 ubuntu-with-stress --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 的两倍。
再开一个窗口
bash[root@docker docker]# docker stats
-
现在暂停 container_A:
bash[root@docker ~]# docker pause container_A container_A
-
top显示 container_B 在 container_A 空闲的情况下能够用满整颗 CPU:

08 export和import容器
export-容器导出
将容器导出为一个tar包
bash
[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
不管此时这个容器是否处于运行状态,都可以导出为文件。
示例:
bash
# 创建容器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包导入成为镜像
bash
[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
示例:
bash
[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导入?
bash
[root@docker ~]# docker load -i myhttpd.tar
open /var/lib/docker/tmp/docker-import-3425397234/boot/json: no such file or directory
思考:docker save导出的文件是否可以使用docker import导入?
提示:我们可以将docker save保存出来的文件解压后分析文件结构。
09 实现容器的底层技术
为了更好地理解容器的特性,本节我们将讨论容器的底层实现技术。
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:
bash
[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
查看容器的 ID:

在 /sys/fs/cgroup/cpu/docker 目录中,Linux 会为每个容器创建一个 cgroup 目录,以容器长ID 命名:

目录中包含所有与 cpu 相关的 cgroup 配置,文件 cpu.shares 保存的就是 --cpu-shares 的配置,值为 512。
同样的,/sys/fs/cgroup/memory/docker 和 /sys/fs/cgroup/blkio/docker 中保存的是内存以及 Block IO 的 cgroup 配置。
namespace
在每个容器中,我们都可以看到文件系统,网卡等资源,这些资源看上去是容器自己的。拿网卡来说,每个容器都会认为自己有一块独立的网卡,即使 host 上只有一块物理网卡。这种方式非常好,它使得容器更像一个独立的计算机。
Linux 实现这种方式的技术是 namespace。namespace 管理着 host 中全局唯一的资源,并可以让每个容器都觉得只有自己在使用它。换句话说,namespace 实现了容器间资源的隔离。
Linux 使用了六种 namespace,分别对应六种资源:Mount、UTS、IPC、PID、Network 和 User,下面我们分别讨论。
Mount namespace
Mount namespace 让容器看上去拥有整个文件系统。
容器有自己的 / 目录,可以执行 mount 和 umount 命令。当然我们知道这些操作只在当前容器中生效,不会影响到 host 和其他容器。
UTS namespace
简单的说,UTS namespace 让容器有自己的 hostname。 默认情况下,容器的 hostname 是它的短ID,可以通过 -h 或 --hostname 参数设置。

IPC namespace
IPC namespace 让容器拥有自己的共享内存和信号量(semaphore)来实现进程间通信,而不会与 host 和其他容器的 IPC 混在一起。
PID namespace
我们前面提到过,容器在 host 中以进程的形式运行。例如当前 host 中运行了两个容器:
bash
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a80f1f8b692c ubuntu "/bin/bash" 26 seconds ago Up 4 seconds interesting_davinci
835dd4eebda2 httpd "httpd-foreground" 56 seconds ago Up 55 seconds 80/tcp adoring_pare
通过 ps axf 可以查看容器进程:

所有容器的进程都挂在 dockerd 进程下,同时也可以看到容器自己的子进程。 如果我们进入到某个容器,ps 就只能看到自己的进程了:
bash
[root@docker ~]# docker exec -it a80f1f8b692c bash
root@a80f1f8b692c:/#
root@a80f1f8b692c:/# ps axf
PID TTY STAT TIME COMMAND
17 pts/1 Ss 0:00 bash
25 pts/1 R+ 0:00 \_ ps axf
1 pts/0 Ss+ 0:00 /bin/bash
而且进程的 PID 不同于 host 中对应进程的 PID,容器中 PID=1 的进程当然也不是 host 的 init 进程。也就是说:容器拥有自己独立的一套 PID,这就是 PID namespace 提供的功能。
Network namespace
Network namespace 让容器拥有自己独立的网卡、IP、路由等资源。我们会在后面网络章节详细讨论。
User namespace
User namespace 让容器能够管理自己的用户,host 不能看到容器中创建的用户。
bash
[root@docker ~]# docker exec -it a80f1f8b692c bash
root@a80f1f8b692c:/#
root@a80f1f8b692c:/# useradd gaoqd #容器中创建用户gaoqd
root@a80f1f8b692c:/#
root@a80f1f8b692c:/# exit
exit
[root@docker ~]# su - gaoqd #宿主机中并没有用户gaoqd
su: user gaoqd does not exist
在容器中创建了用户 gaoqd,但 host 中并不会创建相应的用户。
小结
本章首先通过大量实验学习了容器的各种操作以及容器状态之间如何转换,然后讨论了限制容器使用 CPU、内存和 Block IO 的方法,最后学习了实现容器的底层技术:cgroup 和 namespace。
下面是容器的常用操作命令:
create 创建容器
run 运行容器
pause 暂停容器
unpause 取消暂停继续运行容器
stop 发送 SIGTERM 停止容器
kill 发送 SIGKILL 快速停止容器
start 启动容器
restart 重启容器
attach attach 到容器启动进程的终端
exec 在容器中启动新进程,通常使用 "-it" 参数
logs 显示容器启动进程的控制台输出,用 "-f" 持续打印
rm 从磁盘中删除容器
到这里,我们已经学习完了容器章节。
t@a80f1f8b692c:/# useradd gaoqd #容器中创建用户gaoqd
root@a80f1f8b692c:/#
root@a80f1f8b692c:/# exit
exit
root@docker \~\]# su - gaoqd #宿主机中并没有用户gaoqd su: user gaoqd does not exist 在容器中创建了用户 gaoqd,但 host 中并不会创建相应的用户。 **小结** 本章首先通过大量实验学习了容器的各种操作以及容器状态之间如何转换,然后讨论了限制容器使用 CPU、内存和 Block IO 的方法,最后学习了实现容器的底层技术:cgroup 和 namespace。 **下面是容器的常用操作命令:** > create 创建容器 > > run 运行容器 > > pause 暂停容器 > > unpause 取消暂停继续运行容器 > > stop 发送 SIGTERM 停止容器 > > kill 发送 SIGKILL 快速停止容器 > > start 启动容器 > > restart 重启容器 > > attach attach 到容器启动进程的终端 > > exec 在容器中启动新进程,通常使用 "-it" 参数 > > logs 显示容器启动进程的控制台输出,用 "-f" 持续打印 > > rm 从磁盘中删除容器 到这里,我们已经学习完了容器章节。