eBPF

Posted by fjw on August 11, 2023

eBPF 主要挂载点性能分析

eBPF 挂载点(probe/attachment types)的性能比较是性能分析中最核心的话题,选错类型可能导致系统整体延迟上升 10–50%+,甚至触发 soft lockup 或测量失真。

主流内核的单次触发 overhead 排名(ns 级,热路径下,JIT 启用,x86-64/arm64 类似)。

关键说明

  • 性能度量:单次触发/执行 overhead (ns),热路径下(高频,如每秒百万级)。数据来源于 iximiuz 测试(fentry 最快)、Cloudflare benchmark(tracing: fentry > raw_tp > kprobe)、Cilium XDP 测试(~10 ns/packet)、LWN/Alexei 原始提交(raw_tp 比 tp 快 10–30%)。
  • Networking 类:这些不是“probe”而是“inline 处理器”,开销极低(早路径),但高频包下累积影响大(e.g., XDP driver mode 比 skb mode 快 2–5x)。fentry 可 attach 到它们以 tracing(额外 ~20–80 ns)。
  • 通用结论:Tracing 类优先 fentry/raw_tp(低开销)。Networking 类优先 XDP(最早路径)。整体,eBPF verifier + JIT 确保 <1% 系统影响,但高频下需测试。
  • 排序依据:overhead 从低到高(ns 级),次看高频影响(%)。数据是典型值(x86/arm64,JIT 启用,6.x 内核);实际依负载/架构变。

分类为:

  • Tracing/Observability 类:通用监控/剖析,焦点是低开销事件捕获(之前表格的核心)。
  • Networking/流量专用类:如 XDP/TC/cgroup_skb 等,焦点是包处理/过滤,开销以 per-packet ns 计算(非全量 probe,而是 inline 在网络路径)。
  • Other/杂类:如 cgroup_device、LSM 等,性能视场景而定。

1. Tracing/Observability 类(通用监控,焦点:事件捕获开销)

排名 挂载点/程序类型 典型单次 overhead (ns) 高频场景影响(每秒百万级) 机制核心 为什么这个性能 推荐场景(自动驾驶/中间件) 内核版本要求 SEC 示例
1 fentry / fexit / fmod_ret (BPF_PROG_TYPE_TRACING) 20–80 ns(最优 30–50 ns) <1% CPU/延迟污染 trampoline + direct call + JIT native 无异常、无上下文切换,像普通函数调用 内核函数热点(vfs_read、tcp_v4_rcv、bio_submit 等),也可 tracing XDP/TC/cgroup 程序本身 5.5+(fentry/fexit),5.7+ fmod_ret fentry/vfs_read
2 raw_tracepoint / raw_tp (BPF_PROG_TYPE_RAW_TRACEPOINT) 80–180 ns(常见 100–150 ns) 1–4% tracepoint 框架 + raw args(零拷贝) 去掉 ctx 构造,比普通 tp 快 10–30% 高频 syscall/调度/网络,追求极致低开销 4.17+ raw_tp/sched_switch
3 tracepoint (普通 tp) 100–250 ns 1–3% tracepoint 框架 + 构造稳定 ctx 静态标记 + 少量拷贝,但有预处理 稳定事件(sched_switch、netif_receive_skb、sys_enter_*),跨版本最稳 很早 tp/sched/sched_switch
4 tp_btf (BTF-enabled tracepoint) 接近 raw_tp(略高,90–200 ns) 类似 raw_tp raw_tp 路径 + BTF 重定位 兼顾低开销 + 参数友好 现代工具首选(libbpf CO-RE) 5.6+ tp_btf/sched_switch
5 SDT / USDT (用户态静态探针,BPF_PROG_TYPE_TRACING) 150–400 ns(常见 200–300 ns) 1–5% 静态用户标记 + uprobe-like attach + trampoline 优化 静态 + 少量用户→内核切换,比 uprobe 低 2–5x 应用内部自定义事件(mysql query、redis command、nginx request),中间件热点 4.7+(基本),5.5+(trampoline 优化) usdt:/bin/myapp:probe1
6 kprobe / kretprobe (BPF_PROG_TYPE_KPROBE) 200–800+ ns(热路径 400–600 ns) 5–20%+(灾难级) int3 异常 + kprobes 框架 + singlestep 异常路径 + 寄存器保存/恢复 需要任意 offset 或老内核,慎用热函数 很早 kprobe/vfs_read
7 uprobe / uretprobe (BPF_PROG_TYPE_KPROBE) 500–2000+ ns(甚至更高) 10–200%+(严重污染) 用户态 → 内核切换 ×2 + int3 用户→内核往返 + 额外上下文切换 用户态库/应用函数(libc、java、go、libtorch),生产极慎用 很早 uprobe:/lib/libc.so:read
8 perf_event (采样,BPF_PROG_TYPE_PERF_EVENT) 采样周期决定(Hz 级,非全量) 低(采样非全量) PMU/软件事件采样 非 inline 探针,适合 profiling CPU/缓存/分支预测采样,非精确延迟测量 很早 perf_event
9 LSM (BPF_PROG_TYPE_LSM) 200–500 ns(类似 kprobe) 2–10% LSM 钩子 + trampoline-like 安全检查路径,类似 fentry 但有决策开销 安全模块(task_fix_setuid 等),访问控制 5.7+ lsm/task_fix_setuid

2. Networking/流量专用类

这些是网络栈 inline 程序,非“probe”而是“处理器/过滤器”。开销以 ns/packet 计算(高频包下累积 ~1–5% 系统 CPU)。fentry 可 attach 以 tracing 它们(额外 20–80 ns)。基于 Cilium 测试(XDP 最快)、iximiuz benchmark(XDP driver mode 优于 skb)。

