第 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原文来自同 tagselectors.md。无压测则不写 CPU 占用百分比,不伪造每秒事件数。
本篇落到五轴里的 过滤分层 之后、导出之前:内核已经决定 Post,用户态仍可能收不到。
一、cgroup-rate:只节流 base sensor 的 EXEC / EXIT,而且是 per-CPU
1. 文档钉死的范围
cgroup-rate.md 的概念:按 cgroup
监视事件速率,超过阈值就停止向用户态投递该 cgroup
的事件;速率在限值之下稳定后恢复。文档两条 note
必须一起读:
- 阈值按 per CPU 监视。事件分散在多颗 CPU 上时,只有那颗 CPU 上超过阈值才会节流那颗 CPU。
- 当前只监视并限制 base sensor
事件:
PROCESS_EXEC、PROCESS_EXIT。
开关是
--cgroup-rate,默认关闭。cgrouprate.go
在 opts.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() 把
Events 与 Interval 写进去;BPF
cgroup_rate() 读到 interval == 0
则直接放行。
源码侧需要一句对齐,避免把文档读窄或读宽:pkg/cgrouprate/cgrouprate.go
注释写监视 exec/fork/exit;RegisterCgroupRate
把 cgroup_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 语义
文档:
- 超过该 cgroup 速率上限时发
THROTTLEstart。 - 速率重新低于上限并 稳定 5 秒 后发
THROTTLEstop。
用户态常量与文档一致:aliveCnt = 5,process()
里每秒 tick 一次
processCgroups()。processCgroup()
在所有 CPU 要么已经静默超过 5 秒、要么 rate
低于阈值且已暂停满 5 秒时,清掉 Throttled
并发送 THROTTLE_STOP。Check()
从事件处理路径把该 cgroup
推进队列;updateCgroups() 第一次见到该 id
时发出 THROTTLE_START。源码注释承认与 BPF
cgroup_rate 存在竞态:用户态清
Throttled
的同时内核可能再次超限,则下一次事件会重新节流。
map 上限
cgRateMaxEntries = 32768(注释写可再调)。cleanupCgroups
每 cleanupInterval(1 分钟)扫一次,把所有 CPU
都已 cleanupInactiveTime(1
分钟)无活动的键删掉。节流状态机与 map
垃圾回收是两件事:STOP 不等于立刻从 hash 里消失。
BPF bpf/process/bpf_rate.h 的
cgroup_rate() 用 per-CPU
hash(BPF_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_START 的 process_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_key 含
func_id、action、tid
以及 MAX_POSSIBLE_ARGS * 40 字节的
data。作用域常量:ACTION_RATE_LIMIT_SCOPE_THREAD(0)、PROCESS(1)、GLOBAL(2)。文档默认按线程;rateLimitScope: process
扩到进程内所有线程;global 扩到所有进程。
matchActions:
- action: Post
rateLimit: 5mratelimit_map 是
BPF_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
缓冲满)。
两层丢失不要合成一个数:
- 内核没写进
ring:
tetragon_bpf_missed_events_total(尤其ENOSPC/EBUSY)。 - ring 里有
LostSamples:
tetragon_observer_ringbuf_events_lost_total(observer 读record.LostSamples)。 - 用户态队列满:
tetragon_observer_ringbuf_queue_events_lost_total(eventsQueue的default分支直接丢)。 - listener
满:
tetragon_notify_overflowed_events_total。
历史名字 tetragon_missed_events_total /
tetragon_ringbuf_perf_event_lost_total
出现在更早的 PR 叙述里;v1.7.0 源码里的符号以上表为准。
失败表象:策略匹配、cgroup 未节流、tetra 与
JSON 仍缺事件,同时 ringbuf_events_lost_total
或 ringbuf_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_total、bpf_missed_events_total |
导出 denylist(第 4 篇 Caution:denylist 不影响 gRPC) |
第 4 篇已经钉死:JSON allow/deny 不过滤
gRPC /
tetra。节流与环缓冲在过滤分层之前或并行:它们会让
两条 sink 一起缺事件。若只有 JSON
缺、tetra 仍在,先查第 4 篇而不是本节。
文档演示负载是
while :; do sleep 0.001s; done 加
tetra 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 篇)。
开放问题:
- per-CPU 阈值与总速率:跨 CPU
均分的洪水是否应被当成超限。入口:tag
cgroup-rate.mdLimitations;bpf_rate.h的 per-CPU map。 - 40 字节键碰撞 / 漏限流:IPv6
够用的假设对长路径、长 Unix sockaddr
是否仍成立。入口:
selectors.md;KEY_BYTES_PER_ARG。 - 节流与强制执行解耦:cgroup-rate 停的是 Post,不自动关掉 Override/Sigkill。过载时仍阻断但看不见 EXEC,是否可接受。入口:第 10 篇 mode 与本篇误杀面。
六、小结
--cgroup-rate默认关;打开后只按文档覆盖PROCESS_EXEC/PROCESS_EXIT,并且 per-CPU。THROTTLE_START表示该 cgroup 在某 CPU 上越过阈值;THROTTLE_STOP表示限值之下稳定约 5 秒。Post.rateLimit:内核 ≥5.3;默认同线程;每个被检查参数前 40 字节。tetragon_observer_ringbuf_events_lost_total等指标解释管道丢失;本篇无 pps 实测。- 节流造成的空洞会同时打到 JSON 与 gRPC,与第 4 篇 denylist 只打 JSON 的 Caution 不同。
下一篇把这些计数器放到升版与 SIEM 接线里:哪些指标在 v1.7.0 改了名字,以及文件 sink 才是日志管道能看见的那一侧。
七、参考资料
规范 / 官方文档(A)
- Tetragon v1.7.0 Event throttling —
docs/content/en/docs/concepts/cgroup-rate.md(不要用 live/concepts/event-throttling/)。 - Tetragon v1.7.0 Selectors —
PostRate limiting 段(线程、40 字节、内核 v5.3)。 - Tetragon v1.7.0 Events — JSON / gRPC 分流 Caution(与本节对照)。
源码(A)
pkg/cgrouprate/cgrouprate.go(aliveCnt、THROTTLE_START/STOP、RegisterCgroupRate)@ v1.7.0bpf/process/bpf_rate.h(cgroup_rate、send_throttle、per-CPU hash)@ v1.7.0bpf/process/ratelimit_maps.h(KEY_BYTES_PER_ARG、ratelimit_key)@ v1.7.0pkg/observer/metrics.go(RingbufLost等)@ v1.7.0pkg/metrics/eventmetrics/eventmetrics.go(MissedEvents、NotifyOverflowedEvents)@ v1.7.0
核心论文
- Bishop, Dilger. Checking for Race Conditions in File Accesses. Computing Systems 9(2), 1996.
- Garfinkel. Traps and Pitfalls. NDSS 2003, §4.3(监视完整性假设)。
- McCanne, Jacobson. The BSD Packet Filter. USENIX Winter 1993(内核过滤以降负载;外链 ebpf/,不重写)。
站内对照
实验台账
--cgroup-rate未在本环境跑;THROTTLE JSON 仅引用文档示例。无 CPU% / pps 数字。
→ 上一篇:强制执行 · 系列目录 · 下一篇:运维与导出工程
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【Tetragon / eBPF】运行时安全全景:缺口、五轴坐标系与 16 篇路线
相对 Cilium 数据面、可观测对照与 Falco 口号,钉清 Tetragon v1.7.0 运行时安全内核缺口;定义进程血缘、Sensor/Hook、策略加载、过滤分层、强制执行五条坐标系,给出 16 篇阅读路线。
【Tetragon / eBPF】进程模型:exec_id、血缘、in_init_tree 与 procfs 回填
钉清 Tetragon v1.7.0 的进程身份:process_exec/exit 字段、exec_id 如何由 node:ktime:pid 生成、execve_map 与用户态 cache 的分工、in_init_tree 如何区分 kubectl exec 注入,以及 fork 时父不在 map 则子不入图的失败路径。
【Tetragon / eBPF】Sensor 与 Hook:进程传感器、kprobe、tracepoint、uprobe、LSM 与 fentry
钉清 Tetragon v1.7.0 内置 __base__ 进程传感器与 TracingPolicy 动态 hook 的分工:kprobe/kretprobe、tracepoint/rawtp、uprobe/USDT、LSM 与 fentry 各自能观察什么、依赖哪些内核能力,以及选错 hook 类型时的失败表象。
【Tetragon / eBPF】事件导出路径:JSON、gRPC、tetra 与过滤边界
钉清 Tetragon v1.7.0 事件离开 agent 之后的分流:文件 JSON、gRPC 与把 JSON 再喂给 tetra 不是同一集合;官方 Caution 写明 allow/deny 只作用于 file sink;field filter 与 redaction 各在哪一层生效。