土法炼钢 · 系统与基础设施

【Tetragon / eBPF】节流与观测税:cgroup-rate、Post.rateLimit 与环缓冲压力

文章导航

分类入口
kubernetessecurity
标签入口
#tetragon#ebpf#cgroup-rate#ratelimit#ringbuf#throttling#v1.7.0

目录

第 10 篇 讨论动作是否发生。本篇讨论事件是否到达 agent:选择器已经匹配,仍然可能看不到 process_exec 常见误区是立刻改 TracingPolicy。v1.7.0 把节流写在 tag 路径 docs/content/en/docs/concepts/cgroup-rate.md(live 站点 /concepts/event-throttling/ 不是本版本锚点)。另外还有选择器级 Post.rateLimit 和 ring buffer 压满。三层都可以让「策略没匹配」的假象成立。

本篇在系列中的位置

篇目 核心内容
第 10 篇 · 强制执行 Override / SIGKILL
第 11 篇 · 节流与观测税 cgroup-rate、rateLimit、环缓冲
第 12 篇 · 运维与导出 metrics 与 SIEM 接线
系列目录 全部篇目

版本锚定:Tetragon v1.7.0。cgroup-rate 以 tag cgroup-rate.md + pkg/cgrouprate/ + bpf/process/bpf_rate.h 为准。Post.rateLimit 原文来自同 tag selectors.md。无压测则不写 CPU 占用百分比,不伪造每秒事件数。

本篇落到五轴里的 过滤分层 之后、导出之前:内核已经决定 Post,用户态仍可能收不到。


一、cgroup-rate:只节流 base sensor 的 EXEC / EXIT,而且是 per-CPU

1. 文档钉死的范围

cgroup-rate.md 的概念:按 cgroup 监视事件速率,超过阈值就停止向用户态投递该 cgroup 的事件;速率在限值之下稳定后恢复。文档两条 note 必须一起读:

开关是 --cgroup-rate,默认关闭。cgrouprate.goopts.Events == 0 || opts.Interval == 0 时打 Cgroup rate disabled 并返回。语法 <events,interval>,例如 1000,1s 表示每秒 1000 个事件。文档例子包括:

文档示例 含义
--cgroup-rate=10,1s 每秒 10 个事件
--cgroup-rate=1000,1s 每秒 1000 个事件
--cgroup-rate=100,1m 每分钟 100 个事件
--cgroup-rate=10000,10m 每 10 分钟 10000 个事件

配置成功时日志有 Cgroup rate started (...) 一行(文档叙述;本机未跑则不粘贴时间戳)。cgroup_rate_options_map 是单槽 ARRAY,用户态 Config()EventsInterval 写进去;BPF cgroup_rate() 读到 interval == 0 则直接放行。

源码侧需要一句对齐,避免把文档读窄或读宽:pkg/cgrouprate/cgrouprate.go 注释写监视 exec/fork/exit;RegisterCgroupRatecgroup_rate_map 挂到 IsExecve / IsFork / IsExit 三类 base 程序,并额外挂 raw_tracepoint/cgroup_rmdir 以便 cgroup 删除时清 map。官方文档口径仍是导出事件类型 PROCESS_EXEC / PROCESS_EXIT。本篇把 fork 导出事件写成 cgroup-rate 的节流对象。NewCgroupRate 在 map 尚未向 base sensor 注册时直接报错 cgrouprate has not been registered to base sensor——开关打开但 base 程序没带上 rate map,属于加载失败,不是「阈值太高所以没 START」。

2. START / STOP 语义

文档:

用户态常量与文档一致:aliveCnt = 5process() 里每秒 tick 一次 processCgroups()processCgroup() 在所有 CPU 要么已经静默超过 5 秒、要么 rate 低于阈值且已暂停满 5 秒时,清掉 Throttled 并发送 THROTTLE_STOPCheck() 从事件处理路径把该 cgroup 推进队列;updateCgroups() 第一次见到该 id 时发出 THROTTLE_START。源码注释承认与 BPF cgroup_rate 存在竞态:用户态清 Throttled 的同时内核可能再次超限,则下一次事件会重新节流。