排名 挂载点/程序类型 典型单次 overhead (ns/packet) 高频场景影响(每秒百万包) 机制核心 为什么这个性能 推荐场景(自动驾驶/中间件) 内核版本要求 SEC 示例
1 XDP (BPF_PROG_TYPE_XDP) 5–50 ns(driver mode 最优 10–20 ns) <1%(早路径) 驱动接收前处理(drop/filter) 最早网络路径,几乎零开销(hardware offload <5 ns) DDoS 防护、负载均衡、早包过滤(自动驾驶网关) 4.8+ xdp 或 xdp/dev
2 TC (sched_cls / sched_act) (BPF_PROG_TYPE_SCHED_CLS / ACT) 50–200 ns(cls 稍低) 1–3% 流量分类/动作(qdisc 后) 网栈中段,稍多拷贝但高效 流量整形、分类、转发(中间件路由) 4.1+ classifier 或 action
3 cgroup_skb (BPF_PROG_TYPE_CGROUP_SKB) 100–300 ns 1–5% cgroup 级包过滤(ingress/egress) 容器边界过滤,有 cgroup 切换 容器网络安全、过滤(Kubernetes pod 流量) 4.10+ cgroup_skb/ingress
4 sock_ops (BPF_PROG_TYPE_SOCK_OPS) 100–400 ns 2–5% 套接字操作钩子(connect/bind 等) 连接事件处理,类似 tp 但网络专用 TCP 参数调优、redir(服务网格 sidecar) 4.13+ sockops
5 sk_msg (BPF_PROG_TYPE_SK_MSG) 150–400 ns 2–6% 消息级重定向/过滤(sendmsg/recvmsg) 应用层前过滤,有 msg 解析 服务发现、重定向(Envoy proxy) 4.18+ sk_msg
6 cgroup_sock_addr (BPF_PROG_TYPE_CGROUP_SOCK_ADDR) 150–500 ns 2–7% cgroup 套接字地址修改(bind/connect) 地址操作钩子,类似 sock_ops 端口重映射、NAT(容器网络) 4.17+ cgroup/sock_addr
7 sk_skb (BPF_PROG_TYPE_SK_SKB) 200–500 ns 3–8% 流级包处理(verdict) 网栈较后,类似 cgroup_skb 但流导向 流追踪、过滤(旧式防火墙) 4.12+ sk_skb/stream_parser
8 flow_dissector (BPF_PROG_TYPE_FLOW_DISSECTOR) 100–300 ns 1–4% 流解析(hash/分类) 早解析路径,高效但专用 负载均衡 hash、分类(高吞吐网关) 4.20+ flow_dissector
9 netfilter (BPF_PROG_TYPE_NETFILTER) 200–600 ns 3–10% Netfilter 钩子替换(iptables) 防火墙路径,类似 TC 但更通用 传统防火墙迁移(但 XDP/TC 更快) 5.17+ netfilter

3. Other/杂类(性能视具体钩子)

这些较少用于性能剖析,开销类似 tracing 类(~100–500 ns),焦点是控制/决策。

排名 挂载点/程序类型 典型单次 overhead (ns) 高频场景影响 机制核心 为什么这个性能 推荐场景 内核版本要求 SEC 示例
1 cgroup_device (BPF_PROG_TYPE_CGROUP_DEVICE) 100–300 ns 1–3% 设备访问控制(open/read 等) cgroup 边界检查,低开销决策 容器设备权限(GPU/存储访问) 4.15+ cgroup/dev
2 cgroup_sysctl (BPF_PROG_TYPE_CGROUP_SYSCTL) 150–400 ns 1–4% sysctl 修改钩子 配置变更控制,类似 LSM 容器 sysctl 限制 5.2+ cgroup/sysctl
3 sk_reuseport (BPF_PROG_TYPE_SK_REUSEPORT) 100–300 ns 1–3% 端口复用选择(accept) 连接分发,低开销 SO_REUSEPORT 负载均衡(高并发服务器) 4.12+ sk_reuseport
4 sk_lookup (BPF_PROG_TYPE_SK_LOOKUP) 150–400 ns 1–5% 套接字查找/重定向 早连接路由 服务网格路由(Istio) 5.9+ sk_lookup
5 lwt (BPF_PROG_TYPE_LWT_*) 200–500 ns 2–6% 轻量隧道(in/seg6/out) 隧道封装/解封 IPv6 段路由(SRv6) 4.10+ lwt_in
6 struct_ops (BPF_PROG_TYPE_STRUCT_OPS) 200–600 ns 2–7% 结构体操作替换(TCP cong 等) 自定义内核行为 自定义 TCP 拥塞控制 5.6+ .struct_ops
7 ext (BPF_PROG_TYPE_EXT) 视基程序(+50–200 ns) +1–3% 扩展/替换现有程序 模块化 eBPF 动态替换程序(无重载) 4.15+ ext
8 syscall (BPF_PROG_TYPE_SYSCALL) 300–800 ns 3–10% 系统调用替换 用户态 syscall 钩子 自定义 syscall(实验性) 5.20+ syscall
9 lirc_mode2 (BPF_PROG_TYPE_LIRC_MODE2) 200–500 ns 2–5% IR 遥控解码 专用硬件路径 红外设备处理(嵌入式) 4.18+ lirc_mode2

建议

  • 全链路监控:Tracing (fentry/raw_tp) + Networking (XDP/TC) 组合,覆盖内核 + 网络延迟。e.g., 用 fentry tracing XDP 程序的 return codes(iximiuz 示例)。
  • 高频路径:优先 XDP (最早) > TC > cgroup_skb。Tracing 优先 fentry > raw_tp(搜索结果一致:fprobes 低开销胜 kprobe)。
  • 测试工具:bpftrace(快速原型)、libbpf-tools(生产)、Cilium Hubble(网络 observability)。
  • 潜在坑:高频下,verifier 开销 + map 访问可加 10–50 ns;用 CO-RE + JIT 缓解。
  • 未来趋势:fentry 扩展到更多类型(e.g., tracing user programs),XDP hardware offload 推到 <5 ns。

总结

  • fentry/fexit 是当前 tracing overhead 的天花板(trampoline + direct call,几乎零额外异常),在热点函数上比 kprobe 快 5–10 倍,比 raw_tp 快 1.5–3 倍(Cloudflare benchmark、iximiuz 测试一致)。
  • raw_tp 比普通 tracepoint 快 10–30%(Alexei 原始 benchmark:task_rename 1.0M vs 947K events/s),但仍逊于 fentry(因为还有 tracepoint 框架间接调用)。
  • tracepoint / raw_tp稳定 + 低开销的黄金组合,尤其有预定义事件时(sched、net、block、syscall 类)。
  • kprobe 只在必须挂函数中间指令(+offset)或内核 <5.5 时用,否则性能毒瘤。
  • uprobe 开销最大(用户态上下文切换 ×2),生产中高频函数(如推理引擎循环)用 uprobe 基本会把性能拉垮 50–200%(Oligo Security 2024 测试、Cycode 2025 案例)。

