Linux eBPF 工业边缘性能观测实战

工业边缘设备的问题往往发生在现场:进程突然重启、块设备延迟抖动、网络重传升高、CPU 被某个采集任务吃满、容器里的服务偶发卡顿。传统排障依赖日志、top、iostat 和 tcpdump,采样粒度粗,也难以回答"当时内核里发生了什么"。

eBPF 的价值在于:在内核事件发生的位置采集和聚合数据,再把有限的结果交给用户态。它适合做低侵入的动态观测,但不是魔法探针。生产使用前仍要确认内核能力、权限模型、程序复杂度、输出频率和观测组件自身的资源开销。

本文从能力边界、工具选择、现场排障、代码实现和生产化控制五个层面展开。

一、eBPF 能解决什么问题

1. 基本工作方式

eBPF 程序挂载到内核 hook 上,在事件发生时执行:

text 复制代码
内核事件
  ├─ 系统调用 tracepoint
  ├─ 调度 / 块设备 / 网络事件
  ├─ kprobe / kretprobe
  └─ cgroup / socket / 网卡事件
        ↓
   eBPF 程序
        ↓
   maps / ring buffer / perf buffer
        ↓
   用户态采集与导出

程序加载前由内核 verifier 检查:

  • 内存访问是否安全;
  • 指针是否有效;
  • 循环是否有界;
  • 栈和 helper 调用是否超出限制;
  • 该程序类型能否调用对应 helper。

通过检查后由 JIT 编译执行。verifier 保证的是内核内存安全和程序可终止,不保证业务语义正确,也不代表观测本身没有开销。

2. 适合的观测目标

目标 典型信号 适合的 hook
进程异常 exec / exit / 进程参数 syscall tracepoint
文件访问 openat / close / 读写延迟 syscall、VFS 路径
块设备延迟 IO issue 到 complete 的耗时 block tracepoint
调度延迟 进程等待 CPU 的时间 sched tracepoint
网络质量 重传、连接失败、往返时延 TCP tracepoint
CPU 热点 on-CPU / off-CPU 栈 perf event
容器资源 cgroup CPU / 内存 / IO cgroup、tracepoint
用户态函数耗时 指定函数 entry / return uprobe

3. 不适合当成什么用

eBPF 不适合替代:

  • 访问控制和权限体系;
  • 完整安全审计与入侵防御系统;
  • 应用内部的业务调用链;
  • 时间序列数据库和告警系统;
  • 结构化的应用日志。

它更适合作为内核级行为观测的数据源。安全场景可以使用 eBPF 辅助发现异常,但告警、取证和处置仍需要完整流程。

二、现场前置检查

1. 内核与 BTF

先确认内核版本和 BTF:

bash 复制代码
uname -r
ls -lh /sys/kernel/btf/vmlinux
bpftool feature probe

几点判断:

  • 较新的发行版 LTS 内核通常更适合 CO-RE;
  • 内核版本旧不代表完全不能用,但可用 hook、helper 和 map 类型可能缺失;
  • BTF 存在时,libbpf 可以做 CO-RE,减少对具体内核结构的依赖;
  • kprobe 绑定的是内核函数符号,函数名和参数可能随内核变化;
  • syscall tracepoint 通常比内核内部函数 kprobe 更适合做长期观测;
  • 最终以 bpftool feature probe 和目标板实测为准。

2. 权限

加载 eBPF 程序是特权操作。常见方式:

  • 调试阶段直接使用 root;
  • 生产环境优先使用最小 capability;
  • 根据程序类型准备 CAP_BPFCAP_PERFMONCAP_NET_ADMIN
  • 旧内核可能还涉及 CAP_SYS_RESOURCE 和 memlock 限制;
  • LSM、安全启动、内核 lockdown 策略可能进一步限制加载。

可用下面命令查看当前限制:

bash 复制代码
capsh --print
ulimit -l
cat /proc/sys/kernel/unprivileged_bpf_disabled

生产代理不要长期以 root 运行。更稳妥的做法是由安装阶段的特权任务完成加载,运行期降低权限,并通过审计日志记录加载和卸载事件。

3. 观测组件自身的资源

工业边缘设备资源有限,观测系统本身要有预算:

  • 代理进程 CPU / 内存上限;
  • map 数量和容量;
  • ring buffer 大小;
  • 本地落盘速率;
  • 上传带宽;
  • flash 写入寿命;
  • 断网时的缓冲策略。

如果观测代理把 CPU 或磁盘打满,它就从诊断工具变成了故障源。

三、工具链怎么选