map 上限 cgRateMaxEntries = 32768(注释写可再调)。cleanupCgroupscleanupInterval(1 分钟)扫一次,把所有 CPU 都已 cleanupInactiveTime(1 分钟)无活动的键删掉。节流状态机与 map 垃圾回收是两件事:STOP 不等于立刻从 hash 里消失。

BPF bpf/process/bpf_rate.hcgroup_rate()per-CPU hashBPF_MAP_TYPE_PERCPU_HASH)按 cgrpid 记账。越过阈值时 send_throttle() 发出 MSG_OP_THROTTLE,并把 val->throttled 设为当前时间;函数返回 !val->throttled——已节流则不再投递该次事件。滑动窗口(源码注释)把时间切成长度为 \(I\) 的区间,用上一窗与当前窗拼出瞬时速率:

\[ R = \frac{s \cdot n_{\mathrm{prev}}}{I} + n_{\mathrm{curr}}, \qquad s = I - (t - t_{\mathrm{win}}) \]

\(R \ge N\)--cgroup-rate 的事件数)且尚未节流时启动节流。这是实现细节,不是本机测得的 pps。

send_throttle() 在 BPF 里发出 MSG_OP_THROTTLE;用户态 updateCgroups() 再变成带 THROTTLE_STARTprocess_throttle 事件。STOP 只在用户态 ticker 上产生,没有对应的 BPF STOP 消息。排障时不要在 ring 里找 STOP opcode。

以下 JSON 来自 v1.7.0 文档示例,不是本仓库节点输出。THROTTLE_STOP 字段相同,只把 type 换成 THROTTLE_STOP。文档示例里停止负载循环后大约 5 秒出现 STOP。cgroup 字段在文档里是 systemd scope 名;源码 Check()kube.Docker 字节当作名字推进队列——cgroup 显示名与 Docker/cri id 截断如何对齐,本篇不编映射表,排障时以事件里实际出现的字符串为准。

{
  "process_throttle": {
    "type": "THROTTLE_START",
    "cgroup": "session-429.scope"
  },
  "node_name": "ubuntu-22",
  "time": "2024-07-26T13:07:43.178407128Z"
}

STOP 文档示例(同一页,时间戳不同):

{
  "process_throttle": {
    "type": "THROTTLE_STOP",
    "cgroup": "session-429.scope"
  },
  "node_name": "ubuntu-22",
  "time": "2024-07-26T13:07:55.501718877Z"
}
stateDiagram-v2
  [*] --> Passing: cgroup_rate enabled
  Passing --> Throttled: per-CPU rate crosses N in interval I
  Throttled --> Passing: all CPUs below limit stable 5s
  Passing --> Passing: EXEC EXIT posted
  Throttled --> Throttled: EXEC EXIT not posted

3. 误杀面

节流开始之后,该 cgroup 在超限 CPU 上的 PROCESS_EXEC / PROCESS_EXIT 不再投递。短命进程、fork 风暴、密集 sleep 循环(文档用 while :; do sleep 0.001s; done 演示)都会让血缘轴看起来「缺 exec」。这不是第 2 篇的 execve_map 未命中,也不是第 8 篇的 podSelector 没打中。看见 THROTTLE_START 却仍用「策略 YAML 写错了」解释缺口,坐标系就偏了。

文档 limitation 再强调一次:per-CPU;仅 base sensor 的 EXEC/EXIT。kprobe / tracepoint 策略事件不在这层节流里——它们走下一节的 Post.rateLimit 或环缓冲。


二、Post.rateLimit:同线程、同动作、参数前 40 字节

第 7 篇把 Post 标成默认动作。v1.7.0 selectors.md Rate limiting 段原文(不改写数字):