benchmark 摘录

  • Alexei raw_tp 引入时(LWN 2018,至今经典):raw_tp 在高频 syscall 上比 tracepoint 快 ~20%,比 kprobe 明显快。
  • Cloudflare ebpf_exporter(持续维护):tracepoint > fentry > kprobe(单核 getpid 测试,fentry 最接近零开销)。
  • Cycode/Cimon优化:raw_tp > tracepoint > fentry > kprobe > uprobe(但 fentry 在函数级热点上反超 raw_tp)。
  • iximiuz Labs / eBPFChirp:fprobes (fentry) 远低于 kprobe,尤其热函数;raw_tp 略优于普通 tp。

核心交互方式

eBPF(扩展Berkeley Packet Filter) 的内核态(Kernel)和用户态(Userspace)之间的交互,主要依赖几种精心设计的高效、安全机制。下面从性能分析专家的角度,完整梳理目前主流的交互方式,以及各自的适用场景、优缺点和性能关注点。

核心交互方式汇总

交互方向 主要机制 引入内核版本 单次拷贝次数 是否保序 吞吐能力排序 典型使用场景 性能关键点 / 注意事项
用户态 → 内核态 BPF_MAP_UPDATE_ELEM 早期 1 次 - 下发配置、控制参数、规则更新 锁竞争(尤其是hash map)、更新频率不宜过高
内核态 → 用户态 Perf Buffer (BPF_PERF_EVENT_OUTPUT) ~4.1 2~3 次 中低 早期日志、少量事件 页面拷贝 + 用户态poll开销大,已逐渐被淘汰
内核态 → 用户态 Ring Buffer (BPF_RINGBUF) 5.8 0~1 次 高频事件、日志、指标、Tracing 当前最推荐,reserve/commit模型最优,内存效率高
内核态 → 用户态 User Ring Buffer 5.19+ 0 次 极高 用户态主动向内核“投递”数据(反向最高效) 内核直接读用户态内存,需pin页面,安全性要求高
双向 Hash / Array / LRU Map 早期 1 次/操作 - 状态保存、计数器、缓存、键值对配置 存在锁(per-cpu最优)、False sharing风险
双向(特殊) Queue / Stack Map 中期 1 次 是(FIFO/LIFO) 生产者-消费者模型 push/pop 开销较小,但容量有限
双向(低频大块) Struct / Prog Array 等 - 1 次 - 稀疏配置、大结构体 不适合高频读写

最常用的几种组合

  1. 最高性能组合(目前主流推荐) 内核态写 → Ring Buffer(bpf_ringbuf_reserve / commit / submit) 用户态读 → ring_buffer__consume() / poll()(libbpf封装) 用户态写 → BPF_MAP_UPDATE_ELEM(普通map)或 User Ring Buffer(极致场景)

    为什么性能最好?

    • Ring Buffer 几乎零拷贝(只在边界有一次可能拷贝)
    • 生产者(内核)不需要锁,消费者(用户态)可以多线程消费
    • 事件顺序严格保证
    • 内存利用率远高于 perf buffer(没有固定大小的perf event header浪费)
  2. 经典但已被逐步替代的组合 内核态:bpf_perf_event_output() → BPF_MAP_TYPE_PERF_EVENT_ARRAY 用户态:perf_buffer__poll() 或 read()

    性能瓶颈(为什么不推荐高吞吐场景):

    • 每次输出都要经过 perf 事件框架 → page 拷贝 → 用户态再拷贝
    • 存在全局锁竞争(perf ring buffer 管理)
    • 丢事件概率较高(满时直接丢)
  3. 控制 + 少量状态双向 用户态 → Hash Map / Per-CPU Hash / Array(写配置、采样率、开关) 内核态 → Ring Buffer(输出事件/指标) + Per-CPU Array(原子计数)

性能分析的关注点

  • 拷贝次数:Ring Buffer / User Ring Buffer 是目前拷贝最少的方案。
  • 锁竞争:优先用 BPF_MAP_TYPE_PER_CPU_HASH / PER_CPU_ARRAY 消除锁。
  • 内存效率:Ring Buffer > Perf Buffer > 普通 Hash(Ringbuf 没有固定大小的 header 浪费)。
  • 丢数据容忍度
    • 能丢 → perf buffer 仍然可用(实现最简单)
    • 不能丢 → ring buffer(reserve 失败才丢,可统计丢包数)
  • 延迟:Ring Buffer poll 延迟通常在微秒级,perf buffer 稍高。
  • CPU 开销:User Ring Buffer 最极致(内核直接读用户内存),但使用门槛高、需 pin 内存页。

eBPF的应用cilium

Cilium 的 eBPF 应用是目前 Kubernetes 网络领域最成熟、最广泛使用的 eBPF 数据路径实现之一,尤其在取代 kube-proxy(包括 iptables 和 IPVS 模式)方面表现突出。下面从实现原理、挂钩阶段、核心机制和为什么比 IPVS 好四个维度给出完整拆解(基于 Cilium 官方文档、Isovalent benchmark实测数据)。

Cilium eBPF 应用实现(核心数据路径架构)

Cilium 完全基于 eBPF 构建分布式网络数据路径(datapath),不依赖 iptables/IPVS,而是把所有网络逻辑(路由、负载均衡、NAT、策略执行、可观测性)直接注入内核

  • 控制平面(用户态):Cilium agent(运行在每个 node 上)监听 Kubernetes API(Service、Endpoint、Pod 等变化),动态生成/更新 eBPF map(如 BPF_HASH 用于 service 查找、BPF_LRU_HASH 用于连接跟踪)。
  • 数据平面(内核态):多个 eBPF 程序挂载到不同内核钩子(hook points),共同组成完整的 datapath 流水线。
    • Pod-to-Pod(东-西流量):socket-level LB(socket LB),在 connect() 时直接改 dst IP/port。
    • 外部到 Service(北-南流量):XDP + tc 级转发 + NAT。
    • 策略执行:NetworkPolicy 在 tc/cgroup 层强制执行(L3–L7)。

