这一章在做什么
回答一个问题:在虚拟机和容器里,为什么指标「看着不对」,该去哪里找真话。书里比较三种虚拟化:硬件虚拟机、容器(操作系统虚拟化)、轻量虚拟机,看各自的开销、资源控制,以及在宿主上和在客户机里各能看见什么。最该带走的是:限额比物理资源先用完,所以先查限额;容器里的 top、free、uptime 多半报的是整台宿主;CPU 节流、steal、内存上限各有自己的计数,要自己去读。
什么时候翻这一章
- 容器里的服务时不时卡几十毫秒,CPU 看着却不满多半是 CPU 限额在节流:配额在一个周期的前一小段就被几个线程用光,剩下的时间全体停着。§11.3.3§11.3.4场景手册 →
- 云主机里 vmstat 的 st 一直不为零你的虚拟 CPU 想跑却没轮到,时间给了邻居或宿主自己。客户机里能看到量,看不到原因。§11.2.4场景手册 →
- 容器里 free 说还有十几 GB,进程却被 OOM 杀了free 报的是宿主的内存;容器的上限在 cgroup 的 memory.max 里。§11.3.3场景手册 →
- 容器里 uptime、iostat 显示很忙,自己的服务却闲着这些工具读的是整台宿主,算进了别的租户。§11.3.4
- 测试时很快,上线和别的服务挤在一起就慢了几倍测试时在空机器上「借」了闲置 CPU,上线后借不到了。份额只保证下限。§11.3.3
- 在宿主上排查某个容器或虚拟机怎么把容器里的 PID 对到宿主上,怎么进它的命名空间跑工具,虚拟机的退出原因怎么看。§11.2.4§11.3.4
这一章不讲新的资源,讲的是第 6–10 章的方法搬进虚拟化环境以后哪里要改。USE 方法要把每一种限额当成一种资源来查(§2.5.9);CPU 那一章的 steal(§6.6.2)、限额(§6.9.8)在这里展开。
先用中文过一遍
三种虚拟化,差在有几个内核
硬件虚拟化给每个客户机一台「假电脑」,客户机跑自己的内核;容器只有一个内核,靠内核把进程分成几组、互相看不见;轻量虚拟机(MicroVM,书里以 Firecracker 为例)还是一客户机一内核,但虚拟机管理程序(hypervisor)做得很小,启动快、占内存少。
有几个内核,决定了两件事。一是开销:虚拟机里用户态代码直接在 CPU 上跑,开销主要出在特权操作、内存地址的二次翻译和 IO 上,有硬件支持后已经很小;容器在 CPU 和内存上没有额外开销,代价是大家挤同一个内核里的锁、缓存和队列(§11.2.2、§11.3.2)。二是谁看得清:虚拟机的用户有自己的内核,perf、BPF 都能用,但看不到宿主;容器的运营方在宿主上什么都看得见,容器里的用户却多半用不了内核跟踪工具(§11.6)。书里点出的尴尬是:从虚拟机搬到容器,更容易撞上内核里的争用,同时失去查它的手段。
steal:虚拟 CPU 想跑却没轮到
虚拟机里的 CPU 是虚拟 CPU(vCPU),由宿主调度到物理 CPU 上。宿主把这颗物理 CPU 给了别的客户机,或者拿去处理 IO、执行实例类型的限额,你的 vCPU 就得等。客户机内核把这段时间记成 steal(被偷走的时间),vmstat 的 st、mpstat 的 %steal 就是它(§11.2.4)。书里说这是少见的「知道自己被虚拟化了」的指标。
读它要注意两点。第一,它只告诉你被拿走了多少,不告诉你被谁拿走:客户机看不到邻居,原因得问云厂商或者换实例。第二,它在客户机里算,以 KVM 为例,记的是 vCPU 有活要干、却没被宿主调度的时间;vCPU 自己闲着时,宿主拿走它不算 steal。所以 steal 高说明「你想要的 CPU 没给足」,和利用率、排队一起看才完整。
容器:命名空间管看见什么,cgroup 管用多少
Linux 内核里没有「容器」这个东西。Docker 之类的软件用两样内核功能拼出容器:命名空间(namespace)决定一组进程能看见什么(自己的进程号、挂载点、网卡、主机名),控制组(cgroup)决定它们能用多少(CPU、内存、IO、进程数)。所以容器里的 1 号进程在宿主上是另一个 PID,宿主上用 grep NSpid /proc/PID/status 能看到两边的对应(§11.3.1、§11.3.4)。
这两样东西分开做,留下了这一章最大的坑:很多统计并没有按命名空间隔离。/proc 下的 stat、meminfo、loadavg 在常见配置下仍是整台宿主的数,于是容器里的 top 头部、uptime、free、vmstat、mpstat、iostat 报的都是宿主;ps、pidstat 只列容器自己的进程,因为 PID 命名空间隔离了进程表。本站实测:在一个限了半个 CPU、256 MB 内存的 cgroup 里,nproc 仍然说 16,free 仍然说有 11 GB 可用。用这些数给线程池、缓存定大小,就会定错。
真话在 cgroup 的目录里。cgroup v2 下,cat /proc/self/cgroup 给出自己所在的路径,前面加上 /sys/fs/cgroup 就是目录:cpu.max 是 CPU 限额,cpu.stat 是用量和节流,memory.max、memory.current 是内存的上限和当前用量。
CPU 限额:配额、周期和节流
cgroup 管 CPU 有三种手段(§11.3.3)。cpuset 把整颗 CPU 划给容器,稳定但闲着也不让别人用。份额(v1 叫 shares,v2 叫 weight)只在大家都抢的时候按比例分,没人抢时可以「借」闲置的 CPU,这叫突发(bursting)。带宽限额是硬上限:每个周期(默认 100 ms)给一份配额,用完就停到下个周期。cgroup v2 里写成 cpu.max 的两个数,200000 100000 就是每 100 ms 给 200 ms,相当于两颗 CPU。
配额是所有线程合用的。一个有 4 个线程的服务,限额一颗 CPU:4 个线程在 4 个核上同时跑,25 ms 就把 100 ms 的配额花完,然后全体停 75 ms。平均下来它确实只用了一颗 CPU,可每个请求都可能撞上一次 75 ms 的停顿。这时宿主上大半的核空着,mpstat 一切正常;只有 cpu.stat 里的 nr_throttled(被掐的周期数)和 throttled_usec(被掐的时间)在涨。下面「动手」一节量出的就是这个数。
内存限额:上限之内,page cache 也算
虚拟机的内存是配置时定死的,客户机内核自己在这个范围里换页、回收。容器则由 memory cgroup 管:容器读写文件产生的 page cache 记在容器名下,不用另外设置(§11.3.3)。cgroup v2 里 memory.max 是硬上限,用量回收不下来就在这个 cgroup 里触发 OOM;memory.high 是软一些的线,超过后进程被放慢、被迫回收。
读数时要分清两种内存:memory.stat 里的 anon 是堆和栈,回收它只能换出;file 是 page cache,大多可以直接丢。memory.current 贴着 memory.max 不一定危险,可能只是缓存把空间用满了;危险的信号是 memory.events 里 max、oom_kill 在涨,或者 anon 一路长。
| 英文 | 中文 | 一句话 |
|---|---|---|
| hypervisor / VMM | 虚拟机管理程序 | 创建和调度虚拟机的软件,书里按它跑在硬件上还是宿主内核里分两种配置 §11.2 |
| host / guest / tenant | 宿主 / 客户机 / 租户 | 物理机是宿主,上面的虚拟机或容器是客户机,客户机的使用者是租户 §11.1 |
| vCPU | 虚拟 CPU | 客户机看到的 CPU,由宿主调度到物理 CPU 上 §11.2.3 |
| steal time | 被偷走的时间 | vCPU 想跑却没被宿主调度的时间,vmstat 的 st §11.2.4 |
| guest exit | 客户机退出 | vCPU 遇到要宿主处理的事件时停下、交给宿主;退出越多越久,开销越大 §11.2.2 |
| SR-IOV / PCI pass-through | 单根 IO 虚拟化 / PCI 直通 | 让客户机直接访问硬件,IO 快了,宿主却可能看不见这些 IO §11.2.2 |
| namespace | 命名空间 | 过滤进程能看见的东西:PID、挂载、网络、主机名等 §11.3.1 术语页 |
| cgroup (control group) | 控制组 | 限制和统计一组进程用多少资源;v2 是一棵统一的树 §11.3.1 术语页 |
| CPU shares / weight | CPU 份额 / 权重 | 大家都抢时按比例分,只保证下限,没人抢时可以多用 §11.3.3 |
| CPU bandwidth (quota / period) | CPU 带宽限额(配额 / 周期) | 每个周期给一份 CPU 时间,用完就停到下个周期,硬上限 §11.3.3 术语页 |
| throttling | 节流 | 被限额掐住;CPU 的看 cpu.stat 的 nr_throttled 和被掐的时间 §11.3.4 |
| bursting | 突发 | 借用别人闲着的资源,跑得比自己的份额快;邻居一忙就没了 §11.3.3 |
| noisy neighbor | 吵闹的邻居 | 同一台宿主上大量占资源、拖慢别人的租户 §11.1.5 |
| MicroVM | 微型虚拟机 | 轻量虚拟化:一客户机一内核,但管理程序极小、启动快 §11.4 |
方法:按什么顺序查
书里的原则只有一句:限额比物理上限先到,所以先查限额(§11.3.4)。宿主运营方和容器用户都该这么做;USE 方法把每一种限额当作一种资源,查它的利用率、饱和度和错误(§2.5.9)。落到一次排查上,本站整理成下面几步,前三步在容器或客户机里就能做,不要 root。
- 我在哪种环境里?
systemd-detect-virt说出是哪种虚拟机或容器;lscpu的Hypervisor vendor一行;cat /proc/self/cgroup给出所在的 cgroup,只有一行0::/…就是 cgroup v2。环境不同,下面能信哪些数也不同。§11.2.4§11.3.4 - 有哪些限额?
在自己的 cgroup 目录里读
cpu.max、cpu.weight、cpuset.cpus.effective、memory.max、memory.high、pids.max。max表示没限。把这些数记下来,后面所有利用率都按它算,而不是按nproc和free。§11.3.3 - CPU 是被限额掐住,还是被邻居挤了?
隔几秒读两次
cpu.stat:nr_throttled和throttled_usec在涨,就是带宽限额在节流。用了 cpuset,就看那几颗 CPU 是不是满了。都不是,而进程的nonvoluntary_ctxt_switches(/proc/PID/status)在涨、宿主上没有空闲的 CPU,那是份额之争:别的租户也在抢。虚拟机里看vmstat 1的st。§11.3.4§11.2.4 - 内存离上限多远?
memory.current对memory.max;memory.stat里anon和file各多少;memory.events的max、oom_kill有没有在涨;memory.pressure给出因为内存而停顿的时间比例。§11.3.3§11.3.4 - IO 和网络:先看延迟
虚拟磁盘、虚拟网卡的「忙」不说明什么,因为你不知道背后是什么设备;看延迟。虚拟机里有自己的内核,可以用 BPF 工具逐个看 IO 的延迟,找出既不是因为 IO 大、也不是因为排在写回后面的离群值,那多半是物理上的争用。网络的限速常常只有宿主或云厂商的接口看得到。§11.2.4
- 到宿主上接着查
能上宿主时:
systemd-cgtop按 cgroup 排出谁在用 CPU 和内存;grep NSpid /proc/PID/status对上容器里外的 PID;nsenter -t PID -m -p top进它的命名空间跑工具。KVM 的客户机在宿主上是 qemu 进程,pidstat -t -p PID 1看每个 vCPU 线程,perf kvm stat live看退出原因(要 root)。§11.2.4§11.3.4
工具速查
| 想知道 | 敲什么 | 看哪里 | 节 |
|---|---|---|---|
| 在哪种虚拟化里 | systemd-detect-virtlscpu | 输出 kvm、microsoft、docker 之类(本站的 WSL2 上输出 wsl);lscpu 的 Hypervisor vendor | §11.2.4 |
| 所在 cgroup、是不是 v2 | cat /proc/self/cgroupstat -fc %T /sys/fs/cgroup | 一行 0::/路径、文件系统类型 cgroup2fs 就是 v2;v1 是一堆按控制器分开的行 | §11.3.1 |
| CPU 限额 | cat cpu.max cpu.weight | 配额 周期,两数相除是几颗 CPU;max 100000 是不限 | §11.3.3 |
| 有没有被节流 | cat cpu.stat,隔几秒再读一次 | nr_throttled / nr_periods 的增量比例;throttled_usec 在涨就是限额在掐 | §11.3.4 |
| 内存离上限多远 | cat memory.current memory.maxcat memory.events | 上限是不是 max;memory.stat 的 anon 和 file;oom_kill 不为零就是被杀过 | §11.3.3 |
| 按 cgroup 的停顿 | cat cpu.pressure memory.pressure | 和 /proc/pressure/* 一样的格式,但只算这个 cgroup 里的任务 | §11.3.4 |
| 虚拟机被偷了多少 CPU | vmstat 1mpstat -P ALL 1 | st / %steal 持续不为零;看是不是所有 vCPU 都有 | §11.2.4 |
| 宿主上谁在用 | systemd-cgtop | 按 cgroup 排的 %CPU、内存、任务数;容器放在哪一层随运行时和 cgroup 驱动而变,本站的 WSL2 上是 /docker 和 /kubepods | §11.3.4 |
| 容器里的 PID 在宿主上是谁 | grep NSpid /proc/PID/statusls -l /proc/PID/ns/ | NSpid 第一个数是宿主的 PID,后面是各层命名空间里的;命名空间 ID 相同才是同一个 | §11.3.4 |
| 进容器的命名空间跑工具 | nsenter -t PID -m -p topnsenter -t PID -n ss -tin | -p 只看到容器的进程,但 top 头部仍是宿主的数;-n 用容器的网络栈 | §11.3.4 |
| 有哪些命名空间 | lsns | 每个命名空间的类型、进程数、第一个进程 | §11.3.1 |
| 容器平台的汇总 | docker statskubectl top pod | docker stats 给每个容器的 CPU 百分比(可超过 100%)和内存用量对上限;kubectl top pod 给的是 CPU 核数(250m 是四分之一核)和内存用量,要另外对照 limit | §11.3.4 |
| KVM 客户机为什么退出 | perf kvm stat live | 退出次数少且多是 HLT:客户机闲着;IO 类退出多:它在做虚拟磁盘、网卡的 IO | §11.2.4 |
| Xen 各客户机的用量 | xentop | 每个域的 CPU、内存、虚拟网卡和块设备;VBD_OO 是块设备请求排队 | §11.2.4 |
表里的 cgroup 文件都在自己的 cgroup 目录下(/sys/fs/cgroup 加上 /proc/self/cgroup 第三段),读它们不要 root;书里的例子是 cgroup v1 的文件名,对照见「成书之后」。systemd-cgtop、systemd-detect-virt、systemd-run 随 systemd 自带;nsenter、lsns 在 util-linux 里;vmstat 在 procps,mpstat、pidstat 在 sysstat。宿主上的 nsenter、perf kvm、读别的用户的 /proc 要 root。工具名链到本站的工具卡:vmstat、mpstat、systemd-cgtop、nsenter、lsns、perf、xentop。
动手:在自己的机器上试
给自己套一个 CPU 限额,量出节流的停顿
下面这个程序起几个线程空转,每个线程不停地读时钟,记下两次读之间最长隔了多久。不限额时这个间隔只有一两毫秒(被别的进程抢一下)。先猜:限额一颗 CPU、跑 4 个线程,最长会停多久?
// stall.c:N 个线程各自空转,记下两次读时钟之间最长停了多久
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
static double now_ms(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec * 1e3 + t.tv_nsec / 1e6;
}
static void *spin(void *arg) {
double start = now_ms(), last = start, worst = 0;
long stalls = 0; // 停了 5 ms 以上的次数
while (last - start < 3000) { // 跑 3 秒
double t = now_ms();
if (t - last > worst) worst = t - last;
if (t - last > 5) stalls++;
last = t;
}
printf("thread %ld: longest stall %.1f ms, stalls>5ms: %ld\n",
(long)arg, worst, stalls);
return NULL;
}
int main(int argc, char **argv) {
int n = argc > 1 ? atoi(argv[1]) : 4;
pthread_t th[32];
if (n < 1 || n > 32) n = 4;
for (long i = 0; i < n; i++) pthread_create(&th[i], NULL, spin, (void *)i);
for (int i = 0; i < n; i++) pthread_join(th[i], NULL);
return 0;
}
$ gcc -O2 -pthread stall.c -o stall # 不限额,作对照 $ ./stall 4 # 放进一个限额 1 颗 CPU 的临时 cgroup 里跑,跑完读它的 cpu.stat $ systemd-run --user --scope --quiet --expand-environment=no -p CPUQuota=100% sh -c './stall 4; cat /sys/fs/cgroup$(cut -d: -f3 /proc/self/cgroup)/cpu.stat'
本站在 16 核的 WSL2 上看到的(各截了一部分):
不限额: thread 0: longest stall 1.9 ms, stalls>5ms: 0 限额后: thread 0: longest stall 80.0 ms, stalls>5ms: 31 usage_usec 3111431 nr_periods 31 nr_throttled 31 throttled_usec 9037843
- 每个线程 3 秒里停了 31 次,每次七八十毫秒:每个 100 ms 的周期里,4 个线程各跑 25 ms 左右,合起来就花完了配额,然后一起等。
nr_throttled等于nr_periods:每个周期都被掐了。usage_usec约 3.1 秒,3 秒多的墙钟时间正好用掉一颗 CPU,限额起作用了。throttled_usec是 9 秒,比墙钟时间还长:从数上看,它是每个 CPU 上被掐的时间加在一起(4 个核 × 31 次 × 约 75 ms)。所以它只适合看涨没涨、涨得快不快,不要拿它和墙钟时间直接比。- 试试
./stall 1:一个线程用不完一颗 CPU 的配额,nr_throttled基本是 0;./stall 2时最长停顿变成 50 ms 左右。线程数按nproc定、限额却只有一两颗 CPU 的服务,就是 4 个线程这一种。 - 节流的同时在另一个终端跑
mpstat 1:本站看到全机 85% 空闲、%steal是 0。只看全机指标,这个服务没有任何问题。
再看一眼限额里的程序以为自己有多少资源:
$ systemd-run --user --scope --quiet --expand-environment=no -p CPUQuota=50% -p MemoryMax=256M sh -c 'nproc; free -m; C=/sys/fs/cgroup$(cut -d: -f3 /proc/self/cgroup); cat $C/cpu.max $C/memory.max' # 对照:nproc 认 CPU 亲和性(cpuset 就是靠它);本站的 coreutils 9.7 不认配额 $ taskset -c 0,1 nproc
nproc: 16 free: total 14874 available 11713 (MB) cpu.max: 50000 100000 memory.max: 268435456 taskset 后: 2
限了半颗 CPU、256 MB,nproc 和 free 完全没察觉。nproc 会随 CPU 亲和性变(cpuset 也是靠亲和性生效的),配额它不管;这是本站 coreutils 9.7 的行为,2025 年 9 月发布的 coreutils 9.8 起 nproc 会参考 cgroup v2 的配额(--all 仍然不管),先在自己机器上试一下。照 nproc 起线程池、照 free 定缓存大小的程序,放进容器就会定错,要改成读 cgroup 的文件,或者让部署时把数传进来。
- WSL2
- 本站实测过上面所有命令,没有用 root。能这样做,是因为本机的 systemd 把 cpu、memory、pids 三个控制器委派给了用户(看
user@UID.service那个 cgroup 目录里的cgroup.controllers)。本机的 WSL2(Debian 13)只挂了 cgroup v2。 - 服务器
- 没有委派时
systemd-run --user设不了限额,用 root 的systemd-run --scope -p CPUQuota=100%,或者docker run --cpus=1。生产上要看已有容器的节流,直接读它的 cgroup 目录里的cpu.stat,路径用/proc/容器进程的PID/cgroup找,各种运行时放的位置不一样。 - ARM 板
- 要内核开了 cgroup 和 CPU 带宽控制(
CONFIG_CGROUPS、CONFIG_CFS_BANDWIDTH,看你的内核配置)。没有 systemd 时用 root 手工建:mkdir /sys/fs/cgroup/t,往cgroup.subtree_control写+cpu,往t/cpu.max写100000 100000,再把 PID 写进t/cgroup.procs。程序本身只用 pthread,交叉编译即可。
成书之后
cgroup v2 成了主流发行版的默认,v1 在退场
- 书里
- 说 Linux 有 v1 和 v2 两版 cgroup,Kubernetes 等很多项目还在用 v1,v2 在路上;预计容器技术几年内迁到 v2,Fedora 31 已经切换(§11.3.1)。§11.3.3、§11.3.4 的例子都是 v1 的文件。
- 今天
- runc 和 Kubernetes 的文档列出的默认用 cgroup v2 的发行版有:Fedora 31 起、Debian 11 起、Ubuntu 21.10 起、RHEL 9 及其衍生版起。Kubernetes 1.35 把 cgroup v1 支持标为弃用,kubelet 默认不再在 v1 节点上启动。systemd 258 去掉了 v1 支持,开机只挂 v2。判断自己的机器:有
/sys/fs/cgroup/cgroup.controllers就是 v2。读者的 WSL2(Debian 13)也是 v2。
书里的 v1 文件名,在 v2 里换了名字
- 书里
- CPU 看
cpu,cpuacct目录下的cpuacct.usage和cpu.stat的throttled_time(纳秒),内存看memory.limit_in_bytes,磁盘看blkio.throttle.*(§11.3.3、§11.3.4)。 - 今天
- v2 每个 cgroup 只有一个目录,各控制器的文件放在一起。CPU 用量在
cpu.stat的usage_usec,节流时间是throttled_usec(单位是微秒),限额是cpu.max,份额是cpu.weight;内存是memory.max、memory.high、memory.events;磁盘限速是io.max,统计在io.stat。v2 还给每个 cgroup 配了自己的cpu.pressure、memory.pressure、io.pressure(PSI)。以上 CPU、内存和 PSI 的文件本站在 WSL2(内核 6.6)上逐个读过。
带宽限额可以攒着突发:cpu.max.burst
- 书里
- 带宽限额是每个周期一份配额,用不完不能留到下个周期;书里管理突发的办法是份额加上限,或者提醒用户别依赖突发(§11.3.3)。
- 今天
- 内核给 CFS 带宽控制加了 burst:cgroup v2 的
cpu.max.burst允许把前面周期没用完的配额攒一点,留给后面的短时高峰;cpu.stat里随之多了nr_bursts、burst_usec。默认是 0,不开。对延迟敏感、负载一阵一阵的服务,它能减少上面那种整周期的停顿,代价是这段时间对邻居的干扰更大。
分节地图
这一章先铺背景(§11.1),再把三种虚拟化各按「实现、开销、资源控制、可观测性」四节走一遍(§11.2–11.4),最后是别的形态和一张对比。先读的是容器那一半:资源控制和可观测性两节最长,也是「指标看不准」的答案所在。虚拟机的 steal、宿主上的工具在 §11.2.4,云主机用户用到时再查。
- The rise of cloud computing solves…约 400 词开篇学习目标和四个部分,以及三种虚拟化在云上普及的先后。
- Background约 300 词背景宿主、客户机、租户、多租户这几个词怎么用。
- Instance Types约 200 词实例类型云厂商的实例家族和规格,名字会过时,看厂商文档即可。
- Scalable Architecture约 300 词可扩展的架构纵向和横向扩展的架构常识,和单机排查关系不大。
- Capacity Planning约 800 词容量规划自动伸缩会掩盖性能退化;突发是什么。
- Storage约 200 词存储云上的网络存储为什么比本地盘难预测。
- Multitenancy约 200 词多租户吵闹的邻居,和两种对策:资源控制与可观测性。
- Orchestration (Kubernetes)约 400 词编排(Kubernetes)Pod、Node、Service 的关系;调度和网络是两大性能问题。
- Hardware Virtualization约 700 词硬件虚拟化两种管理程序配置;二进制翻译、半虚拟化、硬件辅助的演进。
- Implementation约 400 词实现VMware、Xen、Hyper-V、KVM、Nitro 的来历,排查用不上。
- Overhead约 1800 词开销CPU、内存映射、内存、IO、多租户争用各自的开销从哪来。
- Resource Controls约 900 词资源控制vCPU、缓存划分、气球驱动;客户机内的优先级宿主看不见。
- Observability约 1900 词可观测性宿主上看 vCPU 线程和退出原因,客户机里看 steal 和设备延迟。
- OS Virtualization约 500 词操作系统虚拟化容器比虚拟机好在哪、差在哪;前两条缺点要合起来读。
- Implementation约 400 词实现命名空间和 cgroup 各管什么;内核里没有容器这个概念。
- Overhead约 700 词开销CPU 和内存没有额外开销;IO 多几层;主要问题是多租户争用。
- Resource Controls约 1300 词资源控制优先级和上限两类;CPU 的 cpuset、份额、带宽怎么配合。
- Observability约 3400 词可观测性容器里的工具报谁的数,宿主上怎么按容器查,节流怎么判断。
- Lightweight Virtualization约 300 词轻量虚拟化MicroVM 想兼得虚拟机的隔离和容器的轻快。
- Implementation约 200 词实现几个项目的来历和启动时间,名单会变。
- Overhead约 100 词开销和 KVM 相近,内存占用小得多。
- Resource Controls约 100 词资源控制管理程序是宿主上的进程,用宿主的 cgroup 管。
- Observability约 300 词可观测性宿主上整台虚拟机只是一个进程;客户机里的统计只算自己。
- Other Types约 300 词其他形态函数即服务、SaaS、Unikernel:没有可登录的系统时怎么分析。
- Comparisons约 300 词比较一张对比表;可观测性对运营方和用户各偏向哪种技术。
- Exercises约 200 词习题习题;本页的动手和诊断题可以代替。
词与短语
只收先读和用到再查两档的节。斜体那一行是原句里连续的几个词,在电子书里搜它就能找到那一句。
§11.1.3 Capacity Planning
§11.1.5 Multitenancy
§11.1.6 Orchestration (Kubernetes)
§11.2 Hardware Virtualization
§11.2.2 Overhead
§11.2.3 Resource Controls
§11.2.4 Observability
§11.3.1 Implementation
§11.3.2 Overhead
§11.3.3 Resource Controls
§11.3.4 Observability
§11.6 Comparisons
诊断题:换成你来查
场景和输出都是本站编的。先自己判断,再点选项看解释。
场景:你的 C++ 网关跑在 Kubernetes 里,CPU limit 是 2。程序按 std::thread 的 hardware_concurrency() 起了 32 个工作线程。p99 偶尔到 90 多 ms,平均延迟很好。节点上 mpstat 显示 70% 空闲。你在容器里隔 10 秒读了两次:
cpu.max: 200000 100000
cpu.stat: nr_periods 1200 → 1300
nr_throttled 410 → 49710 秒 100 个周期里 87 个被掐,节流非常频繁。hardware_concurrency() 返回的是节点的核数,不知道限额,32 个线程同时跑,配额很快用完,剩下的时间所有线程一起等,平均值看不出来,p99 看得出来。节点空闲说明的是物理资源,限额先到了。改法:按限额定线程数(2–3 个工作线程),或者提高 limit;加线程只会更快地花完配额。steal 是虚拟机的概念,容器里节流看的是 cpu.stat。
场景:一台 4 vCPU 的云主机跑行情推送服务,下午开始变慢,代码和流量都没变。
r us sy id wa st 6 48 9 3 0 40 5 45 10 2 0 43
st 是被偷走的时间:这台虚拟机的 vCPU 有活要干,宿主却把物理 CPU 给了别人,或者在执行实例类型的限额。id 只剩 2–3%,四成被偷,实际能用的 CPU 只有六成左右,r 高是结果不是原因。客户机里看不到是谁在占,能做的是确认实例类型有没有 CPU 积分之类的限制、找云厂商,或者换一台实例。等磁盘是 wa。
场景:一个做日志归档的容器,内存上限 1 GiB。监控报警说内存用到了 98%。你进去看:
memory.max: 1073741824
memory.current: 1052000000
memory.stat: anon 210000000
file 820000000
memory.events: max 0 oom_kill 0容器读写文件的 page cache 记在它的 cgroup 名下,98% 里有八成是 file,内核需要时可以回收。真正回收不了的是 anon,只有 200 MB。memory.events 的 max 和 oom_kill 都是 0,说明还没撞过上限。free 报的是宿主,和容器的上限无关;清缓存只是让数字好看,归档还会把缓存读回来。报警该按 anon 或 PSI 设,不按 memory.current。
场景:你在容器里排查一个编译服务,容器限额 4 颗 CPU,服务现在没有任务。
uptime: load average: 31.2, 29.8, 27.5
cpu.stat: usage_usec +40000 in 10s
nr_throttled +0在常见的容器配置里,/proc/loadavg 没有按容器隔离,uptime 和 top 头部显示的是宿主的负载,包括所有邻居。容器自己的 CPU 用量要看 cgroup:10 秒 40 ms,几乎为零,也没有节流。这时的高负载最多说明宿主很忙,可能影响你,但不是你造成的。负载平均给出三个窗口,不只是 15 分钟。
场景:一个图片转码服务,测试时在一台新节点上单独跑,每张图 40 ms。上线后和十几个服务挤在同一类节点上,每张图 200 ms。它只配了 CPU request(份额),没配 limit。生产上:
cpu.max: max 100000 nr_throttled: 0 node %idle: 0.5
没设 limit,所以不会节流,nr_throttled 为 0 不能排除 CPU。份额在没人抢时允许多用,在空节点上测出的是突发后的速度;生产节点几乎没有空闲,每个服务只能拿到按份额算的那一份。书里管这叫突发对容量规划的误导。办法是按份额测(在有负载的节点上测,或者临时设一个等于 request 的 limit),按真实需要申请 request。
值得停下来的句子
Since these limits, where present, are encountered long before the physical limits, they are more likely to be in effect and should be checked first.— §11.3.4。本站译:这些限额只要存在,就会远比物理上限先撞上,所以更可能正在起作用,应该先查。
前面十章教的都是物理资源:CPU 满没满、磁盘忙不忙。到了容器和云主机里,这些检查依然要做,但顺序要倒过来。一个限了 2 颗 CPU 的容器跑在 64 核的宿主上,物理 CPU 用到 3% 时它就到顶了;看 mpstat 永远看不到问题。先读 cpu.max、memory.max,再按它们去算利用率,本页「方法」的第二步就是这么来的。
This can lighten the support burden on container operators: people tend to blame what they cannot observe.— §11.3.4。本站译:这能减轻容器运营方的支持负担:人总爱把问题怪到自己看不见的东西上。
这句话说的是让容器看到宿主统计的好处,但反过来也是给排查者的提醒。看不见宿主,就容易把一切慢都推给「吵闹的邻居」;看得见宿主,又容易把宿主的数当成自己的。两种错都来自同一件事:没弄清手上这个数是谁的。每读一个指标,先问它的范围是整台机器、这个 cgroup,还是这个虚拟机。
带走的几条
一句话:先弄清手上的数是谁的,再先查限额、后查物理资源。
cpu.max、memory.max、cpuset 比物理资源先到顶。利用率要按限额算,不按 nproc 和 free。
§11.3.4隔几秒读两次,nr_throttled、throttled_usec 在涨就是被掐。宿主空闲、mpstat 正常时也会发生。
§11.3.4配额所有线程合用。4 个线程配 1 颗 CPU,实测每 100 ms 停约 75 ms,p99 被拉高。
§11.3.3top 头部、uptime、free、vmstat、mpstat、iostat 多半是整台机器;ps、pidstat 只列自己的进程。
§11.3.4vmstat 的 st 持续不为零,说明 vCPU 想跑没轮到。客户机里看不到原因,查实例类型或找云厂商。
§11.2.4memory.current 贴着上限可能只是缓存;anon 在涨、memory.events 的 oom_kill 不为零才危险。
§11.3.3空机器上测出的速度含突发,邻居一多就没了。按份额测,按真实需要申请。
§11.3.3