Post 接受 rateLimit 时间值;默认单位秒,后缀 m / h 表示分/小时。指定后,该动作会检查:同一动作是否已对同一线程、在时间窗口内、带着相同的被检查参数触发过。(匹配时每个被检查参数只用 前 40 字节。仅内核 v5.3 及以后。)

源码 bpf/process/ratelimit_maps.h 与文档对齐:KEY_BYTES_PER_ARG 为 40,注释写 40 对 IPv6 数据够用;ratelimit_keyfunc_idactiontid 以及 MAX_POSSIBLE_ARGS * 40 字节的 data。作用域常量:ACTION_RATE_LIMIT_SCOPE_THREAD(0)、PROCESS(1)、GLOBAL(2)。文档默认按线程;rateLimitScope: process 扩到进程内所有线程;global 扩到所有进程。

matchActions:
- action: Post
  rateLimit: 5m

ratelimit_mapBPF_MAP_TYPE_LRU_HASH,agent 在需要该功能时 resize;ratelimit_heap 是 per-CPU ARRAY,给参数拷贝留 128 字节余量以免 verifier 不通过。LRU 意味着键被挤出后,下一次相同参数会再次 Post——限流不是硬配额,是「最近见过则抑制」。

失败模式与 cgroup-rate 不同:这里丢的是 TracingPolicy 钩子事件,而且是「看起来像重复」的那一些。前 40 字节相同的 sockaddr / 路径前缀会被当成同一次;40 字节之后才分叉的参数会当成不同键,限流不生效。内核 < 5.3 时不要把 rateLimit 写成已生效能力。NoPost 则是另一条路:匹配后根本不 Post,与 rateLimit「偶发 Post」不是同一开关。


三、环缓冲压力:丢失计数,不是吞吐排名

BPF 经 perf/ring 把事件交给 pkg/observer。v1.7.0 pkg/observer/metrics.go 把计数器命名为:

指标 含义(Help 原文口径)
tetragon_observer_ringbuf_events_received_total ring buffer 收到的 perf 事件数
tetragon_observer_ringbuf_events_lost_total ring buffer 丢失的 perf 事件数
tetragon_observer_ringbuf_errors_total 读 ring buffer 时的错误数
tetragon_observer_ringbuf_queue_events_received_total 用户态事件队列收到的数量
tetragon_observer_ringbuf_queue_events_lost_total 用户态事件队列丢失的数量

Observer.PrintStats() 用 received 与 lost 算丢失比例并打日志;本篇不引用任何具体百分比。pkg/metrics/eventmetrics 另有 tetragon_bpf_missed_events_total(内核 perf_event_output 失败,带 error 标签:unknown / ENOENT / E2BIG / EBUSY / EINVAL / ENOSPC / EAGAIN)以及 tetragon_notify_overflowed_events_total(listener 缓冲满)。

两层丢失不要合成一个数:

历史名字 tetragon_missed_events_total / tetragon_ringbuf_perf_event_lost_total 出现在更早的 PR 叙述里;v1.7.0 源码里的符号以上表为准。

失败表象:策略匹配、cgroup 未节流、tetra 与 JSON 仍缺事件,同时 ringbuf_events_lost_totalringbuf_queue_events_lost_total 在涨。这是管道容量问题。本篇不下「加大 ring 就一定够」的未测结论,只把指标名字钉到 tag 源码。


四、三层丢失如何区分

覆盖哪些事件 可见信号 容易误判成
cgroup-rate 仅 EXEC/EXIT(文档);per-CPU process_throttle START/STOP 短命进程无血缘、策略没挂上
Post.rateLimit 该 hook 的 Post 事件变稀,无 START/STOP 选择器没匹配
ring / queue 所有经 observer 的事件 *_lost_totalbpf_missed_events_total 导出 denylist(第 4 篇 Caution:denylist 不影响 gRPC)