核心 eBPF 挂钩类型与阶段(按包/连接处理顺序):

挂钩类型 程序类型 (BPF_PROG_TYPE_*) 挂钩阶段(内核路径) 主要功能(Cilium 中) 开销级别(ns/packet 或 per-connect)
XDP XDP 网卡驱动最早阶段(接收前) 北-南流量早 drop/filter、NodePort 外部入口、高性能 LB(DSR 模式) 5–50 ns(driver mode 最优)
tc ingress SCHED_CLS / SCHED_ACT __netif_receive_skb_core() 内 入方向包处理、策略执行、路由决策 50–150 ns
tc egress SCHED_CLS / SCHED_ACT 出方向 qdisc 前 出方向包处理、encap(VXLAN/Geneve)、策略执行 50–150 ns
cgroup_sock_addr CGROUP_SOCK_ADDR bind()/connect() 系统调用时 重写 socket 地址(ClusterIP → Pod IP 透明转换) 150–400 ns(per connect)
sock_ops SOCK_OPS TCP 状态转换(ESTABLISHED 等) 监控 TCP 连接、attach sk_msg/sk_skb 用于 socket 级转发 100–300 ns(per 状态变)
sk_msg / sk_skb SK_MSG / SK_SKB sendmsg()/recvmsg() 或流级处理 socket 直接转发(local pod-to-pod 绕过 netstack) 150–400 ns
cgroup_skb CGROUP_SKB cgroup 边界包过滤 容器级 ingress/egress 过滤(可选) 100–300 ns

最关键的 kube-proxy 替换机制(Host-Reachable Services / socket LB):

  • 使用 cgroup_sock_addr + sock_ops 程序 attach 到 root cgroup。
  • 进程调用 connect() 到 ClusterIP 时,eBPF 拦截系统调用参数,直接把 dst 改成后端 Pod IP(O(1) hashmap 查找)。
  • 第一包发出时已经是 Pod IP → 后续包走正常路由,无需 per-packet NAT。
  • 对比传统 kube-proxy:connect() 后仍发到 VIP,靠 iptables/IPVS 在 netstack 中 NAT。

实现阶段(对比 kube-proxy/IPVS)

  • kube-proxy iptables:netfilter 阶段(PREROUTING/OUTPUT/POSTROUTING),per-packet 线性规则匹配 + NAT。
  • kube-proxy IPVS:netfilter + IPVS 模块,per-packet hash 查找 + NAT(内核态,但仍走完整 netstack)。
  • Cilium eBPF
    • 连接建立阶段(connect()):cgroup_sock_addr/sock_ops → socket-level 翻译(最早、最省)。
    • 包转发阶段:XDP(最早,北-南)→ tc(入/出)→ socket send/recv(local 直通)。
    • 结果:东-西流量基本绕过 netfilter/netstack;北-南用 XDP 早 drop/forward。

Cilium 把大部分工作前移到 socket/connect 阶段最早的 XDP 阶段,避免了 per-packet 开销。

IPVS 已经是 kube-proxy 的“升级版”(hash 表 + 内核态),比 iptables 好很多,但 Cilium eBPF 仍全面碾压(Isovalent/Plural/Cilium 官方 benchmark 数据):

维度 kube-proxy IPVS Cilium eBPF (kube-proxy replacement) 优势量化(典型 5000+ services / 高并发场景)
负载均衡位置 per-packet(netfilter/IPVS hook) per-connection(socket connect() 时) 减少 90%+ NAT 开销
查找方式 hash 表(内核 IPVS) eBPF hashmap / LRU hash(O(1)) 类似,但 eBPF map 更灵活、更低开销
NAT 开销 每包 SNAT/DNAT connect() 时改 dst,无 per-packet NAT 延迟降 3–5x,CPU 降 50–80%
本地 pod-to-pod 走完整 netstack + lookup socket send/recv 直通(sk_msg/sk_skb) 延迟降至 ~10–20 μs,吞吐升 2–5x
外部流量(NodePort) IPVS + iptables XDP + DSR(Direct Server Return) 吞吐 100+ Gbps vs 40–60 Gbps
规模扩展 规则/表增长 → CPU/延迟线性上升 hashmap 规模无关,Maglev 一致性 hash 10k services 延迟稳定 <50 μs
CPU 使用 高(per-packet 处理 + sync) 低(socket 级 + XDP 早 drop) 50–80% 降低(大规模集群实测)
延迟(p99) 200–500+ μs(高负载) 20–100 μs 3–10x 改善
额外功能 基本 LB L7 策略、加密、observability、DSR、Maglev 功能远超

一句话总结:IPVS 只是把 iptables 的线性匹配换成了 hash,但仍停留在 per-packet 阶段,走完整网络栈。Cilium 把 LB 前移到 socket/connect() + 最早的 XDP,实现 per-connection 翻译 + 零 per-packet NAT,路径最短、开销最低、规模最好。

网络栈图示

Linux 网络协议栈图(Inbound 方向,Ingress 流量示例)

Linux 内核网络协议栈(network stack)是一个分层处理数据包的流水线,从网卡接收包到用户态进程(或反向发送)。一个简化但完整的图示(基于内核 5.x–6.x 主流路径,聚焦 IPv4/TCP 场景),并标注 IPVS(kube-proxy IPVS 模式)和 Cilium eBPF(kube-proxy replacement 模式)的主要拦截/处理点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[物理网卡 / NIC Driver]
      ↓
[ XDP (eXpress Data Path) ]  ← Cilium 最早拦截点(北-南流量、NodePort、DSR、早 drop)
      ↓   (XDP_DROP / XDP_PASS / XDP_TX / XDP_REDIRECT)
[ netif_receive_skb() / GRO / napi_gro_receive() ]
      ↓
[ SoftIRQ / NAPI polling ]
      ↓
[ __netif_receive_skb_core() ]
      ↓
[ tc ingress (Traffic Control classifier / clsact) ]  ← Cilium 主要处理点(策略执行、入方向路由、encap/decap)
      ↓
[ Bridge / veth / lxc 接口处理(如果有桥接或容器 veth) ]
      ↓
[ ip_rcv() / ip_local_deliver() ]   ← IP 层接收
      ↓
[ NF_INET_PRE_ROUTING ]  ← Netfilter PREROUTING 钩子
      ↓
[ IPVS (Netfilter hook + ip_vs_in) ]  ← kube-proxy IPVS 拦截点(ClusterIP/NodePort/Service LB NAT)
      ↓   (如果命中 Service VIP → DNAT 到 Pod IP)
[ ip_forward() 或 ip_local_deliver_finish() ]
      ↓   (本地交付)
[ NF_INET_LOCAL_IN ]
      ↓
[ tcp_v4_rcv() / udp_rcv() ]   ← L4 层(TCP/UDP)
      ↓
[ inet_lookup() / sock lookup ]
      ↓
[ cgroup_sock_addr / sock_ops ]  ← Cilium socket-level LB(connect() 时改 dst,Host-Reachable Services)
      ↓   (Cilium cgroup 钩子拦截系统调用 connect/bind 等,透明改 VIP → Pod IP)
[ tcp_v4_do_rcv() / udp_unicast_rcv_skb() ]
      ↓
[ sk_receive_skb() ]
      ↓
[ Socket Buffer (sk_buff) → 用户态 socket recv() ]
      ↓
[ 用户进程 / Pod ]

出方向(Egress)类似反向流程

  • 用户进程 send() → cgroup_sock_addr/sock_ops(Cilium 可拦截/改地址)
  • → tcp_transmit_skb() → ip_queue_xmit() → NF_INET_LOCAL_OUT / POST_ROUTING
  • → tc egress(Cilium 出方向处理、encap、策略)
  • → XDP(如果 redirect 到其他接口)
  • → 网卡驱动发送

IPVS 和 Cilium 的拦截位置对比标注

  • IPVS(kube-proxy IPVS 模式)
    • 主要拦截在 Netfilter PREROUTING(NF_INET_PRE_ROUTING)钩子 → ip_vs_in()。
    • 处理时机:per-packet(每个包都走一次 hash lookup + NAT)。
    • 位置在 IP 层(ip_rcv 后),包已经进入内核网络栈较深。
    • 为什么在这里?传统 kube-proxy 依赖 iptables/netfilter 框架,IPVS 作为 netfilter target 挂载。
  • Cilium eBPF(kube-proxy replacement)
    • 多点拦截,路径更早、更灵活:
      1. XDP(驱动层最早,网卡收包瞬间):处理北-南外部流量(NodePort、LoadBalancer、早 drop、DSR)。
      2. tc ingress/egress(__netif_receive_skb_core 内):入/出方向包处理、策略执行、路由、encap/decap(VXLAN/Geneve)。
      3. cgroup_sock_addr + sock_ops(socket 系统调用层,connect/bind/sendmsg 时):socket-level LB(Host-Reachable Services),连接建立时一次性改 dst IP/port → 后续包无 NAT。
    • 处理时机:per-connection(连接建立时改地址) + per-packet(只在必要时,如策略/encap)。
    • 位置更早(XDP/tc)或更高层(socket),绕过大量 netfilter/IP 层开销。

性能视角总结

  • IPVS:拦截晚(Netfilter 阶段),每个包都要经过完整 IP/TCP 栈 + NAT + hash lookup → 高负载下 CPU/延迟高(p99 容易 200–500 μs+)。
  • Cilium:拦截早(XDP/tc)+ 高层(socket),实现“零 per-packet NAT” + 早 drop + socket 直通 → 延迟降 3–10x,CPU 降 50–80%,规模到 10k+ Services 仍稳定。

Outbound 方向简化网络协议栈图(Egress 流量示例)

图示基于内核主流路径(5.x–6.x),并标注 IPVS(kube-proxy IPVS 模式)和 Cilium eBPF 的主要拦截/处理点。Cilium 在 Outbound 上更注重 socket-level 改写 + tc egress 策略/encap,而 IPVS 仍依赖 Netfilter POSTROUTING 的 per-packet 处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[ 用户进程 / Pod ]
      ↓   (send()/write() 系统调用 → socket sendmsg/recvmsg)
[ cgroup_sock_addr / sock_ops ]  ← Cilium 主要拦截点(Host-Reachable Services)
      ↓   (Cilium cgroup 钩子拦截 connect/bind/sendmsg 等,透明改 src/dst IP/port 或重定向;socket-level LB/策略)
[ tcp_v4_sendmsg() / udp_sendmsg() ]   ← L4 层发送(TCP/UDP)
      ↓
[ tcp_transmit_skb() / udp_send_skb() ]   ← 构建 sk_buff
      ↓
[ ip_queue_xmit() / ip_append_data() ]   ← L3 层(IP 封装)
      ↓
[ NF_INET_LOCAL_OUT ]  ← Netfilter LOCAL_OUT 钩子
      ↓
[ ip_output() / __ip_finish_output() ]
      ↓
[ NF_INET_POST_ROUTING ]  ← Netfilter POSTROUTING 钩子
      ↓
[ IPVS (Netfilter hook + ip_vs_out) ]  ← kube-proxy IPVS 拦截点(SNAT/Masquerade for external egress,如果 Service 涉及)
      ↓   (如果需要 SNAT → 改 src IP 为 node IP)
[ tc egress (Traffic Control classifier / clsact) ]  ← Cilium 主要处理点(出方向策略执行、encap VXLAN/Geneve、NAT、路由决策)
      ↓   (Cilium tc egress 程序处理包:L3/L4 策略、隧道封装、masquerade 等)
[ Bridge / veth / lxc 接口处理(如果有桥接或容器 veth) ]
      ↓
[ dev_queue_xmit() / __dev_queue_xmit() ]
      ↓
[ SoftIRQ / NAPI polling ]
      ↓
[ 网卡驱动发送队列 ]
      ↓
[ 物理网卡 / NIC Driver ]
      ↓   (可选:XDP tx/redirect,如果 Cilium 用 XDP_TX/REDIRECT 加速本地转发或 hairpin)
[ 物理链路 ]

出方向关键说明

  • 从用户进程到 socket:这是最高层,Cilium 用 cgroup_sock_addr/sock_ops 在系统调用层面拦截/改写(per-connection,一次性)。
  • L4/L3 封装后:包进入 netfilter 和路由路径。
  • tc egress:在 qdisc 前(出接口发送队列前),Cilium 在这里执行出方向策略、隧道封装(overlay 网络常见)、masquerade(如果需要 SNAT 到外部)。
  • XDP:Outbound 上 XDP 不直接可用(XDP 是 ingress-only),但 Cilium 可通过 tc egress + XDP redirect(某些场景)实现类似早路径加速(e.g. hairpin 流量)。

IPVS 和 Cilium 在 Outbound 的拦截位置对比

  • IPVS(kube-proxy IPVS 模式)
    • 主要拦截在 Netfilter POSTROUTING(NF_INET_POST_ROUTING)钩子 → ip_vs_out()。
    • 处理时机:per-packet(每个出包都要 hash lookup + SNAT,如果涉及 external egress 或 masquerade)。
    • 位置在 IP 层较后(ip_output 后),包已封装好,需完整走 netfilter + NAT。
    • 典型场景:Pod 访问外部 Service 或 Cluster-external 时,IPVS 处理 SNAT(改 src 为 node IP)。
  • Cilium eBPF(kube-proxy replacement)
    • 多点拦截,路径更早、更高效:
      1. cgroup_sock_addr + sock_ops(socket 系统调用层,sendmsg/connect 时):socket-level 处理(改地址、重定向、L7 策略预判),连接建立/发送时一次性完成,后续包几乎无额外开销。
      2. tc egress(dev_queue_xmit 前):出方向包处理、L3/L4 策略、隧道封装(VXLAN/Geneve)、masquerade(如果需要 SNAT)。
    • 处理时机:per-connection(socket 层) + per-packet(只在必要时,如策略/encap)。
    • 位置更高层(socket)+ 中间层(tc egress),绕过大量 netfilter 开销,实现“零 per-packet NAT”或最小化 NAT。

性能视角总结(Outbound )

  • IPVS:per-packet SNAT/NAT + Netfilter 遍历 → 高负载下 CPU/延迟高(尤其大规模集群,p99 容易 200–500 μs+)。
  • Cilium:socket 层改写(per-connection)+ tc egress 封装/策略 → 延迟降 3–10x,CPU 降 50–80%。本地 Pod-to-external 或 overlay 隧道时,tc egress 的 encap 开销远低于传统 iptables/IPVS。
  • 实际差距:在高并发 Egress(如 Pod 访问外部 DB/Service),Cilium 的 socket LB + tc 路径能把 Egress 延迟稳定在 20–100 μs,而 IPVS 容易翻倍。

eBPF 性能分析完整流程

从定义 SDT(Statically Defined Tracepoints)开始的完整端到端流程。应用是 C++ 开发的(如一个处理请求的服务器),目标是追踪函数延迟分布,并将 metrics 暴露给 Prometheus。

使用 libbpf(内核官方库)和 BPF skeleton(自动生成接口,简化 C++ 代码)。这避免了 BCC 的 Python 依赖,确保纯 C++ 栈、高效、CO-RE(跨内核兼容)。

环境:系统是 Linux 内核 4.17+,安装了 libbpf(via apt install libbpf-dev 或类似)、clang 和 bpftool。

1. 定义 SDT(在应用中设置静态追踪点)

SDT 是用户态静态定义的追踪点,编译时嵌入应用二进制中。零运行时开销,只有被使能时才执行。适合精确追踪自定义事件,如函数延迟。

  • 步骤:

    在 C++ 应用代码中,使用 <sys/sdt.h> 头文件定义探针。探针格式:DTRACE_PROBE(provider, probe_name[, args…])。

    示例:假设你的应用有一个函数 process_request(),我们定义入口/出口探针来追踪延迟。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    // 应用源代码
    #include <sys/sdt.h>  // SDT 头文件,通常在 /usr/include/sys/sdt.h
      
    void process_request(int arg) {
        DTRACE_PROBE1(myapp, request_start, arg);  // 入口探针,带参数
        // ... 业务逻辑(如处理请求)...
        DTRACE_PROBE(myapp, request_end);          // 出口探针,无参数
    }
      
    int main() {
        // ... 应用启动逻辑 ...
        while (true) {
            process_request(42);
        }
        return 0;
    }
    

    myapp 是 provider(提供者)名称,自定义。

    request_start 和 request_end 是 probe(探针)名称。

    可以带 0~8 个参数(int、string 等),用于传递上下文。

  • 编译应用:

    1
    
    g++ -O2 -g your_app.cpp -o your_app  # -g 保留调试信息,用于 SDT
    

    验证 SDT:用 readelf -n your_app 检查 ELF 文件的 .note.stapsdt 节,应看到你的探针定义。 示例输出:

    1
    2
    3
    4
    5
    6
    7
    
    Displaying notes found in: .note.stapsdt
      Owner                 Data size Description
      stapsdt              0x000000XX NT_STAPSDT (SystemTap probe descriptors)
        Provider: myapp
        Name: request_start
        Location: 0x0000XXXX, Base: 0x0000YYYY, Semaphore: 0x0000ZZZZ
        Arguments: -4@%edi
    
  • 性能提示: SDT 嵌入后,应用性能无影响(<0.1% overhead)。在热点函数中使用,避免过多参数以减少栈开销。适用于生产环境,无需重启应用。

2. 编写 eBPF 内核程序(捕获 SDT 事件并计算延迟)

eBPF 程序运行在内核,附加到 SDT 上。使用 BPF maps 存储时间戳和 histogram(延迟分布)。

  • 步骤:

    生成 vmlinux.h(CO-RE 支持):

    1
    
    bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
    

    创建文件 latency_tracer.bpf.c(eBPF C 代码)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    
    #include "vmlinux.h"
    #include <model.h>
    #include <bpf/bpf_helpers.h>       // 辅助函数
    #include <bpf/bpf_tracing.h>       // tracepoint / kprobe 相关宏
    #include <bpf/bpf_core_read.h>     // CO-RE 读取宏
      
    #define RINGBUF_SIZE_MB 16
    #define RINGBUF_BYTES (RINGBUF_SIZE_MB * 1024 * 1024)
    // ── 1. 定义 map ────────────────────────────────────────────────
    struct {
        __uint(type, BPF_MAP_TYPE_HASH);
        __type(key, u32);               // 例如 pid 或 tid
        __type(value, u64);             // 例如时间戳(ns)
        __uint(max_entries, 1 << 14);   // 16384
    } start SEC(".maps");
      
    struct {
        __uint(type, BPF_MAP_TYPE_RINGBUF);
        __uint(max_entries, RINGBUF_BYTES);
    } events SEC(".maps");
      
    // ── 2. 程序主体(btf 写法)─────────────────────────────
    SEC("tp_btf/sched_switch")
    int BPF_PROG(sched_switch,
                 [[maybe_unused]]bool preempt,     // 第一个参数:是否抢占
                 struct task_struct *prev,
                 struct task_struct *next)
    {
        u32 prev_pid = BPF_CORE_READ(prev, pid);
        u32 next_pid = BPF_CORE_READ(next, pid);
        if (prev_pid == next_pid) {
            return 0;
        }
        u64 ts = bpf_ktime_get_ns();
        // 记录上一个任务被切走的时间
        bpf_map_update_elem(&start, &prev_pid, &ts, BPF_ANY);
      
        // 读取当前任务的开始时间,计算等待时间
        u64 *tsp = bpf_map_lookup_elem(&start, &next_pid);
        if (!tsp)
            return 0;
        u64 delta = ts - *tsp;
        if (delta < 1000)           // 过滤太小的抖动(可选)
            return 0;
      
        // 准备事件
        struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
        if (!e) {
            // 这里可以加 dropped 计数器,或者直接返回
            return 0;  // 或者其他处理
        }
        e->pid = next_pid;
        e->cpu  = bpf_get_smp_processor_id();
        e->delta_ns = delta;
      
        // 提交到用户态(ring buffer 方式最快)
        bpf_ringbuf_submit(e, 0);
      
        return 0;
    }
    // ── 4. license(必须) ──────────────────────────────────────────────
    char LICENSE[] SEC("license") = "GPL";
    

    model.h文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    #ifndef __INCLUDE_MODEL__
    #define __INCLUDE_MODEL__
      
    struct event {
        unsigned int pid;
        unsigned int cpu;
        long long unsigned int delta_ns;
        // char     comm[16];           // 可扩展
        // u32      func_id;
        // u64      latency_us;
    };
      
    #endif /* __INCLUDE_MODEL__ */
    

    编译 eBPF:

    1
    
    clang -O2 -g -target bpf -c latency_tracer.bpf.c -o latency_tracer.bpf.o
    
  • 性能提示: 使用 hash map 高效存储(O(1) 访问)。Histogram 用 log2 桶覆盖宽范围(1ns ~ 秒级)。限制 max_entries 避免内存膨胀。

3. 生成 BPF Skeleton 并使能 SDT(用户态 C++ 加载与附加)

Skeleton 自动生成 C++ 接口,简化加载/attach/map 操作。使能 SDT 即附加 eBPF 程序到应用的探针上。

  • 步骤:

    生成 skeleton:

    1
    
    bpftool gen skeleton latency_tracer.bpf.o > latency_tracer.skel.h
    

    创建用户态 C++ 文件 tracer.cpp(daemon 或工具)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    
    // tracer.cpp: 用户态程序,用于加载 字节码 并读取延迟事件
    #include <atomic>
    #include <model.h>
    #include <cstring>
    #include <iostream>
    #include <csignal>      // signal
    #include <unistd.h>     // getopt, sleep
    #include <fcntl.h>      // open
    #include <sys/resource.h>  // setrlimit
    #include <bpf/libbpf.h>    // libbpf API
    #include "latency_tracer.skel.h"  // BPF skeleton 头文件(bpftool gen 生成)
    #include <sys/epoll.h>
    using namespace std::chrono_literals;
      
    // 全局退出标志
    static std::atomic<bool> exiting {false};
      
    // 信号处理函数(Ctrl+C 退出)
    static void sig_handler([[maybe_unused]]int sig) {
        _exit(0);
    }
      
    // 提升 RLIMIT_MEMLOCK(BPF map 经常需要无限内存锁)
    static int bump_memlock_rlimit() {
        struct rlimit rlim_new = {
            .rlim_cur = RLIM_INFINITY,
            .rlim_max = RLIM_INFINITY,
        };
        return setrlimit(RLIMIT_MEMLOCK, &rlim_new);
    }
      
    // 必须和 libbpf 期望的 perf_buffer_sample_fn 完全一致
    static int handle_event([[maybe_unused]] void *ctx, void *data, unsigned long size)
    {
        const struct event *e = (const struct event *)data;
      
        if (size != sizeof(*e)) {
            fprintf(stderr, "Payload size mismatch: got %lu, expected %zu\n",
                    size, sizeof(*e));
            return 0;
        }
      
        // 输出(cpu 是 perf buffer 自动传的当前 CPU 号,不是事件里的 cpu)
        std::cout << "[LATENCY] PID=" << e->pid
                  << " CPU=" << e->cpu                  // ← 这里能拿到事件发生在哪个 CPU
                  << " latency=" 
                //   << e->delta_ns << "ns ≈ " 
                //   << (e->delta_ns / 1000.0) << "us ≈ " 
                  << (e->delta_ns / 1000000.0) << "ms"<< std::endl;
      
        return 0;
    }
      
    latency_tracer* Get_latency_tracer()
    {
        struct latency_tracer *skel = nullptr;
        // 打开 BPF skeleton
        skel = latency_tracer__open();
        if (!skel) {
            std::cerr << "Failed to open BPF skeleton\n";
            return nullptr;
        }
      
        // 加载并验证 BPF 程序
        int err = latency_tracer__load(skel);
        if (err) {
            std::cerr << "Failed to load BPF object: " << err << "\n";
            return nullptr;
        }
      
        // 附加探针(kprobe/kretprobe)
        err = latency_tracer__attach(skel);
        if (err) {
            std::cerr << "Failed to attach BPF programs: " << err << "\n";
            return nullptr;
        }
      
        std::cout << "Latency tracer started. Press Ctrl+C to exit.\n";
        return skel;
    }
      
    int main() {
        struct ring_buffer *rb = nullptr;
        int err = bump_memlock_rlimit();
      
        // 提升内存锁限制
        if (err) {
            std::cerr << "Failed to increase RLIMIT_MEMLOCK\n" << strerror(err) << std::endl;
            return 1;
        }
      
        // 注册信号
        signal(SIGINT, sig_handler);
        signal(SIGTERM, sig_handler);
      
        auto skel_latency_tracer = Get_latency_tracer();
        // 设置 perf buffer(假设 map 名叫 events)
        rb = ring_buffer__new(
                bpf_map__fd(skel_latency_tracer->maps.events),   // map fd
                handle_event,                     // sample callback(现在匹配)
                nullptr,                          // ctx(上下文指针,可传自定义结构体)
                nullptr                           // opts(高级选项,通常 nullptr)
            );
        if (libbpf_get_error(rb)) {
            std::cerr << "Failed to create perf buffer\n";
            return 0;
        }
        int wakeup_fd = ring_buffer__epoll_fd(rb);
        fcntl(wakeup_fd, F_SETFL, O_NONBLOCK);
        int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
        struct epoll_event ev = {
            EPOLLIN | EPOLLET,          // .events
            { .ptr = (void *)rb }       // .data 是 union,指定 .ptr
        };
        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, wakeup_fd, &ev);
      
        while (!exiting) {
            struct epoll_event events[8];
            int nfds = epoll_wait(epoll_fd, events, 8, -1);  // 永久等待
      
            if (nfds > 0) {
                for (int i = 0; i < nfds; i++) {
                    if (events[i].data.ptr == rb) {
                        // 数据来了,一次性消费干净
                        ring_buffer__consume(rb);
                    }
                }
            }
        }
      
        ring_buffer__free(rb);
        latency_tracer__destroy(skel_latency_tracer);
        return err < 0 ? 1 : 0;
    }
    

    编译:

    1
    
    g++ -O2 tracer.cpp -lbpf -lelf -o tracer
    

    运行:先启动应用,获取 PID,然后 sudo ./tracer (需要 root 或 CAP_BPF)。

  • 性能提示: Attach 后,eBPF overhead <1% CPU。使用 PID filter 只追踪目标进程,避免全局噪声。

4. 监测延迟(用户态读取 map 并分析)

在用户态 polling 或用 ring buffer 读取 histogram,计算 P50/P99 等。

  • 步骤: 在 tracer.cpp 的 main 中添加循环读取。

  • 性能提示: 用 epoll + ring buffer 代替 polling,减少 CPU 使用。分析长尾延迟,识别瓶颈(如 I/O 等待)。

5. Prometheus 暴露 Metrics(集成 exporter)

将 histogram 转为 Prometheus histogram metric,暴露 HTTP endpoint。

  • 步骤: 用 prometheus-cpp 库(安装 via apt install libprometheus-cpp-dev 或类似)。在 tracer.cpp 添加:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    #include <prometheus/exposer.h>
    #include <prometheus/registry.h>
    #include <prometheus/histogram.h>
      
    // 在 main 中...
    auto registry = std::make_shared<prometheus::Registry>();
    auto& latency_histogram = prometheus::BuildHistogram()
        .Name("request_latency_ns")
        .Help("Request latency distribution")
        .Register(*registry)
        .Add({/* labels */}, prometheus::Histogram::BucketBoundaries{1, 2, 4, 8, /* ... log2 桶 */});
      
    prometheus::Exposer exposer{"127.0.0.1:9100"};
      
    // 在循环中...
    while (true) {
        // 清空并更新 histogram
        // 从 skel->maps.latency_hist 读取所有桶,observe 到 latency_histogram
        for (u64 bucket = 0; bucket < 64; ++bucket) {
            u64 *count;
            if (bpf_map_lookup_elem(bpf_map__fd(skel->maps.latency_hist), &bucket, &count) == 0 && *count > 0) {
                double value = 1ULL << bucket;  // 中间值
                for (u64 i = 0; i < *count; ++i) {
                    latency_histogram.Observe(value);
                }
                // 可选:清空 map 以重置
                u64 zero = 0;
                bpf_map_update_elem(bpf_map__fd(skel->maps.latency_hist), &bucket, &zero, BPF_ANY);
            }
        }
        sleep(5);
    }
    
    • 配置 Prometheus scrape:添加 job 指向 :9100/metrics。
  • 性能提示: 暴露时用 labels(如 per-thread)丰富 metrics。避免高频 observe,控制 cardinality <10k。

总结

流程从 SDT 定义到 metrics 暴露,形成闭环:应用嵌入探针 → eBPF 捕获/计算 → 用户态分析/暴露。总 overhead <2%,适合实时系统。生产中,用 CMake 集成 skeleton 生成;监控 OOM(map 大小);测试跨内核(CO-RE)。

image-20260305220216193

架构层次

1. 典型架构:两个独立进程

  • 被追踪应用(your_app): 嵌入 SDT/USDT 探针的那个 C++ 业务进程。 运行正常业务逻辑,探针点只是 NOP(未附加时)或 int3 陷阱(附加后)。
  • eBPF 用户态程序(tracer / daemon / exporter): 这是一个独立的 C++ 进程(用 libbpf + skeleton 实现的那个 main.cpp 或 daemon)。 职责是:
    • 加载 eBPF .o 文件
    • attach 到目标进程的 USDT(通过 PID 指定)
    • 读取 BPF map(hash、histogram、ringbuf 等)
    • 聚合/分析延迟分布
    • 暴露 Prometheus /metrics endpoint
    • 可选:周期性打印、写日志、推送到外部系统

这两个进程是完全分开的

  • PID 不同
  • 地址空间不同
  • 资源竞争主要体现在 CPU/内存/上下文切换上,而不是直接共享进程上下文