“容器性能损耗多少”缺少测量对象和实验条件。应用计算、内存申请、文件系统 copy-up、网络封装都在不同路径上。34只评估启动,不把 steady-state 性能一起平均。本篇先设定可以证伪的对照,再决定哪些数字能归因于容器边界。

把某个程序在宿主上执行十次、在容器里执行十次,得到两列数字后相除,并不能得出“容器比宿主慢 X%”:运行方式可能顺带改变了 CPU 配额、可用核心、进程 UID、文件路径、网络路由与镜像依赖。先用一种不碰磁盘、也不调用网络的短时计算任务建立最小对照;再单独讨论内存、OverlayFS 与网络。每次只改变一个前提,才能知道数字是哪个路径造成的。本篇没有专用 Docker/双 VM,数字一律留空,不用外部基准报告代替自己的实验。

逐项只改变一个条件

同一内核、架构、节点、二进制和输入,先在普通进程跑 bounded_load 的有界计算/内存路径,再在专用容器内跑同样命令;分别记录用户态时间、CPU/内存控制器、可运行 CPU 集与真实的 cgroup 限制。文件系统实验区分宿主相同盘的普通写入、容器可写层首次写入与同一镜像的重复写入;网络实验区分 loopback、veth、bridge 与跨主机封装,应用请求也要用相同连接/大小分布。每组预设预热、次数、并发、成功与失败计数,报告吞吐与分位数及原始数据,不可只挑最快一轮。

可比性从命令以外开始

共用 bounded_load.py 在给定壁钟时间内反复计算 SHA-256,可从程序原始 iterations 与 seconds 求每秒迭代数;它还可申请最多 32 MiB 的有界内存并逐页触碰。它不是 CPU 吞吐标准基准,也不模拟 HTTP 服务:解释它的结果只能限定为这份 Python 工作负载、指定 Python 版本、指定宿主与容器条件。独占 VM 上把同一个源码包解压作为两组输入,容器镜像使用 27 的 Dockerfile.c27,其中原样 COPY 了同一份 bounded_load.py。构建后记录 image digest、基础 Python 镜像 digest、host/container 的 Python 版本与实际架构;如果版本不同,即使脚本 SHA 一样,也不能把差异直接归因于“容器”。

普通进程可能被宿主 systemd 用户 slice 的 cpu.max/memory.max 限制,而 Docker 容器被另一个 cgroup 控制;容器内 nproc 与宿主可运行 CPU 集还可能不同。测前后要保存 /proc/self/cgroup、/proc/self/mountinfo 和实际 CPU affinity,按成员路径与挂载根映射到进程自身的 cgroup,再读取该目录的 cpu.max、cpuset 和内存上限,并确认运行任务的子进程继承了同样限制。采样器记录这一路径来源,只读取当前组;上层组的有效限额以及 cgroup namespace 外不可见的祖先仍须另查。仅把 Docker 命令上的 --cpus 1 与宿主“机器有一核可用”比较,并不表示配额一致;即使两个 cgroup 的 cpu.max 字面相同,也要检查上层组、邻居任务与 CPU 频率变化。不具备相同控制条件时,可以报告两个环境的原始结果,但要标为不可直接归因,不输出“隔离损耗”。

测量工具自身也会占用时间:每次执行 docker run --rm 包含镜像检查、容器创建和退出清理,不能把外层命令的总壁钟时间当成 bounded_load.py 计算时间。本篇采样脚本保存程序输出的迭代数/秒数和父进程的启动耗时,结果比较只使用相同的程序内部工作段;外层耗时单列供 34 的启动问题参考。宿主脚本与容器脚本需要在各自目标环境内运行相同十轮、同一顺序或预先定义的交错顺序,避免一组总在另一组预热后执行。每轮另存退出码和 stderr;程序意外退出绝不能计作每秒零迭代后混进均值。