工具 定位 优点 注意点
bpftrace 一次性排障脚本 表达能力强,适合现场验证 高频输出要控制;版本和 tracepoint 名随内核变化
BCC 工具集和 Python 快速开发 生态成熟,自带很多观测工具 运行时编译依赖较重,嵌入式环境要裁剪
libbpf 生产 C 程序 CO-RE、预编译、体积可控 需要构建链、BTF 和更完整的工程化
libbpf-rs Rust 工程集成 类型安全,便于打包部署 需要 Rust 工具链和 ABI 管理
bpftool 检查和调试 查看 map、prog、链接、feature 主要面向诊断,不是业务运行时

选择建议:

  1. 现场临时排障:先用 bpftrace;
  2. 通用指标:优先使用发行版提供的 BCC 工具;
  3. 长期部署:使用 libbpf / libbpf-rs 做预编译 CO-RE 程序;
  4. 嵌入式镜像:避免在设备上安装完整编译环境;
  5. 多内核批量部署:建立内核能力矩阵和兼容性测试。

四、bpftrace 现场实战

1. 查看可用事件

先确认事件名,避免拿网上脚本直接套:

bash 复制代码
bpftrace -l 'tracepoint:syscalls:*openat*'
bpftrace -l 'tracepoint:sched:*'
bpftrace -l 'tracepoint:block:*'
bpftrace -l 'tracepoint:tcp:*'

2. 观察进程启动

bash 复制代码
bpftrace -e 'tracepoint:syscalls:sys_enter_execve {
    printf("%lld %s %s\n", pid, comm, str(args->filename));
}'

适合排查未知脚本、异常子进程和周期任务。

3. 统计进程切换

bash 复制代码
bpftrace -e 'tracepoint:sched:sched_switch {
    @[args->prev_comm, args->next_comm] = count();
}'

Ctrl+C 结束时会输出聚合结果。适合快速判断哪些任务在频繁切换。

4. 观察 VFS 读延迟分布

bash 复制代码
bpftrace -e 'kprobe:vfs_read {
    @start[tid] = nsecs;
}

kretprobe:vfs_read /@start[tid]/ {
    @read_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

这个例子用了 kprobe / kretprobe,适合临时诊断。由于它绑定内核内部函数,不同内核版本可能需要调整;长期部署优先使用稳定 tracepoint 或 block 层事件。

5. 观察 TCP 重传

bash 复制代码
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb {
    @[comm, pid] = count();
}'

如果重传集中在采集进程,可能是上传链路抖动、缓冲区设置不合理或代理重试策略过于激进。

6. 常用现成工具

bash 复制代码
biolatency     # 块设备延迟分布
execsnoop      # 新进程
opensnoop      # 文件打开
tcpconnect     # TCP 主动连接
runqlat        # 调度等待延迟
biosnoop       # 单次块 IO 详情
tcptop         # TCP 收发统计

这些工具输出细节可能因版本不同而有差异。生产环境中不要长期运行 *snoop 这类逐事件输出工具,优先使用延迟聚合类工具。

五、BCC Python 示例

下面统计每个进程的 openat 次数:

python 复制代码
import time

from bcc import BPF


program = r"""
#include <uapi/linux/ptrace.h>

BPF_HASH(counts, u32, u64);

int trace_open(struct pt_regs *ctx)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 zero = 0;
    u64 *count;

    count = counts.lookup_or_try_init(&pid, &zero);
    if (count) {
        __sync_fetch_and_add(count, 1);
    }

    return 0;
}
"""


bpf = BPF(text=program)
bpf.attach_kprobe(
    event=bpf.get_syscall_fnname("openat"),
    fn_name="trace_open",
)

try:
    while True:
        time.sleep(5)
        print("=" * 30)
        for key, value in bpf["counts"].items():
            print(f"pid={key.value} openat={value.value}")
except KeyboardInterrupt:
    pass

这个例子适合理解 map 的更新方式。BCC 的优势是开发快;缺点是运行时编译需要内核头文件和 LLVM 相关依赖,交叉编译、镜像体积和启动时间都要评估。

六、libbpf CO-RE 示例

生产部署更推荐 libbpf:程序预先编译成对象,运行时由 libbpf 根据目标内核 BTF 做 CO-RE 重定位。

1. eBPF 侧

c 复制代码
// trace.bpf.c
#include "vmlinux.h"

#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>


struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, u32);
    __type(value, u64);
    __uint(max_entries, 10240);
} open_counts SEC(".maps");


SEC("tp/syscalls/sys_enter_openat")
int count_openat(struct trace_event_raw_sys_enter *ctx)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 one = 1;
    u64 *count;

    count = bpf_map_lookup_elem(&open_counts, &pid);
    if (count) {
        __sync_fetch_and_add(count, 1);
    } else {
        bpf_map_update_elem(&open_counts, &pid, &one, BPF_ANY);
    }

    return 0;
}


char LICENSE[] SEC("license") = "GPL";

这里使用 syscall tracepoint,比绑定 sys_openat 内核函数更适合跨内核部署。

2. 用户态加载器

c 复制代码
// trace.c
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

#include <bpf/libbpf.h>

#include "trace.skel.h"


static volatile sig_atomic_t stop;


static void handle_signal(int sig)
{
    stop = 1;
}


static void dump_map(struct trace_bpf *skel)
{
    int fd = bpf_map__fd(skel->maps.open_counts);
    __u32 key = -1;
    __u32 next_key;
    __u64 value;

    while (bpf_map_get_next_key(fd, &key, &next_key) == 0) {
        if (bpf_map_lookup_elem(fd, &next_key, &value) == 0) {
            printf("pid=%u openat=%llu\n", next_key, value);
        }
        key = next_key;
    }
}


int main(void)
{
    struct trace_bpf *skel;
    int err;

    signal(SIGINT, handle_signal);
    signal(SIGTERM, handle_signal);
    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);

    skel = trace_bpf__open_and_load();
    if (!skel) {
        fprintf(stderr, "failed to open and load BPF object\n");
        return 1;
    }

    err = trace_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "failed to attach BPF program: %d\n", err);
        trace_bpf__destroy(skel);
        return 1;
    }

    while (!stop) {
        sleep(5);
        dump_map(skel);
    }

    trace_bpf__destroy(skel);
    return 0;
}

3. 构建流程

以 x86_64 为例:

bash 复制代码
clang --target=bpf -D__TARGET_ARCH_x86 \
  -O2 -g -c trace.bpf.c -o trace.bpf.o

bpftool gen skeleton trace.bpf.o > trace.skel.h

cc -O2 -g -Wall trace.c -o trace -lbpf -lelf -lz

交叉编译时把 __TARGET_ARCH_x86 换成目标架构,例如 ARM64 对应 __TARGET_ARCH_arm64。生成 vmlinux.h、选择内核 BTF、静态链接和最小根文件系统裁剪都应纳入构建流水线。

七、工业边缘常见观测方案

1. 块设备与存储抖动

关注指标:

  • IO 延迟 P95 / P99;
  • 队列深度;
  • 每秒读写字节;
  • 写放大;
  • flash 写入速率。

适合判断数据落盘、日志刷写和数据库提交是否影响实时任务。

2. 调度与 CPU

关注指标:

  • 运行队列延迟;
  • on-CPU 热点;
  • off-CPU 等待;
  • 优先级反转;
  • 采集任务周期抖动。

适合排查协议轮询、压缩、解析和上传任务对实时性的影响。

3. 网络与上传链路

关注指标:

  • TCP 重传;
  • 连接失败;
  • 连接耗时;
  • 收发队列;
  • 网卡丢包。

适合判断断线重连、云端接口延迟和本地队列积压之间的关系。

4. 进程与文件行为

关注指标:

  • 异常 exec;
  • 频繁打开配置或日志文件;
  • 关键文件读写;
  • 僵尸进程;
  • 进程退出原因。

适合辅助定位脚本异常、误配置、日志轮转失控和供应链问题。

5. 容器与 cgroup

容器场景要把内核事件和 cgroup / namespace 关联起来:

  • cgroup 级 CPU 与内存;
  • 容器内进程映射;
  • IO 归属;
  • 网络命名空间;
  • 容器重启前后的行为。

只看容器引擎自身的指标,往往无法解释内核级抖动。

八、输出与开销控制

eBPF 本身有较低开销,但"每个事件都输出到用户态"会明显放大成本。生产方案要区分三类数据:

类型 例子 建议
聚合指标 计数、直方图、P95 内核态聚合,周期性导出
事件样本 异常 exec、慢 IO 限流、采样、保留上下文
全量原始流 每个 syscall、每条包 只用于短时调试,不长期开启

降载策略

  1. 在 eBPF 内先过滤 PID、cgroup、挂载点和方向;
  2. 使用 histogram / count / log2 聚合,避免逐条输出;
  3. ring buffer 设置丢弃策略,不阻塞业务路径;
  4. 用户态批量消费;
  5. 异常事件设置每分钟上限;
  6. 栈采集按需开启;
  7. map 设置 max_entries,控制基数;
  8. 周期任务错峰执行;
  9. 输出数据先落内存或队列,再异步上传;
  10. 断网时限制本地磁盘保留量。

必须监控观测组件自身

至少记录:

  • eBPF 代理 CPU / 内存;
  • map 使用率;
  • ring buffer 丢弃次数;
  • 程序加载失败原因;
  • verifier 拒绝日志;
  • 采集任务耗时;
  • 本地队列长度;
  • 上传失败和重试次数。

上线前做 A/B 对比:开启观测前后分别记录业务延迟、CPU、上下文切换、IO 和网络指标,确认影响在预算内。