第 4 篇已经钉死:JSON allow/deny 过滤 gRPC / tetra。节流与环缓冲在过滤分层之前或并行:它们会让 两条 sink 一起缺事件。若只有 JSON 缺、tetra 仍在,先查第 4 篇而不是本节。

文档演示负载是 while :; do sleep 0.001s; donetetra getevents -o compact。本篇不转载带装饰符号的 compact 输出。能从文档保留的机制结论只有:START 出现在密集 EXEC/EXIT 之后;停止循环后约 5 秒出现 STOP。

观测税在本系列里的定义是:为了不被事件淹没而主动丢掉或合并可见性。税的大小没有本机 CPU% 数字。可核对的是:打开 --cgroup-rate 之后,能否在 START 与 STOP 之间解释 EXEC 空洞;打开 rateLimit 之后,40 字节键是否把不同连接收成同一键。

开放问题:cgroup-rate 只覆盖 EXEC/EXIT 且 per-CPU——多核上每核都低于 \(N\)、总和远高于 \(N\) 时不节流。这是文档写明的限制,不是 bug 清单能改掉的不变量。生产上「检出率 vs 管道存活」的可接受边界,第 16 篇回收。


五、学术谱系、争论与开放问题

谱系:Bishop & Dilger 1996 与 Garfinkel 2003 §4.3 讨论的是检查与使用之间的窗口;节流引入的是另一类窗口——监视器在过载时选择不看见。BPF 经典工作(McCanne & Jacobson, USENIX Winter 1993)把过滤推进内核以降低用户态负担;Tetragon 的 cgroup-rate 是同一方向上的速率阀,但阀门只装在 process 生命周期传感器上。

争论:完整性 vs 存活

主张 代价
不节流 EXEC/EXIT 与策略事件尽量完整,血缘可对齐 ring 丢失时缺口无 START/STOP 语义,更难归因
per-CPU cgroup-rate 过载 cgroup 被暂停投递,并留下 THROTTLE 事件 多核稀释;只覆盖 EXEC/EXIT;STOP 前血缘空洞

没有 benchmark 能在本篇宣布哪一侧 CPU 更低。可检验命题是:THROTTLE 事件是否进入你实际消费的那条 sink(第 4、12 篇)。

开放问题

  1. per-CPU 阈值与总速率:跨 CPU 均分的洪水是否应被当成超限。入口:tag cgroup-rate.md Limitations;bpf_rate.h 的 per-CPU map。
  2. 40 字节键碰撞 / 漏限流:IPv6 够用的假设对长路径、长 Unix sockaddr 是否仍成立。入口:selectors.mdKEY_BYTES_PER_ARG
  3. 节流与强制执行解耦:cgroup-rate 停的是 Post,不自动关掉 Override/Sigkill。过载时仍阻断但看不见 EXEC,是否可接受。入口:第 10 篇 mode 与本篇误杀面。

六、小结

  1. --cgroup-rate 默认关;打开后只按文档覆盖 PROCESS_EXEC / PROCESS_EXIT,并且 per-CPU
  2. THROTTLE_START 表示该 cgroup 在某 CPU 上越过阈值;THROTTLE_STOP 表示限值之下稳定约 5 秒。
  3. Post.rateLimit:内核 ≥5.3;默认同线程;每个被检查参数前 40 字节。
  4. tetragon_observer_ringbuf_events_lost_total 等指标解释管道丢失;本篇无 pps 实测。
  5. 节流造成的空洞会同时打到 JSON 与 gRPC,与第 4 篇 denylist 只打 JSON 的 Caution 不同。

下一篇把这些计数器放到升版与 SIEM 接线里:哪些指标在 v1.7.0 改了名字,以及文件 sink 才是日志管道能看见的那一侧。


七、参考资料

规范 / 官方文档(A)

源码(A)

核心论文

站内对照

实验台账


上一篇:强制执行 · 系列目录 · 下一篇:运维与导出工程

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .