1 如何运行容器?
上一章我们学习了如何构建 Docker 镜像,并通过镜像运行容器。本章将深入讨论容器:学习容器的各种操作,容器各种状态之间如何转换,以及实现容器的底层技术。
运行容器
docker run=docker create + docker start

docker run 是启动容器的方法。在讨论 Dockerfile 时我们已经学习到,可用三种方式指定容器启动时执行的命令:
- CMD 指令。
- ENTRYPOINT 指令。
- 在
docker run命令行中指定。
例如下面的例子:
shell
[root@docker ~]# docker run 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 指定的。

我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。下一节学习如何进入容器内部。
2 两种进入容器的方法
我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。有两种方法进入容器: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 类似,能够持续打印输出。
下一节聊聊运行容器的最佳实践。
3 运行容器的最佳实践
按用途容器大致可分为两类:服务类容器和工具类的容器。
- 服务类容器以 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。
4 容器常用操作
前面讨论了如何运行容器,本节学习容器的其他常用操作。
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参数,attach进去ctrl_c(终止进程)不会重启。


--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 -v $(docker ps -aq -f status=exited)

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


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

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

退出包括正常退出或者非正常退出。这里举了两个例子:启动进程正常退出或发生 OOM,此时 docker 会根据
--restart的策略判断是否需要重启容器。但如果容器是因为执行docker stop或docker kill退出,则不会自动重启。
好了,容器操作就讨论到这里,下一节我们将学习如何限制容器对资源的使用。
6 限制容器对内存的使用
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 资源的使用。
7 限制容器对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:
