容器 31:指标如何归属到一次真实请求
监控面板中一条“容器 CPU 高”曲线,只要没绑定到同一个26 的 Pod UID/容器 ID/PID,就未必能解释这一次 /ready 延迟。最小可迁移做法是从业务请求返回时间追到当时进程,再读该进程所属 cgroup 的 CPU、内存和 I/O 计数器,而不是把不同采样窗口的曲线拼在一起。
最容易出错的是先找一张“看起来同时变高”的监控图,再往上套一个慢请求的故事。应用 200 的耗时包含连接建立、服务接收、进程排队与响应传输;cpu.stat 里的节流次数是某个 cgroup 的累积量。两者测量对象、时间窗口和统计单位不同。即使图上时间线重合,没有请求身份与 cgroup 路径的映射,因果链也不成立。本篇用共用 probe-app 与 bounded_load 构造可限定范围的对照,但不把还未运行的 YAML 当成 CPU 节流实测。
建立时间和身份的双重坐标
固定一份 bounded_load 的输入规模、持续时间与并发,在请求侧保留开始/结束时间、HTTP 状态与总耗时;在应用侧保留日志、PID 和真实服务延迟;在节点侧同一时间窗读取 cpu.stat、memory.events、io.stat 与压力指标。Counter 必须算增量,采样间隔、是否包含子 cgroup、任务迁移时的身份更换都要标记。cpu.stat 的节流增量能证明指定 cgroup 发生节流,但还需排除应用排队、网络超时与缓存变化才可把某次慢请求归因于 CPU。
请求身份不能停在 Pod 名称
请求端记录开始/结束的 UTC 与单调时钟时长、目标 URL、原始 HTTP 状态或异常;响应的 /identity 提供处理它的 PID、PID/mount/net namespace inode 和 /proc/self/cgroup。还需同时保存 Kubernetes Pod UID、容器 ID、所在节点和重启次数:Pod 名称可以重用,PID 数值会重用,容器重新创建也会换一个 cgroup 实例。对于一次超时,可能根本拿不到应用 /identity;这时只能沿客户端目的地址、EndpointSlice 与应用日志缩小范围,不能凭“应该落到这个后端”填入一个虚构 PID。
不把同名 Pod 的多次 kubectl exec 输出自动拼成同一组计数。如果测量脚本运行于与 probe-app 相同的容器 cgroup,脚本自己也是被测工作的一部分;基线与施压两个阶段应运行相同脚本并明确这一测量开销。若 kubelet 实际为容器和 Pod 分配不同层级,容器内 /sys/fs/cgroup/cpu.stat 与宿主 Pod cgroup 统计也不是同一个对象。本篇先在容器内读取 /proc/self/cgroup 和 cgroup v2 统计文件,并将结果限定为这一容器可见 cgroup;要归属 Pod 层的多个容器,需在宿主从固定容器 ID/进程核查父 cgroup 与可访问的层级,不能直接把两个数相加当成 Pod 总量。
Counter 的差分不等于请求的因果解释
Linux cgroup v2 文档说明 cpu.stat 有 usage_usec 等字段;启用 CPU 控制器后还会出现 nr_periods、nr_throttled 和 throttled_usec 等字段。文档同时提示部分限额统计不计入祖先 cgroup 的节流;没有字段时首先查控制器是否启用,不能填零伪装“本次未节流”。采样前后分别保存文件内容和 UTC,在同一路径、同一实例做每个 counter 的差分;差分为负常提示实例已变或读取口径错误,而不是 CPU “退还”时间。百分比必须指出分母:nr_throttled/nr_periods 是统计周期比例,不等于这批 HTTP 请求中慢请求的比例。
memory.events 可显示 high、max、oom、oom_kill 等累计事件,不是进程实时常驻内存;某些字段在父 cgroup 包含子 cgroup,memory.events.local 则是另一口径。io.stat 是设备相关的累计读写统计,某工作负载没有实际块设备访问时输出为空或不变化,不能据此宣布磁盘从未参与业务路径。压力 PSI 反映窗口内资源等待,与 CPU 使用率或应用延迟也不是同一指标。先记录缺失字段和采集失败的原因,再决定有没有足够证据把内存回收、磁盘等待排入候选根因。
同样叫“CPU 使用率”,来源也可能不同。在固定阅读的 Kubernetes v1.33.4 pkg/kubelet/stats/cri_stats_provider.go 里,ListPodStats 在不同配置/回退条件下从 CRI 或 cAdvisor 取得部分字段;某一 CPU nano core 使用率还可能由周期性调用者对累计值差分估算。仪表盘上的一个瞬时值因此并非内核 cpu.stat 的直接别名。要比较两者,先记录采集来源、统计范围、单位、上报与查询的采样间隔,再把两种采样时间轴放到同一窗口。当前 VM 没有 kubelet/CRI,读了固定源码只说明存在这类实现路径,不说明这里实际安装了同版本 kubelet。
当采样跨度很短,两个 CPU 读数之间只夹了几条请求,不能把 usage_usec 增量全部分摊给这几条 HTTP:探针服务本身、采样脚本、同容器的 bounded_load 和可能的运行时辅助进程都消耗资源。增量可用于判断这个 cgroup 在窗口内总体是否被执行和节流;某一个请求的 CPU 时间若要精确归属,需要进一步的进程级事件、线程或链路追踪。相反,采样跨度过大又会把负载运行之前和之后的空闲时段平均进去,隐藏短暂节流。附件因此保存原始采样点与每次 HTTP 的单调时钟耗时,而不是只导出一个“CPU 平均值”。
正常对照让两只 Pod 用同一镜像、相同探针脚本和相同请求频次,仅 CPU limit 不同;限定低额度的一只 Pod 运行包内 bounded_load.py 两秒,观察其请求分布与前后计数。对照 Pod 也执行同一份脚本及有界负载,避免把“只对故障组做了 exec”混进变量;两组都保存响应码和失败数,计数增量不能代替 HTTP 结果。即使低限额组 nr_throttled 增加、请求也变慢,这仍是同一时间窗的关联证据,需要核对是否同节点、是否出现内存或网络异常,并多轮重复对照才能支持更强因果结论。
如果两只 Pod 被调度到不同节点,节点本底负载和 CPU 拓扑就多了一个变量;若要比对限额机制,至少记录节点型号、Pod 调度落点、其它可竞争任务和是否存在父级限额。单次低限额组请求较快也不反驳机制:探针 /ready 的计算量很小,服务可能在未达到配额前就完成;提高负载不能靠无界压测填满共享节点,只能用包内两秒的有界循环,必要时在专用机器中多次复跑。记录每轮的完整耗时样本、成功和失败数,比只抄一个最大值或第一个返回码更有解释力。尾延迟分位数必须标明样本量;样本很少时报告每次原始值比强算 p99 更诚实。
基线还需要“无负载”窗口:在同一容器先运行不启动 bounded_load 的采样,看测量脚本自身造成的计数增量和请求分布,再重复有负载窗口。如果第一个窗口已经有大量节流,应用/父 cgroup/节点上可能有别的竞争,不能把第二个窗口的全部节流归因给自己的脚本。两轮之间应确认 Pod UID 与容器 ID 未变,若已变化则新建实验轮次而非把累加计数相减。合并结果时分别保留每轮的开始结束时刻、持续秒数和原始 cpu.stat,不能只保留差分后的表格而丢失数据来源。
同一 URL 的失败也可能与 CPU 无关
若一只 Pod 重新创建,新容器可能在同名路径或同一 Pod 名称下运行,但其 containerID 与统计实例已更换。沿用旧 PID 采 cpu.stat 可能读到已退出进程的路径,或把 PID 复用后的别人的任务当作该请求;应以新 Pod UID、容器 ID 与 cgroup 路径重新定位。若低限额 Pod 的 nr_throttled 没变而请求超时,也不要反向断言 CPU 一定无关:可能是父级限制、应用锁等待、节点压力、网络拥塞,或者采样窗口错位。需要查看正确层级与 /identity、调用链上的错误,明确数据不支持哪个假设。
另一种误判来自日志:容器 stdout 的缺行不证明某个请求没有执行。进程可能尚未写入、stdout 被缓冲,日志采集器可能丢了行,或 kubectl logs 在重启后默认只显示当前实例。先用 Pod UID/容器 ID、前次容器的 --previous 和应用的请求时间定位日志范围,再分别保存请求侧异常与服务侧证据。由于本篇 probe-app 没有独立的请求 ID 与内部耗时字段,不能仅靠一段文本日志宣称能够把某条客户端延迟精确拆分为服务端执行时间与网络传输时间;那需要另一个明确定义字段/请求 ID 的探针实现与双端时间同步。
把从 API 导出的 JSON、kubectl logs --timestamps 和容器内的时钟一并保存,也仍需警惕日志轮转、节点时钟偏移和 API 可见性的延迟。对于某次客户端超时,如果 probe-app 根本没有收到请求,应用日志当然不会出现对应行;要区分这种情况与日志缺失,需用带请求 ID 的客户端和服务端埋点,或者链路两端同步抓包。本篇不另造一个与共用包不兼容的服务实现,因此明确把“精确地解释哪一次 HTTP 变慢”保留为待补的业务埋点能力,现有指标只提供受限窗口内的相关性检查。
静态附件提供的 c31-pods.yaml 只描述两种资源限额,c31_sample.py 从同容器采样并发送有界请求;二者都依赖运行时把真实镜像 digest 替换进清单。单次采样的目标是证明观察口径和原始结果保存方法,而不是凭合成负载输出一个所有容器的开销系数。对照 Pod 清单、有界同容器采样脚本、专用附件校验与 执行与清理入口可分别下载,并复用 共用探针包。
迁移到另一个运行时或操作系统时,先确认是否有 Linux cgroup v2、对应控制器、CRI 给出的真实身份与相同采样位置;在 Windows 或 cgroup v1 上不得照搬 cpu.stat 字段。对任何“容器慢”诊断,都先找到这次请求实际处理的进程和统计实例,再核对采样窗口与反例,最后再谈节流、内存或 I/O。静态构建页能证明说明和脚本可取得,却不能替代这些字段在 VM 上实际变化的原始记录。
当观测工具从容器内直接读 cgroup 改为集群指标服务时,还要追问数据在何处聚合、标签中的 Pod UID 是否随重建刷新、采样掉点时是否补零以及查询窗口是否覆盖请求发生的那一秒。缺了这些元信息,图表再平滑也可能把两个先后存在的容器画成一条连续曲线。先恢复身份和时间,再解释曲线形状,是这套方法跨监控产品仍适用的边界。
正例以两种限额分别执行同样有界负载,通过 UID→容器 ID→PID→cgroup 关联请求与计数;反例在进程退出重建后只沿旧 PID 读取计数,可能把不相关的新进程误归因。当前环境没有 kind/CRI 和可写 cgroup v2,节点关联实验全部 NOT_RUN;00/09 的普通进程日志不可拼接成一次容器请求。
| 现象 | 关联键 | 适用条件 |
|---|---|---|
| 请求延迟上升 | request 时间窗、Pod UID、cgroup | 计数增量对齐采样 |
| 旧 PID 仍可查 | 进程启动时间/容器 ID | PID 会被重用 |
| 日志缺尾 | stdio/collector 时间窗 | 不能仅凭缺行判定没执行 |
练习
- 先预测容器重启后原 PID 的指标能否继续作该应用的性能证据,在 VM 关联两代容器 ID 与 cgroup 路径再判断。
- 先预测
memory.events无变化、CPU 节流增多时/ready延迟会怎样,采集真实请求分布与各控制器增量,不把一次高延迟当全局结论。
系列总目录:从进程隔离到运行时与编排。





