如何使用 eBPF 监控容器中的系统调用?

来源:C++教程作者:广州GEO公司头衔:草根站长
导读:本期聚焦于广州GEO公司创作的《如何使用 eBPF 监控容器中的系统调用?》,敬请观看详情。传统容器监控方案多依赖内核审计模块或用户态追踪工具,往往难以兼顾性能与细粒度。eBPF 作为一项内核可编程技术,允许在无需修改源码的情况下,安全高效地挂载探针到系统调用路径。它利用内核中的虚拟机执行沙箱化字节码,结合映射表实现内核与用户态数据交换。针对容器场景,通过 cgroup 标识与命名空间信息,可以精确区分不同 Pod 或容器的 syscall 行为。相比 ptrace 带来的上下文切换开销,eBPF 几乎无侵入,适合生产环境持续观测文件访问、网络连接等异常行为。

在云原生环境中,容器以轻量隔离的方式运行大量微服务,但系统调用作为应用与内核交互的底层入口,常常成为故障排查与安全审计的关键线索。eBPF 提供了一套位于内核态的观测能力,使我们能够在不修改容器镜像、不重启进程的前提下,实时捕获特定容器内发起的系统调用类型、参数与返回值。

如何使用 eBPF 监控容器中的系统调用?

eBPF 监控系统调用的底层原理

eBPF 全称 extended Berkeley Packet Filter,最初用于网络包过滤,如今已演变为通用的内核可编程框架。其核心机制是在内核中运行经过验证的字节码程序,这些程序挂载在预设的钩子点(hook point)上,例如 raw_syscalls:sys_enterraw_syscalls:sys_exit 这两个 tracepoint。当任意进程发起系统调用时,内核会自动触发对应 eBPF 程序,程序可读取当前任务的 pidtgid、用户态寄存器以及指向内核栈的上下文。

为了将采集到的数据传回用户空间,eBPF 引入了 BPF_MAP_TYPE_PERF_EVENT_ARRAYBPF_MAP_TYPE_HASH 等映射结构。内核态程序把 syscall 号、时间戳、容器 ID 写入映射,用户态守护进程通过 perf_buffer__pollbpf_map_lookup_elem 读取。由于 eBPF 程序在加载前需通过内核验证器检查,不能出现死循环或越界访问,因此比直接编写内核模块更加安全,也不会导致系统崩溃。

在容器场景下,每个进程都属于某个 PID 命名空间与控制组(cgroup)。eBPF 程序可通过 bpf_get_current_cgroup_id 辅助函数获取当前进程的 cgroup v2 ID,而 Kubernetes 通常将 Pod 内的所有容器归在同一个 cgroup 目录下。配合 /proc//cgroup 文件解析,就能将原始 syscall 事件关联到具体的容器实例,实现逻辑层面的隔离观测。

基于 BCC 工具实现容器 syscall 采集

BCC(BPF Compiler Collection)是一套面向 eBPF 的 Python 前端库,它封装了 clang 编译、字节码加载与映射读取的细节,适合快速编写追踪脚本。下面示例展示如何使用 BCC 监听所有 execve 系统调用,并只打印属于指定 cgroup ID 的容器进程。该方式无需进入容器内部安装任何 agent,仅宿主机具备相应内核版本即可。

from bcc import BPF
import os

# 指定目标容器的 cgroup v2 ID,可通过 cat /sys/fs/cgroup/.../cgroup.id 获取
TARGET_CGROUP_ID = 12345

bpf_src = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

struct data_t {
    u32 pid;
    u64 cgid;
    char comm[16];
};

BPF_PERF_OUTPUT(events);

TRACEPOINT_PROBE(raw_syscalls, sys_enter) {
    struct data_t data = {};
    u64 cgid = bpf_get_current_cgroup_id();
    if (cgid != TARGET_CGROUP_ID)
        return 0;
    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.cgid = cgid;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    events.perf_submit(args, &data, sizeof(data));
    return 0;
}
"""

bpf_src = bpf_src.replace("TARGET_CGROUP_ID", str(TARGET_CGROUP_ID))
b = BPF(text=bpf_src)

def print_event(cpu, data, size):
    event = b["events"].event(data)
    print("pid:%d cgroup:%d comm:%s" % (event.pid, event.cgid, event.comm))

b["events"].open_perf_buffer(print_event)
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

上述代码中,TRACEPOINT_PROBE 宏将 eBPF 程序绑定到 raw_syscalls:sys_enter 跟踪点。我们在程序开头就判断 cgroup ID,只有匹配目标容器的调用才会提交到 perf buffer,这样能大幅减少用户态处理压力。实际部署时,可以把 TARGET_CGROUP_ID 改为从容器运行时接口动态获取,从而同时监控多个容器。

与直接使用 strace -p 附加到容器进程相比,BCC 方案的优点在于零侵入:strace 基于 ptrace,会显著拖慢目标程序并可能触发容器内部的信号处理逻辑,而 eBPF 在内核态完成过滤,几乎不影响业务吞吐。不过 BCC 依赖本地 clang 与内核头文件,在精简版容器宿主机上可能需额外安装 linux-headers 包。

生产环境中的性能与隔离考量

当集群规模扩大,成百上千个容器同时产生 syscall 事件时,eBPF 程序的自身开销与映射表竞争成为关注点。经验表明,在 4 核虚拟机上运行仅做 ID 过滤的 sys_enter 探针,额外 CPU 占用低于 3%,但若在 eBPF 中频繁调用 bpf_probe_read 读取用户态字符串(如文件路径),延迟会明显上升。因此建议仅在必要时复制字符串,并尽量使用 BPF_MAP_TYPE_PERCPU_HASH 降低多核锁冲突。

另外,多租户集群要求监控组件不能越权看到其他客户的容器数据。我们可以通过 cgroup 级 eBPF 附加点(BPF_CGROUP_SYSCTL 或 per-cgroup bpf linkage)将程序限定在指定 cgroup 目录,使得只有该子树下的进程才会触发。结合 Kubernetes 的 securityContext 限制监控 Pod 的 CAP_BPFCAP_SYS_ADMIN 能力,既能完成采集,又避免全局钩子带来的信息泄露风险。

从长期运维看,把 eBPF 采集器打包为 DaemonSet,并暴露 Prometheus 指标如 container_syscall_count 按命名空间聚合,能帮助研发快速定位某次发布后文件描述符暴涨或异常网络连出。配合告警规则,当 open 系统调用频率超阈值时自动抓取堆栈,比传统日志轮询更及时也更省资源。

常见误区与排错思路

不少工程师初次使用 eBPF 监控容器时,发现抓不到任何事件,便误以为内核不支持。实际上多数情况是容器使用了 cgroup v1,而脚本里调用了 bpf_get_current_cgroup_id 期望 v2 的扁平 ID。此时应检查 stat -fc %T /sys/fs/cgroup 输出,若为 tmpfs 说明是 v1,需要改为读取 __cgroup_bpf_attach 或解析 task_struct->cgroups->subsys 数组。另一个误区是认为 eBPF 可以捕获所有 syscall,但若内核编译时未开启 CONFIG_BPF_EVENTS,tracepoint 将无法加载。

排错时推荐先在宿主机用 bpftool prog list 确认程序已挂载,再用 cat /sys/kernel/debug/tracing/trace_pipe 观察是否有内核日志。若用户态 Python 报权限错误,通常是因为缺少 CAP_SYS_ADMIN 或未关闭 Secure Boot 导致的签名校验。理清这些依赖后,eBPF 监控容器系统调用便能稳定服务于日常巡检与攻防溯源。

eBPF容器监控系统调用修改时间:2026-08-18 15:52:39

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。