容器 CPU 和 Memory 限制行为简述

这篇是给之前没啥容器经验的选手准备的一篇文章，主要是讲一下容器的 CPU 和 Memory 限制行为。

CPU 限制

首先 Mac 或者是 Windows 选手在使用 Docker Desktop 的时候，会设置 Docker Desktop 的 CPU 限制，默认是 1，也就是说 Docker Desktop 只能使用 1 个 CPU。这是因为 Docker Desktop 裹了一层虚拟机（Windows 下应该是 WSL2/Hyper-V，Mac 下可能是 QEMU）。这相当于我们在一个特定 CPU 数量的宿主机中跑 Docker

首先提到 CPU 限制，本质上是限制进程的 CPU 使用的时间片，在 Linux 下，进程存在三种调度优先级

SCHED_NORMAL

SCHED_FIFO

SCHED_RR

1 用的是 Linux 中 CFS 调度器，而常见普通进程都是 SCHED_NORMAL 。OK 前提知识带过

说回容器中的 CPU 限制，目前主流语境下，容器特指以 Docker 为代表的一系列的基于 Linux 中 CGroup 和 Namespace 进行隔离的技术方案。那么在这个语境下，CPU 限制的实现利用了 Linux CGroup 中三个 CPU Subsystem。我们主要关心的如下四个参数

cpu.cfs_period_us

cpu.cfs_quota_us

cpu.shares

cpuset.cpus

现在分别来聊一下

首先说 cpu.shares，在 Docker 中的使用参数是 --cpu-shares，本质上是一个下限的软限制，用来设定 CPU 的利用率权重。默认值是 1024。这里对于相对值可能理解有点抽象。那么我们来看个例子 假如一个 1core 的主机运行 3 个 container，其中一个 cpu-shares 设置为 1024，而其它 cpu-shares 被设置成 512。当 3 个容器中的进程尝试使用 100% CPU 的时候（因为 cpu.shares 针对的是下限，只有使用 100% CPU 很重要，此时才可以体现设置值），则设置 1024 的容器会占用 50% 的 CPU 时间。那再举个例子，之前这个场景，其余的两个容器如果都没有太多任务，那么空余出来的 CPU 时间，是可以继续被第一个 1024 的容器继续使用的

接下来聊一下 cpu.cfs_quota_us 和 cpu.cfs_period_us ，这两个是需要组合使用才能生效，本质上含义是在 cpu.cfs_period_us 的单位时间内，进程最多可以利用 cpu.cfs_quota_us （单位都是 us），如果 quota 耗尽，那么进程会被内核 throttle 。在 Docker 下，你可以利用 --cpu-period 和 --cpu-quota 这两个值分别进行设置。也可以通过 --cpu 来进行设置，当我们设置 --cpu 为 2 的时候，容器会保证 cpu.cfs_quota_us 两倍于 cpu.cfs_period_us，剩下的就以此类推了（Docker 默认的 cpu.cfs_period_us 的阈值是 100ms 即 10000us）

现在已经聊了三个参数了，那么我们什么时候该用什么参数呢。通常来说，对于性能相对敏感的进程，我们可以使用 cpu.shares 来保证进程尽可能多的使用 CPU），业务进程可以利用 cpu.cfs_quota_us 和 cpu.cfs_period_us 来保证相对较好的公平分配。但是这样也带来一个问题，就是对于业务流量比较大的应用，可能会因为频繁被 throtlle 导致我们的 RT 等指标出现毛刺。Linux 5.12 之后有了一个新功能，cpu.cfs_burst_us ，即进程可以在 CPU 利用率比较低的空闲时段积累一定的 credit，然后在密集使用的时候换取一定的 buffer，实现更少的 throttle 和更高的 CPU 利用率（当然这个特性还暂时没有被主流容器所完全支持）

现在新的问题来了，无论 share 还是 cpu.cfs_quota_us 和 cpu.cfs_period_us 被 throttle 的概率都不少，如果我们想让进程更好的利用 CPU 怎么办？答案就是 cpuset.cpus ，Docker 中的参数是 --cpuset-cpus，可以让进程进行绑核处理

嗯，CPU 的部分就到这里

Mem 限制

还是前提科普

首先 Mac 或者是 Windows 选手在使用 Docker Desktop 的时候，会设置 Docker Desktop 的 Mem 限制，这相当于我们在一个特定 Mem 数量的宿主机中跑 Docker

然后在我们今天的语境下，Mem 资源的限制还是依托于 CGroup 的 Memory Subsystem，参数有很多，我们目前只需要关心

memory.limit_in_bytes

含义即是容器的最大内存限制，如果设置为 -1，代表着无任何内存的限制。在 Docker 中的参数是 --memory。

行为的话分为这样两种情况

如果系统内存还有空余，但是容器内存超过了 Limit, 那么容器进程会被 OOMKiller Kill 掉

如果系统内存先于容器达到了内核阈值，那么 OOMKiller 会在整个系统范围内根据根据负载等多个因素计算一个 score，然后 rank 后从高到低进行 OOM Kill 的操作

当然实际上还有一种额外的情况。可以通过 --oom-kill-disable 参数设置 memory.oom_control 的值。如果设置为 1，那么容器内存超过 Limit 就不会被 OOM Kill 掉而是会被暂停，如果设置为 0，那么容器内存超过 Limit 就会被 OOM Kill 掉

嗯关于 Mem 的行为差不多就这些

总结

差不多就这样吧，纯新手向的文章，水文一篇，大家别介意（