同一台实验机不可能同时把两组都当成绝对独立样本,后台负载和温度会随时间漂移。先在专用机器预热,再在 host/container 之间交错执行十轮,每轮固定并发 1 和 --seconds 1 --memory-mib 8。实际报告写出每轮的 iterations/seconds、失败轮数、算术或中位摘要以及测试顺序,不仅留一个最高吞吐。对只有十个样本的延迟数据给出 p99 看似精细,实际上容易只等于最大值;需要报告原始数组和样本量,不把低样本分位数伪装成稳态服务的尾延迟分布。

实验记录还要解释容器内外是否真的调用了同一个程序:可分别计算两个 bounded_load.py 的 SHA-256、列出解释器版本、哈希库实现与传入参数。仅见相同文件名不够,因为基础镜像里的 Python 和宿主 Python 可能不同,依赖的系统库也可能不同。先让每组运行同一份自检输入并保存输出格式,若发现 iterations 字段含义或终止时间语义不同,就应停止对比,而不是事后调系数“校准”。负载只有一秒,调度抖动的影响也可能很大;重复运行是为了观察这种波动,不是为了挑出吻合预期的一轮。

如果机器上已有不可控任务,也不要靠一个很大的 CPU limit 假装独占算力。运行前后记录可运行任务和相关 cgroup cpu.stat/CPU pressure;若两组发生在不同 CPU 频率/温度、NUMA 节点或邻居负载下,先重新规划专用节点,再决定哪些局部结果仍可报告。实验里更改 cpuset、配额或并发属于新的条件,应作为新组单列而不是混入老组。即使在理想环境下某一轮容器表现更好,也不应反向说“隔离提高了通用性能”;先用原始输出和对照变量解释这一次样本为何不同。

CPU 与内存要对照不同问题

CPU 的“慢”可能是 CFS 带宽配额在 cpu.max 触发节流,也可能是进程本身计算更少、被系统调度到低频核或与其它任务争同一核。读取 cpu.stat 的 nr_throttled/throttled_usec 差分,配合程序吞吐,才能判断限额是否是候选因素;字段缺失需要确认控制器启用情况,不能补零。即使节流次数变多,单轮应用表现仍须通过完整的迭代数、CPU affinity 与同一时间窗对照。更重要的是,两个环境若限额不同,观察到的吞吐差是资源策略差异的一部分,不应归纳为 namespaces 的成本。

内存实验把 --memory-mib 8 与进程真实 RSS、cgroup memory.current、memory.events 分开:脚本申请并触碰的 8 MiB 不是进程最终总占用,还包含解释器、库和匿名内存。内存压力可能引发回收或直接 OOM,后者属于失败结果,不应当作“内存吞吐较低”的数据点。相同上限下,容器的基础进程数量和应用依赖也会影响总体记账;比较 memory.current 时要锁定采样层级,而不是把宿主用户 slice 与容器子 cgroup 数值混为一谈。若只改变了镜像基础层,哪怕 CPU 结果相同,内存占用也可能不同,这提示需要分离运行时边界和制品内容差异。

copy-up 只作用在特定文件路径

OverlayFS 下对仅存在于 lower 层的文件执行会修改内容的操作,可能先把数据或元数据 copy-up 到 upper 层;内核 v5.10 fs/overlayfs/copy_up.c 是这一实现的固定阅读定点。但是否发生数据 copy-up、触发何种操作、是否启用 metacopy、文件大小和存储设备,都影响实际耗时。普通“写一个新文件”与“第一次修改 lower 文件”不等价;在 PVC/bind mount 上写文件又绕过容器 rootfs 可写层。要比较需先验证文件确实来自 lower、第一次写的 upper 变化、第二次写时已进入 upper,同时固定底层盘和页缓存条件。没有 mountinfo 与上下层证据,仅见第一次写耗时更长,不能断言 copy-up。

