在我们使用容器的时候,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
可以看到当前的 CPU 使用率已经在 100% 左右了。
我们再新打开一个窗口,进入容器内,执行相同的命令:
(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 核的资源。
由此可以得出的结论是,如果不对容器内程序进行 CPU 资源限制,其可能会消耗掉大量 CPU 资源,进而影响其他程序或者影响系统的稳定。
(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 资源。
指定可使用CPU核
可以使用 --cpuset-cpus 来指定分配可使用的 CPU 核,这里我指定为 0,表示使用第一个 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 了。本文节选自专栏。
小结
通过上述的示例,我介绍了如何通过 --cpus 参数限制容器可使用的 CPU 资源;通过 --cpuset-cpus 参数可指定容器内进程运行所用的 CPU 核心;通过 docker update 可直接更新一个正在运行的容器的相关配置。
现在我们回到前面使用 docker run --help | grep CPU,查看 Docker 支持的对容器 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)
--cpus 是在 Docker 1.13 时新增的,可用于替代原先的 --cpu-period 和 --cpu-quota。这三个参数通过 cgroups 最终会实际影响 Linux 内核的 CPU 调度器 CFS(Completely Fair Scheduler, 完全公平调度算法)对进程的调度结果。
前面已经介绍了如何管理容器的 CPU 资源,接下来我们看看如何管理容器的内存资源。相比 CPU 资源来说,内存资源的管理就简单很多了。
同样的,我们先看看有哪些参数可供我们配置,对于其含义我会稍后进行介绍:
(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
在开始进行容器内存管理的内容前,我们不妨先聊一个很常见,又不得不面对的问题:OOM(Out Of Memory)。
当内核检测到没有足够的内存来运行系统的某些功能时候,就会触发 OOM 异常,并且会使用 OOM Killer 来杀掉一些进程,腾出空间以保障系统的正常运行。
这里简单介绍下 OOM killer 的大致执行过程,以便于大家理解后续内容。
内核中 OOM Killer 的代码,在 torvalds/linux/mm/oom_kill.c 可直接看到,这里以 Linux Kernel 5.2 为例。
引用其中的一段注释:
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.
翻译过来就是,当我们处于 OOM 时,我们可以有几种选择,随机地杀死任意的任务(不好),让系统崩溃(更糟糕)或者尝试去了解可以杀死哪个进程。注意,这里我们不需要追求完美,我们只需要变好(be good)就行了。
事实上确实如此,无论随机地杀掉任意进程或是让系统崩溃,那都不是我们想要的。
回到内核代码中,当系统内存不足时,out_of_memory 被触发,之后会调用 select_bad_process 函数,选择一个 bad 进程来杀掉。
那什么样的进程是 bad 进程呢?总是有些条件的。select_bad_process 是一个简单的循环,其调用了 oom_evaluate_task 来对进程进行条件计算,最核心的判断逻辑是其中的 oom_badness。
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;
}
当容器运行一段时间,其中的进程使用的内存变多了,我们想允许容器使用更多内存资源,那要如何操作呢?
我们仍然可以用前面介绍的 docker update 命令完成。
比如使用如下命令,将可用内存扩大至 20m:
(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