本节摘要:验证器挡住越界,挡不住你申请两千万条目的哈希表,也挡不住你在每个系统调用上走完接近上限的指令。memlock、Map 容量、指令数、栈大小、运行时复杂度,是第二套刹车。eBPF 安全包含“不会写坏内存”和“不会把节点合法地饿死”两层。
阅读完本节,你应当能够:
通过验证的程序仍可能是一台合法的吸尘器:每个包查三张大表,每个系统调用送 4KB 事件。内核必须在证明安全之后继续记账。
加载期。 指令条数、验证状态数、程序大小、Map 的键值尺寸和条目上限、锁定内存额度。超了,加载失败。这是好失败:还没挂上。
运行期。 单次执行的复杂度要可预测;Map 插入在满员时失败;环形缓冲区满了丢事件;某些 Helper 有速率限制。运行期失败往往静默,程序还在,数据没了。
医院监护仪不会因为“电极贴法安全”就允许它以每秒百万次写盘。病房的电源和护士注意力也是资源。eBPF 的资源限制就是病房配电。
| 限制 | 主要发生在 | 超限表现 | 处理 |
|---|---|---|---|
| 指令数与复杂度 | 加载 | 验证拒绝 | 拆程序、减循环 |
| 栈大小 | 加载 | 验证拒绝 | 少放局部大结构 |
| memlock / cgroup 内存 | 加载或扩表 | 创建 Map 失败 | 降容量、提额度、算预算 |
| Map 条目 | 运行 | 插入失败 | 告警、限基数、LRU 明确语义 |
| 环形缓冲 | 运行 | 丢事件 | 丢计数、降采样、加大缓冲 |
| 跟踪打印 | 运行 | 被限速或禁用 | 不要当生产日志 |
连接跟踪表最容易把节点吃满。假设每条 128 字节,一百万连接是一百多兆,再乘 per-CPU 或副本,再加上内核自己的连接表。eBPF Map 不是“再开一块无限缓存”。
预算算法很土,但必须写在设计里:
条目上限 × 键值对齐后大小 × 副本或 CPU 数 + 程序与 JIT 映像 + 环形缓冲 = 每节点 eBPF 内存 对照 memlock 与节点余量
开发机 ulimit 很大,生产容器默认很小。于是出现“我家能加载,流水线不能”。这不是验证器问题,是额度问题。第 8 章部署要把额度做成模板,而不是每个操作员手调。
⚠️ 常见坑:把 max_entries 写成一个“足够大的整数”以求永不插入失败。你把 OOM 从应用层搬到内核记账层,排障更难。
💡 关键直觉:宁可插入失败并告警,也不要默默占掉节点上给业务留的内存。失败可见,膨胀不可见。
XDP 和系统调用入口上,多一次哈希、一次字符串扫描,都会变成 P99。验证器保证你停得下来,不保证你停得快。复杂度上限按“最坏路径指令数”想,不要按“常见路径很快”想。攻击者会打你的最坏路径。
实践:
多程序叠加时,内核会在同一事件上跑多条。你的程序“很轻”乘以十个团队各挂一条,节点照样变慢。平台要有挂钩清单和预算,不能只审单个程序。
额度只是许可。物理内存、缓存污染、插入延迟不会一起消失。把表开到接近节点内存,是在用 eBPF 做数据库,这不是它的工作。
两条线。验证器管证明;记账管配额。有时看起来像一次失败,实际是创建 Map 时额度不够。日志和 errno 要一起看。
假设某节点 32 核、64 吉字节内存,计划常驻三套程序:系统调用延迟直方图、XDP 丢包、cgroup 连接限制。预算可以写成:延迟程序 per-CPU 数组可忽略;事件环形缓冲 8 兆,丢失告警阈值每秒 1000;XDP 无大表;连接 HASH 20 万条目,每条约 64 字节,约 12 兆,再加余量到 20 兆。三套合计远小于节点内存的百分之一,但要写进目录,防止第四套“临时”表再来 200 万条目。
CPU 预算按最热钩子算。系统调用跟踪若过滤后每核每秒 2 万次、每次 200 纳秒,大约占用不到百分之一核,可接受。若有人关掉过滤,次数跳到 50 万,占用变成不可接受。所以过滤不是功能,是预算的一部分,开关默认必须开着。XDP 程序每次必须短到能在线速下忽略,否则宁可不挂。
memlock 不要设成无限。无限等于取消第二道闸。设成预算的两倍,让意外的新表在加载失败,而不是在页回收里表现为全节点抖动。失败可见,抖动不可见。容器的记账若和主机 memlock 不一致,以更严的那档为准,并在文档里写明,避免开发者在主机测通、在容器挂不上。
资源告警要有所有者。Map 内存、插入失败、丢失计数,三条告警路由到程序所有者,而不是泛泛的内核组。内核组无法知道你的连接表是不是应该有 20 万。所有权与第 8 章清单是同一件事的资源面。
压测时把预算当断言:超过 CPU 或内存阈值即失败,即使功能正确。功能正确但超预算,对生产来说就是不正确。这能挡住“先上线再优化”的探针,那些探针很少真的会再优化。
应急时开了 200 万条目的 HASH,“先扛过今晚”。今晚过了,表还在,节点内存水位永久抬了一截,回收更勤,业务尾延迟变差。临时没有超时,就是永久。后来临时表必须带 TTL 和所有者,到期由巡检卸掉,卸不掉升级为事故。
memlock 在容器里比主机严,开发机测通、流水线失败。有人把流水线额度调成无限,问题消失,生产容器又失败。正确是让开发容器和流水线、生产使用同一档额度。三套额度是三套现实,程序会在夹缝里假装健康。
CPU 预算没当断言,功能测试全绿,上线后最坏路径被打满。后来压测把最坏路径当必测:构造深嵌套或大表满员,超预算即失败。功能正确但超预算,对生产就是不正确。这句话写进门禁比写进周报有用。
功能测试绿而超预算,对生产就是红。压测必打最坏路径。临时表必须到期和所有者,到期卸不掉当事故。三套额度对齐:开发容器、流水线、生产,取最严。无限 memlock 是拆掉第二道闸。告警有所有者,内核组无法知道你的表是否该有二十万条。CPU 预算按过滤打开时的频率算,过滤是预算的零件,默认必须开。关掉过滤等于超预算运行。超预算运行碰巧没炸,不能写进月报当优化完成。月报要写预算数字和实测数字的差。差在变大,就要减功能,不是加额度。加额度是把房间的保险丝换粗,火灾还在。保险丝粗了,烧的是整层。整层是节点上的业务。业务比探针贵,所以探针让路。让路写在门禁里,不写在口号里。
资源限制看起来像在找茬,其实在替你挡合法灾难。合法灾难比非法越界更难复盘,因为它不 oops,只是慢慢把业务挤瘦。挤瘦会先被当成业务增长。增长会带来更多连接,更多连接把表撑得更满。满了还不告警,检测器就瞎了。瞎了还以为健康。健康的假象是资源闸门存在的理由。把闸门拆掉换无限额度,等于允许假象生长。生长到页回收打颤时,再找 eBPF 的人,已经晚了一个容量规划周期。周期以周计。故障以分钟计。对不上的时候,先怪额度,再怪自己没把预算当断言。
\n\n## 课堂补充\n\n合法灾难不oops只挤瘦业务。满员不告警检测器就瞎。临时无到期就是永久。三套额度取最严。过滤是预算零件。超预算的正确仍是错误。告警路由到所有者。无限memlock是拆闸。最坏路径必测。多程序要总账。eBPF不是数据库。失败可见膨胀不可见。\n\n\n\n## 生产验收条\n\n1. 围绕「资源限制如何防止拖垮内核」,生产验收只认能关掉、能计数、能对账,不认口头保证。\n2. 围绕「资源限制如何防止拖垮内核」,把所有者、版本、卸载方式写成清单三件套,缺一视为幽灵。\n3. 围绕「资源限制如何防止拖垮内核」,对照实验必须能回答开关前后业务指标动了没有。\n4. 围绕「资源限制如何防止拖垮内核」,失败要分类到验证、额度、挂载、未触发、丢失,禁止只丢一句笼统错误。\n5. 围绕「资源限制如何防止拖垮内核」,热路径默认克制,阈值之后才出栈,出栈之前先证明钩子活着。\n6. 围绕「资源限制如何防止拖垮内核」,和平台已有挂钩点冲突时先登记再加载,禁止手工抢挂。\n7. 围绕「资源限制如何防止拖垮内核」,内核版本矩阵没跑绿就不能把功能写成必成功路径。\n8. 围绕「资源限制如何防止拖垮内核」,回滚必须碰到内核对象,心跳停止才算撤回成功。\n\n## 一节小结
下一章离开内核契约,看人怎么把这些程序写出来、选哪套工具,才交得出去。