文件系统路径还有不易看见的变量:容器层、宿主普通路径是否同一物理设备、文件是否 fsync、写入是否只有 page cache 命中、是否使用 overlay 的 redirect/metacopy 选项。第二次写更快可能是页缓存热,也可能是 upper 文件已经生成;为了排除它们,建立宿主相同文件系统的两次写对照,记录每次前的内核缓存状态与完整写入命令。禁止在共享 VM 通过清空全局 page cache 做所谓冷组,必要时使用一次性 VM 快照或明确只作热组观察。即使本篇 CPU 探针已跑通,也不能顺带把文件系统列宣布验收。

网络路径更不可能只有一个数字

容器内部回环、同节点 veth/bridge、宿主端口发布、跨节点封装的路径不同,还可能存在 NAT、conntrack、策略与真实应用处理时间。一次跨节点 HTTP 请求多出的时延,先看源/目标 Pod 所在节点、目的 IP、路由、MTU、DNS 解析与是否复用 TCP 连接;不能把应用首次建连 DNS 的耗时当成“veth 性能”。实验需要在同一源/目的节点、相同报文大小、连接复用方式、请求频率下对照各路径,双端保存实际响应、失败数与抓包。如果本机没有 ip 与双 VM,网络列为 NOT_RUN;CPU bounded_load 的纯本机结果不具备任何网络开销证据。

当测得一条网络路径的平均值略优却有更多超时,不应为了给“容器损耗”报一个漂亮数字而只保留成功请求。请求分位数必须附带状态码和错误比例,连接超时有明确上限,失败请求不能像成功请求那样直接纳入 p99 计算。跨节点 VXLAN 的 MTU 故障还可能只影响大请求体,小包看起来完全正常,因此报文大小也要进入控制变量表。数据流穿过 Sidecar、Service 或网关时,额外代理处理属于另一个实验条件,不能混作内核隔离层开销。

从一份受限结果迁移到下一类问题

同源有界 CPU/内存采样器在目标环境执行共用脚本,保存每轮的原始输出、计数和运行中可见 cgroup 条件;只组合包内两份探针的构建入口与 四路径前提与未运行列可独立取得,本篇附件校验与 独占 VM 的对照入口可分别下载,并复用 共用源码包。脚本不会产生真实文件系统 copy-up 或网络跨主机结果,其两列严格保留 `NOT_RUN`,不能从有界 CPU 样本推算。

当有人问“隔离边界是否导致性能差”时,先填测量对象、资源/缓存/网络路径和真实失败,再问是否只改变隔离这一变量。若一个条件不相等,报告条件差异而非强算百分比;若两组条件真正可控,仍仅对这台节点、这个 runtime/内核/镜像和这份负载下的结果负责。当前云端没有 Docker/containerd、专用相同限额 cgroup、双 VM 或独立网络路径,四类完整性能对照均 NOT_RUN,不能由代码静态运行或站点构建推导通用性能结论。

失败或反例是把一个组放在远端存储、另一个组放在本地盘,却将延迟差归因于 OverlayFS;或让一组命中页缓存、另一组做冷读取,还声称 cgroup 导致 I/O 变慢。必须逐项记录存储设备、缓存条件、CPU 亲和性、流量路径、镜像 digest 和安全策略,结果可解释时才写局部结论。实验矩阵与相同负载给出可复跑入口。本环境无运行时、独立双 VM,容器/普通进程对照 NOT_RUN,没有可报告的吞吐、分位数或“通用百分比”。

现象 控制变量 边界
CPU 差异大 cgroup 限额、cpuset/邻居 不归因于 namespace 本身
首次文件写慢 copy-up 与缓存状态 不推广到所有 snapshotter
网络 p99 高 路由、封装及失败数 不以平均值覆盖失败

练习

  1. 先预测在完全相同 CPU limit 下,普通进程与容器的有界负载耗时应怎样;锁定 cpuset 与并发后重复测量并展示原始数据。
  2. 预测同一文件第一次写与第二次写的延迟差是否能证明 OverlayFS 固有损耗,增加普通进程/相同存储与缓存对照后再归因。

上一篇:34:冷启动;下一篇:36:交付与回退。

系列总目录:从进程隔离到运行时与编排。

参考资料