九、权限与部署策略

1. 分阶段部署

text 复制代码
开发环境验证
  ↓
目标内核矩阵测试
  ↓
单站点灰度
  ↓
只读观测
  ↓
设置资源上限
  ↓
批量启用

2. 安装期与运行期分离

安装期可以提升权限完成:

  • 检查内核能力;
  • 加载 eBPF 程序;
  • 创建 pin map;
  • 配置 capability;
  • 写入审计记录。

运行期只需要读取 map、消费事件和导出指标,尽量降低权限。

3. 升级与回滚

  • eBPF 对象记录版本号;
  • 保留内核与程序版本兼容矩阵;
  • 升级失败自动卸载新程序;
  • map schema 变更要有迁移策略;
  • 长期 pinned map 要纳入清理机制;
  • 卸载后确认 hook 和 map 已释放。

十、常见坑与修正

问题 原因 修正
程序在开发机可用,现场加载失败 内核版本、BTF、helper 或安全策略不同 建立内核矩阵,使用 bpftool feature probe
kprobe 脚本跨内核失效 内核函数名和参数变化 长期观测优先 tracepoint,kprobe 只做临时诊断
CPU 开销突然升高 逐事件输出、高频栈采集、map 基数过大 聚合、过滤、限流、设置容量
磁盘被观测数据写满 全量日志落盘 样本化、限额、轮转和断网保护
权限不足 capability、LSM、lockdown 限制 明确程序类型所需权限并纳入部署检查
verifier 拒绝循环 循环上界无法证明 使用有界循环和更小的数据结构
数据缺失 ring buffer 溢出或事件被过滤 记录丢弃次数并调整预算
安全误判 把 eBPF 当完整审计系统 结合进程上下文、资产、策略和人工复核

十一、生产落地检查清单

  • 已确认内核版本、BTF、可用 hook 和 helper;
  • 已区分调试脚本和长期采集程序;
  • 长期程序优先使用 tracepoint 和 CO-RE;
  • 已交叉编译并验证目标架构;
  • 已在测试环境记录 verifier 错误场景;
  • 已配置最小权限,不以 root 长期运行代理;
  • 已设置 map 容量和 ring buffer 大小;
  • 高频事件已聚合或采样;
  • 异常事件有输出上限;
  • 断网时本地缓存有磁盘上限;
  • 观测代理自身 CPU / 内存 / 队列有监控;
  • 加载、卸载、失败和升级有审计日志;
  • 已做灰度、回滚和内核兼容测试;
  • 已确认观测开启前后的业务性能差异。

TL;DR

eBPF 适合工业边缘的内核级动态观测:不重启进程、不改业务代码,就能采集系统调用、调度、块 IO、网络和 cgroup 行为。

关键工程结论:

  • bpftrace 适合现场临时诊断,BCC 适合快速开发和通用工具,libbpf / libbpf-rs 更适合长期部署;
  • CO-RE 依赖 BTF,最终以目标内核能力探测为准;
  • 长期观测优先稳定 tracepoint,慎用内核内部 kprobe;
  • eBPF 不是零开销,必须在内核态聚合、过滤和限流;
  • 权限需要最小化,加载失败可能来自 capability、LSM 或 lockdown;
  • 观测组件自身也必须被监控;
  • 上线前做内核矩阵、资源预算、灰度和回滚方案。

用 eBPF 看见内核事件只是第一步,把它变成资源可控、权限清晰、可回滚的观测能力,才是工业边缘生产化的重点。

相关推荐
烂蜻蜓1 小时前
Flask入门教程(二十七):Session Interface API——自定义Session存储后端
网络·ios·flask
skywalk81631 小时前
通过下载Ubuntu ova文件在Virtual Box快速安装ubuntu
linux·运维·ubuntu·virtualbox
wuminyu1 小时前
JVM锁膨胀与Futex源码解析
java·linux·c语言·jvm·c++
happymade1 小时前
7000 台网络设备批量纳管 & 自动拓扑生成实战——MSRM3 完整操作流程与关键要点复盘
运维·服务器·网络·zabbix·grafana·msrm3
啊阿狸不会拉杆1 小时前
《计算机网络-自顶向下方法》4.1 网络层概述 读书笔记
网络·计算机网络·智能路由器·网络层
斑马1391 小时前
Linux软件编程学习笔记(十一)——HTTP协议
linux·笔记·学习
OpenPomeloxCommunity1 小时前
Linux驱动基础(一):模块机制的设计与实现
linux
Shadow(⊙o⊙)2 小时前
Linux网络——文件与网络的连接桥梁struct sock {
linux·运维·网络
李永奉2 小时前
中科蓝讯SDK开发-提示音音频文件格式转换和压缩教程
网络·单片机·嵌入式硬件·mcu·物联网