第10章 云原生与容器
cgroups、Docker、Kubernetes
章节导读
容器和 Kubernetes 已经成为默认的部署形态,但它们给性能分析带来了一个根本性的新挑战:命名空间隔离让传统工具"失明"了。容器内的 top 看到的 CPU 是宿主机的(/proc 没有被命名空间隔离!),容器内的 free 看到的内存也是宿主机的,容器内的 ps 只能看到自己的进程却看不到资源限制的真实边界。更微妙的是,容器的性能问题往往源于"看不见的层":cgroup 的 CPU 配额限流(throttle)在容器内几乎无迹可寻(只有 /sys/fs/cgroup 下的统计文件),内存 limit 触发的 OOM kill 在容器日志里只是一句 "exit code 137",overlay 文件系统的写放大在应用的 I/O 延迟里却找不到文件系统的名字。BPF 是穿透这些层次的技术:它运行在宿主机内核,天然看到所有容器的所有事件,且能按 cgroup、命名空间、容器 ID 精确过滤和归因。
先建立容器的技术本质认知:容器 = 命名空间(隔离视图:PID、网络、挂载、UTS、IPC、user、cgroup、time 八种)+ cgroups(限制资源:CPU、内存、I/O、PID 数等)+ 联合文件系统(overlayfs 的镜像分层)。没有虚拟机、没有独立内核——所有容器共享宿主机内核。这个本质决定了 BPF 分析的三个基本事实:(1) 宿主机上的 BPF 工具看到一切——容器内的进程对宿主机内核而言就是普通进程,execsnoop 在宿主机运行能看到所有容器的进程执行(COMM 和 PID 是宿主机命名空间的视角);(2) 容器内运行 BPF 需要特权——BPF 系统调用需要 CAP_BPF/CAP_SYS_ADMIN,普通容器没有,且容器内看到的 PID 与宿主机不同(PID 命名空间映射),工具输出需要翻译;(3) 归因需要额外信息——把宿主机视角的事件(PID 12345)映射回容器(哪个 Pod、哪个 namespace)需要读取 /proc/PID/cgroup 或 BPF 直接获取 cgroup id(bpf_get_current_cgroup_id 辅助函数,4.18+)。
cgroups 是容器性能问题的最大来源,值得单独深入。CPU 控制器:cfs_quota_us/cfs_period_us(v1)或 cpu.max(v2)设定 CPU 时间配额——配额用尽时整个 cgroup 的所有线程被冻结(throttle)直到下个周期(默认 100ms),表现为应用周期性的 100ms 级停顿!这个"配额限流毛刺"是容器化 Java/Go 服务 P99 延迟问题的头号元凶:应用平均 CPU 用量只有配额的 50%,但突发并发时瞬间用满配额,剩余时间被冻结。诊断:cat /sys/fs/cgroup/cpu/.../cpu.stat 的 nr_throttled 和 throttled_time(v1)或 throttled_usec(v2);BPF 方案追踪 throttle 事件(sched_throttle 相关 tracepoint 或 sched_switch 中的 throttle 标记),runqlat 会显示被限流线程的调度延迟呈 100ms 周期分布——这个指纹极具辨识度。内存控制器:limit 触发时先回收(缓存丢弃、swap)再 OOM kill;memory.high(v2)提供软限流(超过后进程被减速而非杀死)。I/O 控制器:io.max 限制带宽/IOPS,io.latency 提供延迟保护。PID 控制器:防 fork 炸弹。每个控制器都有对应的 BPF 观测方案,本章逐一展开。
Kubernetes 增加编排层:Pod(共享网络和 IPC 的容器组)、节点资源管理(requests/limits 映射到 cgroup 配置)、QoS 等级(Guaranteed/Burstable/BestEffort 决定 OOM 优先级和驱逐顺序)、kubelet 的驱逐机制(节点内存压力时按 QoS 驱逐 Pod)。K8s 环境的性能排查需要"三层对照":应用层(容器内指标)、编排层(kubectl top、events、Pod 状态)、节点层(宿主机 BPF 工具)。常见的跨层问题:Pod 显示 Running 但应用慢(节点层 CPU 争抢或 throttle);Pod 被 OOMKilled 但应用内存监控正常(JVM 堆外内存、页缓存计入 cgroup 账目);服务延迟抖动但 Pod 指标平稳(同节点邻居容器的干扰——noisy neighbor,BPF 的 per-cgroup 分解是定位手段)。
容器网络(CNI)和存储(CSI/overlayfs)的性能分析在第 9 章网络部分已有铺垫,本章补充容器特有的维度:veth pair 的开销、CNI 插件的转发路径(bridge 模式的 iptables 链 vs Cilium 的 BPF 转发)、Service 负载均衡的 conntrack 成本、overlayfs 的写时复制(容器内写镜像层文件触发 copy-up,首次写延迟毛刺的来源)、emptyDir 与容器可写层的存储位置差异。
多租户环境的 BPF 追踪还涉及安全与隐私边界:宿主机特权容器能追踪所有租户的进程(读取函数参数中的敏感数据),BPF 权限必须与租户隔离策略统一设计(K8s 的 PodSecurityPolicy/Standards 禁止特权容器即是此意)。云原生可观测性的最佳实践正在收敛到"BPF 原生"架构:节点级 agent(以 DaemonSet 部署特权 BPF 采集器)+ cgroup/namespace 归因 + 时序数据库——Pixie、DeepFlow、Coroot、Grafana Beyla 等产品都是这个模式。本章的知识是使用和评估这些产品的基础:它们没有魔法,只是把本章的 BPF 技术产品化了。
核心概念
- cgroups
- 容器追踪
- Kubernetes 可观测性
- CNI
- overlayfs
- 资源限流
- 多租户
要点精析
【cgroups v1 vs v2:容器资源控制的两个世界】cgroups v1 是分层控制器模型:每个控制器(cpu、memory、blkio...)独立的层级树,容器运行时在每个控制器下创建对应目录;v2 是统一层级:所有控制器共享一棵树(现代发行版和 K8s 1.25+ 默认 v2)。关键差异影响 BPF 分析:(1) 路径——v1 的 /sys/fs/cgroup/cpu/kubepods/pod-xxx/、v2 的 /sys/fs/cgroup/kubepods.slice/...,工具需要适配两种布局;(2) 内存账目——v1 的 memory.usage_in_bytes 包含页缓存(容器 limit 被缓存"吃掉"的困惑来源),v2 的 memory.current 类似但 memory.high 机制提供软限流缓冲;(3) I/O 控制——v1 的 blkio 只对 direct I/O 有效(buffered 写无法归因到 cgroup,历史缺陷!),v2 的 io 控制器通过回写归因修复了这个问题;(4) CPU 配额——v1 的 cfs_quota_us/cfs_period_us 与 v2 的 cpu.max("配额 周期"格式如 "200000 100000" = 2 核)语义相同;(5) BPF 集成——v2 的 cgroup id 是 bpf_get_current_cgroup_id 的返回值(v1 没有统一 id),cgroup 级 BPF 程序(BPF_PROG_TYPE_CGROUP_*)只支持 v2。检查你的环境:stat -fc %T /sys/fs/cgroup/(cgroup2fs = v2,tmpfs = v1 或混合)。分析工具适配:BCC 工具的 --cgroup 选项、runqlat 的 cgroup 过滤在 v2 上更完整。
【CPU 配额限流(throttle):容器 P99 毛刺的头号元凶】CFS 带宽控制机制:每 period(默认 100ms)给 cgroup 发放 quota 的 CPU 时间,用尽即冻结全部线程到下个周期。病态模式的指纹:应用 P99 延迟出现 ~100ms 的量化毛刺(毛刺值集中在 100ms、200ms——周期的整数倍)、平均 CPU 利用率远低于 limit(如 limit 2 核、平均用 0.8 核,但突发时用满)、cpu.stat 的 nr_throttled 持续增长。为什么平均值骗人:quota 按周期结算,一个 8 线程应用突发并行 50ms 用掉 2 核×100ms 的配额(4 线程秒),剩余 50ms 全部冻结——周期内平均利用率 50% 看似健康,但每个突发尾部都有 50ms 停顿。诊断命令:cat /sys/fs/cgroup/.../cpu.stat(v2:nr_throttled、throttled_usec;v1:nr_periods、nr_throttled、throttled_time 纳秒);BPF 方案:runqlat 按 cgroup 分解看调度延迟的 100ms 周期模式、追踪 sched_switch 中任务被 throttle 的事件。解决方案菜单:(1) 提高 limit(配额宽松化);(2) 减小 period(cpu.cfs_period_us 调到 20ms,冻结粒度变细——K8s 的 CFSQuotaPeriod feature gate);(3) 应用并发度与配额对齐(线程池大小 ≤ 配额核数,避免突发超卖);(4) v2 的 cpu.weight 替代硬 limit(比例分配不限流,Google/Borg 的实践:只设 request 不设 CPU limit);(5) 关闭 CPU limit 只保留 request(K8s 社区的主流建议——CPU 是可压缩资源,限流的伤害大于超卖)。这个知识点是容器性能排查的必修课:遇到 100ms 量化毛刺,先查 throttle。
【容器内存账目:OOMKilled 的完整排查】容器 OOM 的账目构成(v2 memory.current):匿名内存(应用堆栈)+ 页缓存(容器内文件 I/O 的缓存,含镜像层读取!)+ 内核内存(socket 缓冲、dentry 部分)+ swap(如允许)。排查流程:(1) 确认 OOM 类型——kubectl describe pod 的 Last State: OOMKilled(cgroup OOM)vs 节点 dmesg 的全局 OOM;宿主机 oomkill 工具持续记录两者;(2) 分账——OOM 前的 memory.stat(监控应采集):anon 主导 = 应用内存问题(泄漏、堆配置过大、堆外失控);file 主导 = 缓存挤压(limit 未留缓存余量,大量文件 I/O 的容器常见);sock 主导 = 网络连接过多(每连接的 socket 缓冲计入 cgroup!高并发服务的隐形内存消耗);(3) 应用层对照——JVM:-Xmx 只是堆,Metaspace/CodeCache/线程栈/DirectByteBuffer/JNI 都在堆外(容器 limit 应 ≥ Xmx × 1.5 的经验法则,实际按 NMT 输出);Go:GOMEMLIMIT(1.19+)与 cgroup limit 对齐,GC 的堆增长策略;Node.js:--max-old-space-size;(4) 预防——memory.high(v2 软限流:超过后回收加压+进程减速,给监控告警留时间窗)、PSI 内存压力监控(memory.pressure 文件)。经典案例:Java 容器 limit 4GB、-Xmx 3GB,看似留了 1GB 余量,但 Metaspace 300MB + 200 线程×1MB 栈 + DirectBuffer 500MB + 页缓存 = 超限 OOMKilled,而 JVM 自己的监控(堆使用)完全正常——堆外账目必须用 NMT(-XX:NativeMemoryTracking)+ BPF(memleak 追踪 JNI 分配)覆盖。
【容器视角的失真:/proc 陷阱与修正】容器内 /proc 未被命名空间隔离(lxcfs 之前),导致一系列经典误判:(1) top/free 显示宿主机资源——容器内 top 看到 64 核(宿主机),而 limit 只有 2 核,基于"CPU 利用率 3%"的判断完全失真;(2) JVM/Go 运行时的自动配置错误——JVM 8u191 前按宿主机核数算 GC 线程和默认堆(-XX:+UseContainerSupport 修复)、Go 的 GOMAXPROCS 按宿主机核数设置(远超 cgroup 配额导致 throttle 加剧,automaxprocs 库修复);(3) 监控 agent 采集失真——node_exporter 在容器内跑采到的是宿主机数据。修正方案:lxcfs(FUSE 文件系统伪装容器视角的 /proc)、运行时容器感知(UseContainerSupport、automaxprocs)、以及"从 cgroup 文件读真实限制"(/sys/fs/cgroup/cpu.max、memory.max 才是容器的真实资源边界)。BPF 的角色:宿主机视角的 BPF 工具天然免疫这个陷阱(它看到的就是真实内核状态),且能按 cgroup 归因——这是"节点级 BPF 采集 + 容器级归因"架构优于"容器内 agent"架构的根本原因之一。排查口诀:容器性能问题,第一步永远是 cat /sys/fs/cgroup/ 下的 limit 和 stat 文件,而不是容器内的 top/free。
【Kubernetes 资源模型与 QoS:性能视角解读】K8s 的 requests/limits 映射:requests → cgroup 权重(cpu.weight/shares,比例分配)+ 调度依据;limits → cgroup 硬限(cpu.max 限流、memory.max OOM 边界)。QoS 三级:Guaranteed(requests=limits,全部资源锁定)、Burstable(部分设定)、BestEffort(无设定)——决定节点压力时的驱逐顺序(BestEffort 先死)和 OOM 分数。性能视角的解读要点:(1) CPU request 过低 → 权重低 → 节点争抢时分到的 CPU 少 → runqlat 调度延迟长尾(BPF 按 cgroup 分解可见);(2) CPU limit 设定 → throttle 风险(上一要点);(3) memory request 过低 → 节点内存超卖 → 压力时缓存被回收(cachestat 命中率下降)甚至驱逐;(4) 节点级干扰——同节点的 BestEffort Pod 突发占用 CPU/内存/带宽,影响 Guaranteed Pod(noisy neighbor),BPF 的 per-cgroup 资源分解(CPU 时间、I/O 字节、网络字节按 cgroup 聚合)是量化干扰的手段;(5) 驱逐与重启的性能成本——Pod 驱逐后的重新调度、镜像拉取、冷启动(JVM 预热、缓存重建)是分钟级的性能事件,监控 PSI 和节点压力指标做预防。工具链:kubectl top(metrics-server,分钟级粒度太粗)、cAdvisor(kubelet 内置,容器级 cgroup 统计的时序化)、以及 BPF 方案(细粒度事件级)。三层对照排查法:Pod 层(kubectl describe/events)→ 容器层(cgroup stat 文件)→ 节点层(宿主机 BPF 工具),每层数据互相验证。
【容器网络性能:CNI 路径与观测】容器网络路径(Pod A → Pod B 同节点,bridge CNI):A 应用 → A 的 veth → 网桥 cni0 → 宿主机协议栈路由 → B 的 veth → B 应用;跨节点再加:宿主机路由 → iptables/ipvs(Service DNAT)→ 封装(VXLAN,如 flannel)→ 物理网卡 → 对端节点解封 → 对端路径。每段的性能成本:veth 转发约 1-2μs/包(比物理网卡路径多一次协议栈遍历)、iptables DNAT 规则遍历 O(n)(大集群 Service 数万规则时显著——kube-proxy 的 ipvs 模式或 Cilium BPF 模式解决)、VXLAN 封装开销(MTU 减小 50 字节、封装解封 CPU)、conntrack 表(每连接一条记录,表满丢包——nf_conntrack: table full 是容器网络事故高发项)。BPF 观测方案:(1) tcplife/tcpretrans 在宿主机运行,按 Pod IP(veth 的地址)过滤归因;(2) 追踪 veth 转发路径(kprobe 挂 dev_queue_xmit 按设备聚合);(3) conntrack 事件追踪(nf_conntrack 的 tracepoint);(4) Cilium/Hubble 环境直接用 BPF 原生流观测(每条流的允许/拒绝/转发决策)。性能基线测量:iperf3 跨 Pod 打流对比跨节点/同节点/宿主机直连的吞吐和延迟差异,量化 CNI 开销(典型:同节点 Pod 间 RTT 比宿主机 loopback 高 10-30μs,跨节点 VXLAN 比裸网络高 50-100μs)。排查口诀:容器网络问题先在宿主机抓包(tcpdump -i vethXXX 或 BPF),确认包到了哪一层,再决定往 CNI/iptables/物理网络哪个方向深入。
【容器存储:overlayfs 与写放大】Docker/containerd 默认存储驱动 overlay2:镜像层(lowerdir,只读)+ 容器层(upperdir,可写)联合挂载。关键机制 copy-up:容器首次修改镜像层文件时,整个文件从 lowerdir 复制到 upperdir 再修改——大文件的首次写会有显著延迟毛刺(复制 100MB 的 jar 包!),且占用双份磁盘。性能影响点:(1) 容器内写镜像层文件(日志写到应用目录、临时文件、SQLite)触发 copy-up + 后续写在 upperdir(性能接近底层文件系统但有额外间接层);(2) 大量小文件的目录操作(readdir 需要合并多层视图,opendir 成本随层数增长);(3) whiteout 文件(删除镜像层文件产生标记文件,删除大目录产生海量 whiteout)。BPF 观测:追踪 overlayfs 的 kprobe(ovl_copy_up、ovl_workdir_create)测量 copy-up 频率和大小;fileslower 在容器内视角看慢文件操作;biolatency 区分 upperdir 设备的 I/O。最佳实践:应用数据写到 volume(emptyDir/PV,绕过 overlay)、镜像设计避免运行时修改镜像层(日志输出到 stdout 由运行时收集)、大文件预复制到 upperdir(镜像构建时处理)。emptyDir 的存储位置注意:默认在宿主机磁盘(/var/lib/kubelet/pods/...),memory 类型 emptyDir 用 tmpfs(计入容器内存账目!大 tmpfs 使用会触发 OOM)。CSI 卷的性能分析回到第 8 章的存储栈(网络存储再加网络层延迟分解)。
【per-cgroup 资源归因:BPF 的多租户视角】宿主机 BPF 工具按 cgroup 归因资源消耗,是多租户分析的核心能力。实现基础:bpf_get_current_cgroup_id()(v2 层级中的 cgroup inode id)作为 map 键聚合。bpftrace 示例:按 cgroup 统计 CPU 时间(sched_switch 的 next 任务 cgroup id)、I/O 字节(block tracepoint + bio 关联的 cgroup)、网络字节(socket 层 + cgroup)。BCC 工具的 cgroup 支持:runqlat --cgroup、以及 cgroup 级工具(如 memleak 的 cgroup 过滤在演进中)。产品化方案:Cilium/Hubble 的 per-pod 流量统计、Pixie 的 per-service 指标、以及各云厂商的容器监控 agent(底层都是 BPF per-cgroup 聚合)。应用场景:(1) noisy neighbor 定量——某 Pod 延迟毛刺时刻,按 cgroup 分解节点 CPU/IO/网络消耗,找到突发占用的邻居;(2) 成本归因——per-cgroup 资源账本支撑多租户计费(比 cAdvisor 的采样更精确);(3) QoS 验证——限流配置是否生效(被限 cgroup 的实际消耗应被压制在 limit 内,throttle 计数增长);(4) 容量规划——节点真实资源分解(各 Pod 实际用量 vs requests 声明,超卖率计算)。技术细节:cgroup id 到 Pod 名的映射需要用户态维护(读取 /sys/fs/cgroup 层级 + K8s API 的 pod UID 对应关系),BPF 只负责按 id 聚合——这个"内核聚合 + 用户态富化"的分工是所有容器观测产品的通用架构。
【容器内运行 BPF:权限、风险与替代架构】容器内直接运行 BPF 工具的条件:特权容器(privileged: true)或精细权限(CAP_BPF + CAP_PERFMON + CAP_SYS_RESOURCE,5.8+ 内核)、挂载 /sys/kernel/debug 和 /sys/kernel/btf、宿主机内核头(BCC 需要)或 CO-RE 预编译二进制(libbpf 方案免依赖)。风险清单:特权容器逃逸面大(能访问宿主机设备、能加载任意 BPF——BPF 本身历史上是提权漏洞高发区,unprivileged BPF 被多数发行版禁用正是此因)、BPF 程序 bug 影响宿主机内核稳定性(验证器保证安全但极端场景如 map 内存耗尽仍可能影响全机)、多租户环境的隐私越界(能追踪其他容器)。推荐的替代架构——节点级采集:以 DaemonSet 部署单一特权 BPF agent(如 Pixie、DeepFlow、自研采集器),每节点一个,采集全部容器数据并按 cgroup 归因,业务 Pod 保持非特权。这个架构的优势:权限集中管控(只有基础设施命名空间的 DaemonSet 特权)、开销统一预算(一个 agent 的 BPF 程序集而非每 Pod 一个)、数据全局视角(跨 Pod 的调用链关联)。kubectl debug 的临时特权容器(ephemeral container)是排查时的折中:按需注入特权调试容器,用完即删。安全基线:K8s Pod Security Standards 的 restricted 档禁止特权和 hostPID/hostNetwork,BPF 采集器应部署在独立的 infrastructure 命名空间并豁免。
【云原生可观测性架构:BPF 原生产品的技术解剖】新一代可观测性产品(Pixie、DeepFlow、Coroot、Grafana Beyla、Odigos)的共同架构:节点 BPF agent(DaemonSet)+ 内核态聚合 + 用户态富化 + 后端存储。技术组件解剖:(1) 数据采集层——uprobe 挂 SSL/HTTP 库实现协议解析(自动的、无侵入的 RED 指标:请求率/错误/延迟,按服务归因)、tracepoint 覆盖进程/网络/调度事件、profile 持续剖析;(2) 归因层——cgroup id → Pod → Service 的映射链(K8s API watch 维护)、PID 命名空间翻译;(3) 关联层——内核事件与应用上下文的拼接(如把 TCP 重传事件关联到具体的服务调用);(4) 输出——Prometheus 指标、火焰图、调用链(与 OTel 集成)。与传统 APM 的对比:无需改代码/加 SDK(Java agent、埋点)是最大优势,代价是语义深度(BPF 看到 HTTP 请求但不懂业务含义)、语言覆盖差异(SSL uprobe 对静态链接 TLS 的语言失效——Go 需要专门处理)、以及内核版本依赖。评估这类产品的检查清单:内核版本要求(CO-RE?最低 4.19/5.x?)、协议覆盖(HTTP/1.1、HTTP/2、gRPC、SQL、Kafka?)、数据主权(本地存储还是 SaaS)、开销预算(CPU <2%?内存 <500MB?)、与现有体系集成(OTel、Prometheus、服务网格)。理解本章内容后,你能看穿这些产品的技术本质——都是 BPF 工具链的产品化封装,评估时直接问"底层挂的什么探针、开销模型是什么"。
【eBPF 在 K8s 数据面的角色:从观测到替代】BPF 在云原生不只是观测工具,它正在替换核心数据面组件:(1) kube-proxy 替代——Cilium 的 BPF 负载均衡用哈希表 O(1) 查找替代 iptables 的 O(n) 规则遍历,万级 Service 集群的转发性能提升 10 倍+,且 conntrack 可控;(2) Service Mesh 数据面下沉——Istio 的 ambient mesh 用 BPF(ztunnel + waypoint)替代 sidecar 的 L7 代理,Pod 间流量的 mTLS 和 L4 策略在节点级 BPF 处理,每 Pod 的 sidecar 内存开销(50-100MB)归零;(3) 网络策略——Calico/Cilium 的 BPF 模式实现高性能 NetworkPolicy;(4) 安全——Tetragon(Cilium 项目)用 BPF 做运行时安全检测(进程执行、文件访问、网络流的策略执行与阻断——BPF 的 LSM 和 override 能力从"观测"走向"控制")。性能工程师的视角:这些替换改变了排查地图——iptables 规则不再生效(BPF map 替代,用 bpftool map dump 查看转发规则)、conntrack 语义变化、抓包位置变化(BPF 处理在协议栈之前,tcpdump 可能看不到被 BPF 丢弃/重定向的包)。排查工具随之升级:cilium monitor(BPF 事件流)、hubble observe(流级观测)、bpftool 直接查 map。趋势判断:云原生的网络/安全/观测数据面正在全面 BPF 化,本章知识从"高级技能"变成"基础设施素养"。
代码示例
/* 示例1:CPU throttle 诊断——容器 P99 毛刺排查 */
/* 找到容器的 cgroup 路径(v2): */
cat /proc/$(pgrep -f myapp | head -1)/cgroup
/* 0::/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8821.slice/cri-containerd-abc123.scope */
CG=/sys/fs/cgroup/kubepods.slice/.../cri-containerd-abc123.scope
cat $CG/cpu.max /* "200000 100000" = 2核配额 */
cat $CG/cpu.stat
/* nr_periods 45231
nr_throttled 8921 <- 20% 的周期被限流!
throttled_usec 45231000 <- 累计被冻结 45 秒 */
sudo /usr/share/bcc/tools/runqlat --cgroup $CG 10 1 /* 如工具支持 */cpu.stat 的三个数字讲清楚了 throttle 的严重程度:nr_throttled/nr_periods = 8921/45231 ≈ 20%——每 5 个 100ms 周期就有 1 个发生限流,应用每 500ms 就可能遭遇一次冻结停顿;throttled_usec 累计 45 秒的冻结时间。这与"P99 延迟 100ms 级毛刺"的告警完全吻合。修复决策树:(1) 如果应用是突发并行模式(线程池 > 配额核数),优先调整并发度或提高 limit;(2) 如果是稳态负载接近配额,直接提高 limit(CPU 是可压缩资源,limit 提高无 OOM 风险);(3) K8s 层面考虑去掉 CPU limit 只保留 request(社区主流实践——避免 throttle 且节点超卖由调度器管理);(4) 验证修复:调整后 nr_throttled 增速应归零,应用 P99 毛刺消失。注意 v1 路径差异(/sys/fs/cgroup/cpu,cpuacct/kubepods/...,cpu.cfs_quota_us 和 cpu.stat 的 throttled_time 单位纳秒)。这个排查流程是容器性能工程师的日常:告警 P99 毛刺 → 查 cpu.stat → 20% throttle → 调 limit → 毛刺消失,全程十分钟,不需要任何应用侧改动。/* 示例2:bpftrace 按 cgroup 分解 CPU 消耗——noisy neighbor 定位 */
sudo bpftrace -e '
tracepoint:sched:sched_switch {
@cpu_ns[cgroup] = sum(nsecs - @last[args->next_pid]);
@last[args->next_pid] = nsecs;
}
interval:s:10 { print(@cpu_ns); clear(@cpu_ns); clear(@last); }'
/* 输出(cgroup id: CPU 纳秒):
@cpu_ns[4294968451]: 8923410000 <- 某容器吃掉近 9 核秒/10秒
@cpu_ns[4294967823]: 120340000
*/
/* cgroup id 到路径的翻译: */
find /sys/fs/cgroup -inum 4294968451 2>/dev/nullcgroup 内置变量(bpftrace 提供,底层是 bpf_get_current_cgroup_id)让任意 BPF 脚本具备"按容器归因"能力。上面的脚本按 cgroup 聚合 CPU 时间——sched_switch 时 next_pid 上 CPU,记录时间戳,下次切换时结算(简化实现,精确版需处理 CPU 维度)。输出的 cgroup id 是 v2 层级的 inode 号,find -inum 反查路径即知是哪个 Pod(路径含 pod UID)。noisy neighbor 排查流程:受害 Pod 延迟毛刺 → 节点级按 cgroup 分解 CPU/IO/网络 → 找到突发消耗的邻居 → 处置(限流该邻居、迁移、或给受害者提 QoS)。同样的模式扩展到其他资源:I/O 按 cgroup(block tracepoint + bio 的 cgroup 关联,或 v2 的 io.stat 文件直接读)、网络按 cgroup(socket 层追踪)、内存按 cgroup(memory.stat 时序采集)。工程化建议:这类脚本适合做成节点级常态采集(每分钟聚合一次,按 cgroup id 打标签入库),形成"节点资源账本"——事故复盘时回答"当时谁在抢资源",容量规划时回答"各租户的真实用量分布"。注意 cgroup id 的稳定性:Pod 重建后 id 变化,映射表要动态维护(watch K8s API 或周期扫描 /sys/fs/cgroup)。/* 示例3:容器 OOM 事件监控与内存分账 */
sudo /usr/share/bcc/tools/oomkill
/* 输出:
10:23:45 Task in /kubepods.slice/.../cri-containerd-abc.scope killed as a result of limit of same
10:23:45 Memory cgroup out of memory: Kill process 4521 (java) score 1000 ...
*/
CG=/sys/fs/cgroup/kubepods.slice/.../cri-containerd-abc.scope
cat $CG/memory.max /* 4294967296 = 4GB limit */
cat $CG/memory.stat | head -20
/* anon 3221225472 <- 匿名内存 3GB(应用堆+堆外)
file 892341248 <- 页缓存 850MB
sock 120340000 <- socket 缓冲 115MB
kernel 45231000 */
cat $CG/memory.events /* low 0 high 0 max 12 oom 12 oom_kill 3 */oomkill 工具的输出明确区分了 cgroup OOM("killed as a result of limit"——容器自己的 limit 触发,不影响节点)与全局 OOM。memory.stat 的分账是排查的核心:anon 3GB 说明应用自身内存是主体(若 JVM,对照 -Xmx 和 NMT 确认堆内/堆外构成;anon 远超 Xmx 即堆外泄漏嫌疑,memleak 追踪 JNI/native 分配);file 850MB 是页缓存(容器内文件 I/O 产生,计入账目但可回收——OOM 时内核应先回收它,回收不及分配速度才 OOM);sock 115MB 是网络连接缓冲(高并发服务的隐形消耗,每连接几十 KB × 数千连接)。memory.events 的计数序列揭示历史:max 12 次(触到 limit 边缘)、oom_kill 3 次(三次被杀)。修复方向按分账结果:anon 主导 → 应用内存优化或提 limit;file 主导 → 检查是否有不必要的大量文件 I/O(日志刷盘模式)、或 limit 预留缓存空间;sock 主导 → 连接数治理(连接池上限、超时回收)。K8s 层面的配套:memory.high(v2)设置软限(如 limit 的 90%),超过后回收加压 + 进程减速,给告警和自动处置留时间窗,避免直接 OOMKill 的硬着陆。/* 示例4:overlayfs copy-up 追踪——容器写延迟毛刺 */
sudo bpftrace -e '
kprobe:ovl_copy_up_one { @copyup[comm] = count(); @start[tid] = nsecs; }
kretprobe:ovl_copy_up_one /@start[tid]/ {
@copyup_ms = hist((nsecs - @start[tid]) / 1000000);
delete(@start[tid]);
}
kprobe:ovl_write_iter { @writes[comm] = count(); }'
/* 输出:
@copyup_ms:
[16, 32) 3 <- 单次 copy-up 16-32ms!
[32, 64) 1
*/ovl_copy_up 是 overlayfs 的写时复制入口:容器首次修改镜像层文件时,整个文件从 lowerdir 复制到 upperdir。上面的直方图显示单次 copy-up 耗时 16-64ms——如果应用的关键路径上有这个操作(修改配置、写 SQLite、追加镜像内文件),就是毫秒级毛刺的直接来源。诊断特征:毛刺发生在容器启动后的"首次写"(copy-up 只做一次,之后写在 upperdir 正常速度);fileslower 在容器内能看到慢写但文件系统显示 overlay;biolatency 看到对应时刻的大块读(从 lowerdir 读原文件)+ 大块写(写入 upperdir)。排查流程:确认应用是否写镜像层路径(docker diff 容器ID 或 crictl 对比 upperdir 内容——出现在 upperdir 的镜像层文件都是 copy-up 的产物)→ 评估文件大小(copy-up 成本正比文件大小)→ 修复:应用数据目录改用 volume(emptyDir/PV 挂载覆盖镜像路径,写直接落 volume 无 copy-up)、镜像构建时预处理(需要修改的文件在 Dockerfile 里先复制到数据目录)、或接受一次性成本(启动毛刺无碍则忽略)。相关开销点:ovl_workdir_create(工作目录操作)、whiteout 创建(删除镜像层文件)、readdir 的多层合并(大目录 ls 慢)——都可以用同样的 kprobe 模式追踪。/* 示例5:容器网络路径追踪——veth 与转发分析 */
/* 找到 Pod 的 veth 设备: */
kubectl exec mypod -- cat /sys/class/net/eth0/iflink /* 输出 42 */
ip link | grep "^42:" /* 42: veth8f3a2c1 */
/* 宿主机上追踪该 veth 的收发: */
sudo bpftrace -e '
kprobe:__dev_queue_xmit {
$skb = (struct sk_buff *)arg0;
$dev = $skb->dev;
if ($dev->ifindex == 42) { @tx_bytes = sum($skb->len); @tx_pkts = count(); }
}
tracepoint:net:netif_receive_skb {
if (args->ifindex == 42) { @rx_bytes = sum(args->len); @rx_pkts = count(); }
}'
/* 对照 Pod 内视角:kubectl exec mypod -- cat /proc/net/dev */容器网络排查的核心技能是"宿主机-容器双视角对照"。步骤:(1) 通过 iflink 找到 Pod eth0 对应的宿主机 veth(iflink 值 = 宿主机侧设备的 ifindex);(2) 宿主机 BPF 追踪该 veth 的收发字节/包数;(3) 与 Pod 内 /proc/net/dev 的计数对照——两侧数字应一致(veth 是管道,一端进另一端出),不一致说明中间有丢弃(qdisc、tc BPF、netfilter)。完整路径追踪的扩展:Pod 发出 → veth → 网桥/路由(追踪 ip_rcv、ip_local_deliver)→ iptables/nftables(trace 规则命中较难,Cilium 环境用 bpf map)→ 出方向物理网卡(netdev tracepoint 按设备过滤)。跨节点再加封装层(VXLAN 的 udp 4789 端口流量)。工具链对照:tcpdump -i veth8f3a2c1 抓包看内容,BPF 计数看流量,tc -s qdisc show dev veth8f3a2c1 看队列丢弃,conntrack -L 看连接跟踪表。常见问题的定位模式:Pod 间不通 → 双视角对照找到包消失的层(veth 有发出但网桥没收到 = 网桥/ebpf 规则问题;宿主机发出但对端没收到 = 物理网络/封装问题);Pod 网络慢 → veth 队列丢弃(tc 统计)+ 软中断分布(第 6 章)+ conntrack 表压力。这套"分层对照"方法与第 8 章 I/O 栈、第 9 章网络栈的排查逻辑完全同构——栈式系统的通用排查法。/* 示例6:节点级容器资源账本——综合采集脚本 */
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read { @syscalls[cgroup] = count(); }
tracepoint:syscalls:sys_enter_write { @syscalls[cgroup] = count(); }
tracepoint:block:block_rq_complete { @io_lat[cgroup] = hist(args->error ? 1 : 0); }
kprobe:tcp_sendmsg { @net_bytes[cgroup] = sum(arg2); }
interval:s:60 {
print(@syscalls); print(@net_bytes);
clear(@syscalls); clear(@net_bytes); clear(@io_lat);
}'
/* 每分钟输出各 cgroup(容器)的系统调用数、网络发送字节——节点资源账本 */这个脚本是"节点级多租户资源账本"的雏形:按 cgroup 聚合系统调用频率(CPU 交互强度)、网络发送量(带宽消耗)、I/O 延迟(存储质量),每分钟输出一次。工程化改造方向:(1) 输出格式——改用 JSON(bpftrace 的 printf 拼 JSON 或用 BCC Python 前端)便于采集入库;(2) 归因富化——cgroup id 到 Pod/Service 名的映射由用户态组件维护(watch K8s API);(3) 开销控制——syscalls tracepoint 是高频事件(每秒数十万),全量计数在超高负载节点开销约 1-2%,可降为采样或只追踪特定系统调用;(4) 指标扩展——加入 CPU 时间(sched_switch)、内存(memory.stat 周期读取即可,无需 BPF)、throttle 事件。这个账本的三大用途:容量规划(各租户真实用量 vs requests 声明,超卖率与碎片分析)、成本分摊(按实际消耗计费而非声明量)、干扰排查(事故时刻的资源快照——"当时谁在抢")。这正是 Pixie/DeepFlow 等产品的核心数据模型:节点级 BPF 采集 + cgroup 归因 + 时序存储。自己实现一个简化版是理解这些产品的最好方式——从这个 20 行脚本开始,逐步加上归因、存储、可视化,你就重走了云原生可观测性产品的演进路。实验笔记
本实验构建容器环境的 BPF 分析能力。预计耗时 3-4 小时。环境:一台 Linux 主机(内核 5.4+,推荐 v2 cgroup)安装 Docker 或 containerd + ctr/nerdctl;如有条件用 minikube/kind 搭单节点 K8s(实验五需要)。root 权限。
【实验一:容器视角失真验证】(1) 运行 docker run -it --cpus=1 --memory=512m ubuntu bash,容器内执行 nproc、free -m、top——记录看到的 CPU 数和内存(宿主机值,失真!);(2) cat /sys/fs/cgroup/cpu.max 和 memory.max(容器内路径,v2)——这才是真实限制;(3) 容器内跑一个 Java 程序(docker run --cpus=1 openjdk:17 java -XX:+PrintFlagsFinal -version | grep -E "ParallelGCThreads|ConcGCThreads"),观察 JVM 是否按 1 核配置 GC 线程(UseContainerSupport 的效果);再用 --cpus=1 但宿主机 64 核的环境对比老版本 JVM 行为(如有);(4) 写一段 Go 代码打印 runtime.GOMAXPROCS(0),在 --cpus=2 容器内运行(64 核宿主机),验证默认值失真,再引入 automaxprocs 对比修复效果。
【实验二:CPU throttle 复现与诊断】(1) docker run --cpus=1 容器内运行 stress-ng --cpu 4 --timeout 60(4 线程压 1 核配额);(2) 宿主机找到该容器的 cgroup 路径(cat /proc/PID/cgroup),watch -n 1 cat $CG/cpu.stat 观察 nr_throttled 增长;(3) 容器内写一个"延迟敏感"程序(每 10ms 记录一次时间戳,检测间隔异常),观察 throttle 造成的 ~100ms 停顿在时间戳序列中的指纹;(4) 把 --cpus 提到 4 重复,验证 throttle 消失;(5) 用 runqlat(宿主机运行,观察全系统)对照 throttle 期间的调度延迟分布。写出"配额限流毛刺"的完整证据链:cpu.stat 计数 + 应用停顿时间戳 + 调度延迟分布。
【实验三:容器 OOM 完整排查】(1) docker run --memory=256m 容器内运行内存增长程序(stress-ng --vm 1 --vm-bytes 512M);(2) 宿主机运行 oomkill 工具捕获事件;(3) docker inspect 查看 OOMKilled 状态和 exit code 137;(4) 复现前采集 memory.stat 分账(anon/file/sock),确认 OOM 时的账目构成;(5) 跑一个 Java 程序(-Xmx200m + 大量线程 + DirectByteBuffer)在 --memory=256m 容器内触发堆外 OOM,验证"JVM 堆监控正常但容器 OOM"的场景,用 jcmd VM.native_memory(需 -XX:NativeMemoryTracking=summary)分账堆外。
【实验四:overlayfs 与容器 I/O】(1) 容器内写镜像层文件(echo test > /usr/share/bigfile,先在镜像里放一个 100MB 文件),用示例4的脚本追踪 copy-up 耗时;(2) docker diff 查看 upperdir 变化;(3) 对比写镜像层 vs 写 volume(docker run -v /tmp/vol:/data)的延迟差异(fileslower 或简单计时);(4) 容器内删除镜像层大文件,观察 whiteout 产生(docker diff 的 .wh. 前缀文件);(5) 大量小文件场景:容器内 ls 一个镜像层的万文件目录 vs 宿主机直接 ls,对比 readdir 开销。
【实验五:K8s 环境综合排查(如有集群)】(1) 部署一个"问题应用":CPU limit 过低(100m)+ 突发负载的 Deployment;(2) kubectl top pod 观察(分钟级粒度可能看不到突发);(3) 节点上按实验二流程查 cpu.stat;(4) 部署第二个 Pod 制造 noisy neighbor(stress-ng),用示例2的 per-cgroup 脚本量化两个 Pod 的资源竞争;(5) 用 kubectl debug 注入特权 ephemeral 容器运行 BPF 工具排查(体验"临时特权调试"模式)。无集群则用两个 docker 容器 + 宿主机脚本模拟同样场景。
【实验报告要求】五个实验的数据(失真对照表、throttle 证据链、OOM 分账、copy-up 耗时、per-cgroup 账本输出)。核心交付物:实验二的"100ms 停顿指纹"时间戳序列图,和实验三的分账表。附加思考:(1) 为什么 K8s 社区建议"设 CPU request 不设 CPU limit"?从 throttle 机制和可压缩资源特性论证;(2) 容器内 BPF 与节点级 BPF agent 两种架构的权限模型和开销模型对比。
延伸阅读
- Brendan Gregg, "BPF Performance Tools" 第 10 章(云与容器相关章节)——原书对容器追踪的论述,含 cgroup 感知工具和命名空间翻译的基础方法。
- Linux 控制组文档 Documentation/admin-guide/cgroup-v2.rst——cgroups v2 的权威说明:每个控制器(cpu/memory/io/pids)的接口文件、语义、throttle 机制的精确描述,容器资源排查的字典。
- Kubernetes 官方文档 "Resource Management for Pods and Containers" 与 "Node Pressure Eviction"——requests/limits 到 cgroup 的映射规则、QoS 等级、驱逐机制的官方定义,K8s 性能问题的第一参考。
- Cilium 文档 (docs.cilium.io) 与 Hubble 项目——BPF 替换 kube-proxy/iptables 的完整技术文档,eBPF 数据面模式的架构说明,容器网络性能与观测的工业级实践。
- Brendan Gregg 博客 "Docker Performance Tools" 与 Netflix 容器性能系列 (brendangregg.com/blog)——容器性能分析的早期奠基文章,/proc 失真、cgroup 感知工具的原始讨论。
- Pixie (docs.px.dev) 与 DeepFlow (deepflow.io) 技术文档——两个开源 BPF 可观测性产品的架构文档:协议追踪的实现、cgroup 归因链、开销模型,评估和自建容器观测系统的参考。
- LWN 文章 "The unified cgroup hierarchy" 系列与 cgroup v1 的问题分析——v1 到 v2 演进的设计动机(buffered I/O 归因缺陷、多层级混乱),理解两代差异的深度材料。
- 《Kubernetes 故障排查》类实战书籍与 learnk8s 的排障指南 (learnk8s.io/troubleshooting-deployments)——K8s 常见问题(OOMKilled、CrashLoopBackOff、Pending)的系统化排查流程,与本章 BPF 手段互补。