文章目录
- 如何运行容器
- 让容器长期运行
- 两种进入容器的方法
- docker attach
- docker exec
- attach VS exec
- 运行容器的最佳实践
- 小结
- 容器常用操作
- stop/start/restart容器
- pause/unpause容器
- 删除容器
- 一张图搞懂容器的生命周期
- 限制容器对内存的使用
- 内存限额
- 限制容器对CPU的使用
- 限制容器的Block IO
- block IO权重
- 限制bps和iops
- 实现容器的底层技术
- cgroup
- namespace
- Mount namespace
- UTS namespace
- IPC namespace
- PID namespace
- Network namespace
- User namespace
- 小结
如何运行容器
docker run
指定容器启动时执行的命令:
- CMD命令
- ENDPOINT命令
- 在
docker run
命令行中指定
例子:
docker run ubuntu pwd
容器启动时执行pwd
,返回/
是容器中的当前目录。执行docker ps
或docker container ls
可以查看Docker host中当前运行的容器,要查看所有状态的容器,使用docker ps -a
或docker container ls -a
让容器长期运行
容器的声明周期依赖于启动时执行的命令,只要该命令不结束,容器也就不会退出。
我们可以通过执行一个长期运行的命令来保持容器的运行状态,例如:
qiqi_dell@dell-2:~$ docker run ubuntu /bin/bash -c "while true;do sleep 1;done"
while
语句让bash不会退出,我们可以打开另一个终端查看容器状态:docker ps
但是这种方式
加上参数-d
以后台方式启动容器。
qiqi_dell@dell-2:~$ docker run -d ubuntu /bin/bash -c "while true;do sleep 1;done"
现在我们有了两个正在运行的容器。这里CONTAINER ID
是容器的”短ID“,前面启动容器时返回的是”长ID"。短ID视长ID的前12个字符。
NAMES
字段显示容器的名字,在启动容器时可以通过--name
参数显示地为容器命名,如果不指定,docker会自动为容器分配名字。如:
qiqi_dell@dell-2:~$ docker run --name "my_http_server" -d httpd
我们可以看到容器运行的命令是httpd-foreground
,通过docker history
可知这个命令是通过CMD指定的、
两种进入容器的方法
我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。有两种方法进入容器:attach
和exec
。
docker attach
通过docker attach
可以attach到容器启动命令的终端,例如:
这次我们通过“长ID”attach到了容器的启动命令终端,之后看到的是echo
每隔一秒打印的信息。
注:可通过docker stop <短ID>
停止容器
docker exec
通过docker exec
进入相同的容器
说明:
-
-it
以交互模式打开容器,执行bash,其结果就是打开了一个bash终端 - 进入到容器中,容器的hostname就是其短ID
- 可以像在普通Linux中一样执行命令。
ps -elf
显示了容器启动进程while
以及当前的bash
进程 - 执行
exit
退出容器,回到docker host
attach VS exec
attach与exec主要区别如下:
- attach在容器启动命令的终端,不会启动新的进程。
- exec则是在容器中打开新的终端,并且可以启动新的进程。
- 如果想直接在终端中查看启动命令的输出,用attach;其他情况使用exec。
当然,如果只是为了查看启动命令的输出,可以使用docker logs
命令
-f
的作用与tail -f
类似,能够持续打印输出。
运行容器的最佳实践
按用途容器大致可分为两类:服务类容器和工具类的容器。
- 服务类容器以daemon的形式运行,对外提供服务。比如web server,数据库等。通过
-d
以后台方式启动这类容器时非常合适的。如果要排查问题,可以通过exec -it
进入容器 - 工具类容器通常给我么您提供一个临时的工作环境,通常以run -it方式运行,比如:
运行busybox,run -it
的作用是在容器启动后就直接进入。我们这里通过wget验证了再容器中访问internet的能力。执行exit
退出终端,同时容器停止。
工具类容器多使用基础镜像,例如busybox、debian、ubuntu等。
小结
- 当CMD或ENTRYPOINT或docker run命令行指定的命令运行结束时,容器停止。
- 通过
-d
参数在后台启动容器 - 通过
exec -it
可进入容器并执行命令
容器常用操作
stop/start/restart容器
**通过docker stop
可以停止运行的容器。**容器在docker host中实际上是一个进程,docker stop
命令本质上是向该进程发送一个SIGTERM信号。
如果想快速停止容器,可使用docker kill
命令,其作用是向容器进程发送SIGKILL信号。
对于处于停止状态的容器,可以通过docker start
重新启动。docker start
会保留容器第一次启动时的所有参数。
容器可能会因某种错误而停止运行。对于服务类容器,我们通常希望在这种情况下容器能够自动重启。启动容器时设置--restart
就可以达到这个效果。
--restart=always
意味着无论容器因何种原因退出(除了docker stop
),就立即重启。该参数的形式还可以是--restart=on-failure:3
,意思是如果启动进程退出代码非0,则重启容器,最多重启3次。
pause/unpause容器
有时我们只是希望暂时让容器暂停工作一段时间,比如要对容器的文件系统打个快照,或者docker host需要使用CPU,这时可以执行docker pause <镜像名/短ID/长ID>
。取消暂停可以使用docker unpause <镜像名/短ID/长ID>
删除容器
使用docker一段时间后,host上可能会有大量已经退出了的容器。这些容器依然会占用host的文件系统资源,如果确认不会再重启此类容器,可以通过docker rm
删除。
docker rm
一次可以指定多个容器,如果希望批量删除所有已经退出的容器,可以执行如下命令:
docker rm -v $(docker ps -aq -f status=exited)
以及,docker rm
删除的是容器,而docker rmi
删除的是镜像。
一张图搞懂容器的生命周期
补充:
- 可以先创建容器,稍后再启动
docker create httpd
docker create
创建的容器处于Created状态docker start
将以后台方式启动容器。docker run
命令实际上是docker create
和docker start
的组合
- 当容器的启动进程退出时,
--restart
生效
退出包括正常退出或非正常退出。这里举了两个例子:启动进程正常退出或发生OOM,此时docker会根据--restart
的策略判断是否需要重启容器。但如果容器是因为执行docker stop
或docker kill
退出,则不会自动重启。
限制容器对内存的使用
一个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内存和300M的swap。默认情况下上面两组参数为-1,即对容器内村和swap的使用没有限制。
下面我们将使用progrium/stress镜像来学习如何为容器分配内存。该镜像可用于对容器执行压力测试。执行如下命令:
docker run -it -m 200M --memory-swap=300M progrium/stress --vm 1 --vm-bytes 280M
--vm 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
容器最多使用200M物理内存和200M swap。
限制容器对CPU的使用
默认设置下 ,所有容器可以平等地使用host CPU资源并且没有限制。
Docker可以通过-c
或--cpu-shares
设置容器使用CPU的权重。如果不指定,默认值为1024。
与内存限额不同,通过-c
设置的cpu share并不是CPU资源的绝对数量,而是一个相对的权重值。某个容器最终能分配到的CPU资源取决于它的cpu share占所有容器cpu share总和的比例。
换句话说:通过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
下面用progrium/stress做实验:
- 启动container_A,cpu share为1024:
--cpu
用来设置工作线程的数量。因为当前host只有1颗CPU,所以一个工作线程就能将CPU压满。如果有多颗CPU则需要相应增加--cpu
的数量。
- 启动container_B,cpu share为512:
- 在host中执行
top
,查看容器对CPU的使用情况
- 现在暂停container_A
-
top
显示container_B在container_A空闲的情况下能够用满整颗CPU:
限制容器的Block IO
前面学习了如何限制容器对内存和CPU的使用,本节我们来看Block IO。
Block IO是另一种可以限制容器使用的资源。Block IO指的是磁盘的读写,docker可通过设置权重、限制bps和iops的方式控制容器读写磁盘的带宽,下面分别讨论。
注:目前Block IO限额只对direct IO(不使用文件缓存)有效。
block IO权重
默认情况下,所有容器能平等地读写磁盘,可以通过设置--blkio-weight
参数来改变容器block IO的优先级。
--blkio-weight
与--cpu-shares
类似,设置的是相对权重值,默认为500。在下面的例子中,container_A读写磁盘的带宽是container_B的两倍。
docker run -it --name container_A --blkio-weight 600 ubuntu
docker run -it --name container_B --blkio-weight 300 ubuntu
限制bps和iops
bps是byte per second,每秒读写的数据量
iops是io per second,每秒IO的次数。
可通过以下参数控制容器的bps和iops:
--device-read-bps
,限制读某个设备的bps。
--device-write-bps
,限制写某个设备的bps。
--device-read-iops
,限制读某个设备的iops。
--device-write-iops
,限制写某个设备的iops。
下面这个例子限制容器写/dev/sda的速率为30 MB/s
实验结果:
通过dd测试在容器中写磁盘的速度。因为容器的文件系统是在host/dev/sda上的,在容器中写文件相当于对host/dev/sda进行写操作。另外,oflag=direct
指定用direct IO方式写文件,这样--device-write-bps
才能生效。
结果表明,bps 25.6 MB/s没有超过30MB/s的限速。
作为对比测试,如果不限速,结果如下:
其他参数的使用方法类似。
实现容器的底层技术
为了更好地理解容器的特性,本节我们将讨论容器的底层实现技术。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
:
查看容器的ID:
在/sys/fs/cgroup/cpu/docker目录中,Linux会为每个容器创建一个cgroup目录,以容器的长ID命名:
目录中包含所有与cpu有关的配置,文件cpu.shares保存的就是--cpu-shares
的配置,值为512.
同样地,/sys/fs/cgroup/memory/docker和/sys/fs/cgroup/blokiio/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中运行了四个容器,通过ps axf
可以查看容器进程:
如果我们进入到某个容器,ps
就只能看到自己的进程了:
而且进程的PID不同于host中对应进程的PID,容器中PID=1的进程也不是hostPID=1的init进程。也就是说:容器拥有自己独立的一套PID,这就是PID namespace提供的功能。
Network namespace
Network namespace让容器拥有自己独立的网卡、IP、路由等资源。
User namespace
User namespace让容器能够管理自己的用户,host不能看到容器中创建的用户。
小结
本章学习了容器的各种操作以及容器状态之间如何转换,然后讨论了限制容器使用CPU、内存和Block IO的方法,最后学习了实现容器的底层技术:cgroup 和namespace。
容器的常用操作命令有:
命令 | 含义 |
create | 创建容器 |
run | 运行容器 |
pause | 暂停容器 |
unpause | 取消暂停继续运行容器 |
stop | 发送SIGTERM停止容器 |
kill | 发送SIGKILL快速停止容器 |
start | 启动容器 |
restart | 重启容器 |
attach | attach到容器启动进程的终端 |
exec | 在容器中启动新进程,通常使用 |
logs | 显示容器启动进程的控制台输出,用"-f"持续打印 |
rm | 从磁盘中删除容器 |