本文经授权转载自GitChat(ID:GitChat)
作者 | 张晋涛
责编 | 胡巍巍
Docker 上手很容易,但如果将其应用于生产环境,则需要对它有更深入的理解。只有这样,才能确保应用符合我们的预期,或在遇到问题时可及时解决。所以,要想真正掌握 Docker 的核心知识,只靠网络上零散的信息往往是不够的,必须系统性地学习。
容器,作为 Docker 的核心特性之一,是 Docker 使用者们无法回避的重要知识点。要想了解容器的核心原理,甚至自己动手写容器,不深入了解容器资源管理的相关的内容是绝对不行的。
本文将以容器资源管理为主题,解决以下三个问题:
-
哪些分配给容器的资源可被我们管理? -
容器实际使用了多少资源? -
如何对容器使用的资源进行管理?
资源类型
对于第一个问题,当我们启动一个容器的时候,它可以使用一些系统资源,这与我们在物理机上启动程序基本是一致的。比如主要的几类:
-
CPU -
内存 -
网络 -
I/O -
GPU
这些系统资源是在我们启动容器时,需要考虑和可被我们管理的。比如,我们可以执行 docker run --help 查看 docker run 命令所支持的全部参数。现在 docker run 命令所支持的参数已超过 90 项,这里就不一一列出了。
查看容器占用资源
docker stats
Docker 提供了一个很方便的命令 docker stats,可供我们查看和统计容器所占用的资源情况。
我们仍然启动一个 Redis 容器作为示例。
# 启动一个容器
(MoeLove) ➜ ~ docker run -d redis
c98c9831ee73e9b71719b404f5ecf3b408de0b69aec0f781e42d815575d28ada
# 查看其所占用资源的情况
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
c98c9831ee73 amazing_torvalds 0.08% 2.613MiB / 15.56GiB 0.02% 3.66kB / 0B 0B / 0B 4
这里传递了一个 --no-stream 的参数,是因为 docker stats 命令默认是一个持续的动态流式输出(每秒一次),给它传递 --no-stream 参数后,它就只输出一次便会退出了。
接下来我为你介绍下它输出内容的含义:
-
Container ID:容器的 ID,也是一个容器生命周期内不会变更的信息。 -
Name:容器的名称,如果没有手动使用 --name 参数指定,则 Docker 会随机生成一个,运行过程中也可以通过命令修改。 -
CPU %:容器正在使用的 CPU 资源的百分比,这里面涉及了比较多细节,下面会详细说。 -
Mem Usage/Limit:当前内存的使用及容器可用的最大内存,这里我使用了一台 16G 的电脑进行测试。 -
Mem %:容器正在使用的内存资源的百分比。 -
Net I/O:容器通过其网络接口发送和接受到的数据量。 -
Block I/O:容器通过块设备读取和写入的数据量。 -
Pids:容器创建的进程或线程数。
docker top
除了上面提到的 docker stats 命令外,Docker 也提供了另一个比较简单的命令 docker top,与我们平时用的 ps 命令基本一致, 也支持 ps 命令的参数。
(MoeLove) ➜ ~ docker top $(docker ps -ql)
UID PID PPID C STIME TTY TIME CMD
systemd+ 6275 6248 0 16:50 ? 00:00:24 redis-server *:6379
# 可以使用 ps 命令的参数
(MoeLove) ➜ ~ docker top $(docker ps -ql) -o pid,stat,cmd
PID STAT CMD
6275 Ssl redis-server *:6379
管理容器的 CPU 资源
在我们使用容器的时候,CPU 和内存是我们尤为关注的资源。不过,对于 CPU 资源的管理,涉及的内容会比较偏底层一些,有些涉及到了内核的 CPU 调度器,比如 CFS(Completely Fair Scheduler)等。
我们可以先来查看下 Docker 提供了哪些控制 CPU 资源相关的参数。使用 docker run --help |grep CPU 即可查看。
(MoeLove) ➜ ~ docker run --help |grep CPU
--cpu-period int Limit CPU CFS (Completely Fair Scheduler) period
--cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota
--cpu-rt-period int Limit CPU real-time period in microseconds
--cpu-rt-runtime int Limit CPU real-time runtime in microseconds
-c, --cpu-shares int CPU shares (relative weight)
--cpus decimal Number of CPUs
--cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)
这里暂时先不对参数的具体含义进行深入展开,我们直接以几个示例来分别进行说明,帮助大家理解。
默认无限制
备注:我这里以一个 4 核 CPU 的电脑进行演示。
现在我们启动一个容器,我们以体积很小的 Alpine Linux 为例好了。
(MoeLove) ➜ ~ docker run --rm -it alpine
/ #
在另一个窗口,执行上面介绍的查看容器资源的命令:
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
106a24399bc9 friendly_varahamihira 0.00% 1.047MiB / 15.56GiB 0.01% 5.01kB / 0B 1.67MB / 0B 1
可以看到,当前容器内没有过多的 CPU 消耗,且 PIDS 为 1,表示当前只有一个进程。
现在我们回到刚才启动的容器,执行以下命令:
sha256sum /dev/zero
-
sha256sum 是一个用于计算和检查 SHA256 信息的命令行工具;
-
/dev/zero 是 Linux 系统上一个特殊的设备,在读它时,它可以提供无限的空字符串(NULL 或者 0x00 之类的)。
所以上面的命令,会**让 sha256sum 持续地读 /dev/zero 产生的空串,并进行计算。**这将迅速地消耗 CPU 资源。
我们来看看此时容器的资源使用情况:
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
106a24399bc9 friendly_varahamihira 100.59% 1.5MiB / 15.56GiB 0.01% 14.4kB / 0B 1.99MB / 0B 2
(MoeLove) ➜ ~ docker top $(docker ps -ql) -o pid,c,cmd
PID C CMD
825 0 /bin/sh
965 99 sha256sum /dev/zero
(MoeLove) ➜ ~ docker exec -it $(docker ps -ql) sh
/ # sha256sum /dev/zero
查看容器使用资源的情况:
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
f359d4ff6fc6 nice_zhukovsky 200.79% 1.793MiB / 15.56GiB 0.01% 4.58kB / 0B 0B / 0B 4
(MoeLove) ➜ ~ docker top $(docker ps -ql) -o pid,c,cmd
PID C CMD
825 0 /bin/sh
965 99 sha256sum /dev/zero
1236 0 sh
1297 99 sha256sum /dev/zero
可以看到现在两个进程,已经让两个 CPU 满负载运行了。这里需要额外说明的是,选择 sha256sum 作为示例,是因为它是单线程程序,每次启动一个 sha256sum 并不会消耗其他 CPU 核的资源。
分配 0.5 CPU
(MoeLove) ➜ ~ docker update --cpus "0.5" $(docker ps -ql)
f359d4ff6fc6
我们可以重新启动一个容器,在 docker run 时,为它添加资源限制。
# 终止进程
/ # sha256sum /dev/zero
^C
# 启动程序
/ # sha256sum /dev/zero
查看资源占用情况:
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
f359d4ff6fc6 nice_zhukovsky 49.87% 1.777MiB / 15.56GiB 0.01% 112kB / 0B 1.59MB / 0B 3
(MoeLove) ➜ ~ docker top $(docker ps -ql) -o pid,c,cmd
PID C CMD
825 0 /bin/sh
1236 0 sh
7662 49 sha256sum /dev/zero
可以看到,该进程使用了 50% 左右的 CPU。我们接下来再启动另一个 sha256sum 的进程:
/ # sha256sum /dev/zero
查看资源使用情况:
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
f359d4ff6fc6 nice_zhukovsky 50.92% 1.891MiB / 15.56GiB 0.01% 113kB / 0B 1.59MB / 0B 4
(MoeLove) ➜ ~ docker top $(docker ps -ql) -o pid,c,cmd
PID C CMD
825 0 /bin/sh
1236 0 sh
可以看到,该容器整体占用了 50% 的 CPU,而其中的两个 sha256sum 进程则各占了 25%。
# 更新配置,使用 1.5 CPU
(MoeLove) ➜ ~ docker update --cpus "1.5" $(docker ps -ql)
f359d4ff6fc6
分别使用之前的两个窗口,执行 sha256sum /dev/zero 进行测试:
/ # sha256sum /dev/zero
查看资源使用情况:
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
f359d4ff6fc6 nice_zhukovsky 151.23% 2MiB / 15.56GiB 0.01% 122kB / 0B 1.59MB / 0B 4
(MoeLove) ➜ ~ docker top $(docker ps -ql) -o pid,c,cmd
PID C CMD
825 0 /bin/sh
1236 0 sh
25167 77 sha256sum /dev/zero
25211 74 sha256sum /dev/zero
可以看到,结果与我们的预期基本相符,150% 左右的 CPU,而两个测试进程,也差不多是平分了 CPU 资源。
(MoeLove) ➜ ~ docker update --cpus "1.5" --cpuset-cpus 0 $(docker ps -ql)
f359d4ff6fc6
分别使用之前的两个窗口,执行 sha256sum /dev/zero 进行测试:
/ # sha256sum /dev/zero
查看资源情况:
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
f359d4ff6fc6 nice_zhukovsky 99.18% 1.988MiB / 15.56GiB 0.01% 221kB / 0B 1.59MB / 0B 4
(MoeLove) ➜ ~ docker top $(docker ps -ql) -o pid,c,cmd
PID C CMD
825 0 /bin/sh
1236 0 sh
25119 50 sha256sum /dev/zero
25164 48 sha256sum /dev/zero
可以看到,虽然我们仍然使用 --cpus 指定了 1.5 CPU,但由于使用 --cpuset-cpus 限制只允许它跑在第一个 CPU 上,所以这两个测试进程也就只能评分该 CPU 了。本文节选自专栏。
小结
(MoeLove) ➜ ~ docker run --help |grep CPU
--cpu-period int Limit CPU CFS (Completely Fair Scheduler) period
--cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota
--cpu-rt-period int Limit CPU real-time period in microseconds
--cpu-rt-runtime int Limit CPU real-time runtime in microseconds
-c, --cpu-shares int CPU shares (relative weight)
--cpus decimal Number of CPUs
--cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)
管理容器的内存资源
(MoeLove) ➜ ~ docker run --help |egrep 'memory|oom'
--kernel-memory bytes Kernel memory limit
-m, --memory bytes Memory limit
--memory-reservation bytes Memory soft limit
--memory-swap bytes Swap limit equal to memory plus swap: '-1' to enable unlimited swap
--memory-swappiness int Tune container memory swappiness (0 to 100) (default -1)
--oom-kill-disable Disable OOM Killer
--oom-score-adj int Tune host's OOM preferences (-1000 to 1000)
OOM
If we run out of memory, we have the choice between either killing a random task (bad), letting the system crash (worse).
OR try to be smart about which process to kill. Note that we don't have to be perfect here, we just have to be good.
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
const nodemask_t *nodemask, unsigned long totalpages)
{
long points;
long adj;
if (oom_unkillable_task(p, memcg, nodemask))
return 0;
p = find_lock_task_mm(p);
if (!p)
return 0;
/*
* Do not even consider tasks which are explicitly marked oom
* unkillable or have been already oom reaped or the are in
* the middle of vfork
*/
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN ||
test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
in_vfork(p)) {
task_unlock(p);
return 0;
}
/*
* The baseline for the badness score is the proportion of RAM that each
* task's rss, pagetable and swap space use.
*/
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;
task_unlock(p);
/* Normalize to oom_score_adj units */
adj *= totalpages / 1000;
points += adj;
/*
* Never return 0 for an eligible task regardless of the root bonus and
* oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
*/
return points > 0 ? points : 1;
}
而为了能够最快地进行选择,这里的逻辑也是尽可能的简单,除了明确标记不可杀掉的进程外,直接选择内存占用最多的进程。(当然,还有一个额外的 oom_score_adj 可用于控制权重)
-
可以回收很多内存; -
可以避免缓解 OOM 后,该进程后续对内存的抢占引发后续再次的 OOM。
管理容器的内存资源
(MoeLove) ➜ ~ docker run --help |grep 'memory'
--kernel-memory bytes Kernel memory limit
-m, --memory bytes Memory limit
--memory-reservation bytes Memory soft limit
--memory-swap bytes Swap limit equal to memory plus swap: '-1' to enable unlimited swap
--memory-swappiness int Tune container memory swappiness (0 to 100) (default -1)
可用的配置参数有上述几个,我们通常直接使用 --memory 参数来限制容器可用的内存大小。我们同样使用几个示例进行介绍:
(MoeLove) ➜ ~ docker run --rm -it --memory 10m alpine
/ #
那我们如何验证它的可用内存大小是多少呢?在物理机上,我们通常使用 free 工具进行查看。但在容器环境内,它还是否生效呢?
/ # free -m
total used free shared buffers cached
Mem: 15932 14491 1441 1814 564 3632
-/+ buffers/cache: 10294 5637
Swap: 8471 693 7778
很明显,使用 free 得到的结果是宿主机上的信息。当然,我们前面已经介绍了 docker stats 命令,我们使用它来查看当前的资源使用情况:
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
e260e91874d8 busy_napier 0.00% 1.172MiB / 10MiB 11.72% 16.1kB / 0B
0B / 0B 1
可以看到 MEM USAGE / LIMIT 那一列中的信息已经生效,是我们预期的样子。
# 在容器内执行
/ # cat /sys/fs/cgroup/memory/memory.limit_in_bytes
10485760
或者可以在宿主机上执行以下命令:
(MoeLove) ➜ ~ cat /sys/fs/cgroup/memory/system.slice/docker-$(docker inspect --format '{{ .Id}}' $(docker ps -ql)).scope/memory.limit_in_bytes
10485760
注意:以上命令在 Linux 5.2 内核下测试通过,不同版本之间目录结构略有差异。
更新容器内存资源限制
(MoeLove) ➜ ~ docker update --memory 20m $(docker ps -ql)
e260e91874d8
# 验证是否生效
(MoeLove) ➜ ~ docker stats --no-stream $(docker ps -ql)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
e260e91874d8 busy_napier 0.00% 1.434MiB / 20MiB 7.17% 35.3kB / 0B 0B / 0B 1
如果还不够,需要扩大至 100m 呢?
(MoeLove) ➜ ~ docker update --memory 100m $(docker ps -ql)
Error response from daemon: Cannot update container e260e91874d8181b6d0078c853487613907cd9ada2af35d630a7bef204654982: Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time
会发现这里有个报错信息。大意是 memory limit 应该比已经配置的 memoryswap limit 小,需要同时更新 memoryswap。
内存限制参数的特定行为
(MoeLove) ➜ ~ cat /sys/fs/cgroup/memory/system.slice/docker-$(docker inspect --format '{{ .Id}}' $(docker ps -ql)).scope/memory.limit_in_bytes
10485760
本文节选自GitChat专栏,戳链接查看详情:https://gitbook.cn/m/mazi/comp/column?columnId=5d70cfdc4dc213091bfca46f
文章评论