导读:本期聚焦于创作的《如何使用bpf_get_current_cgroup_id获取cgroup ID实现网络资源隔离?》,敬请观看详情。为什么同样的eBPF程序,在容器环境里却拿不到想要的数据?问题往往出在缺少cgroup感知。bpf_get_current_cgroup_id这个辅助函数能在eBPF程序运行时返回当前进程所属的cgroup ID,它就像一把钥匙,让网络过滤、流量统计、带宽限制等逻辑能够按容器粒度精确生效。本文先讲清cgroup v2下的层级结构和ID生成原理,再给出完整的代码示例,演示如何挂载到cgroup v2路径、在BPF程序里获取ID并写入map,最后结合实际场景分析常见坑点,比如挂载点选错导致ID恒为0、容器运行时路径差异等问题,帮助你在Kubernetes或裸机容器环境中稳定落地基于cgroup的网络资源隔离方案。

在容器化部署越来越普遍的今天,网络隔离早已不是简单的iptables规则就能搞定的事情。当几十个容器共享同一台宿主机的网络栈时,如何精确识别某个数据包属于哪个容器,进而实施差异化的网络策略,就成了一道绕不开的门槛。eBPF提供的bpf_get_current_cgroup_id辅助函数正是为此而生,它允许在BPF程序执行时获取当前上下文所属的cgroup ID,配合cgroup v2的层级结构,可以做到容器级别的网络资源隔离与管控。

如何使用bpf_get_current_cgroup_id获取cgroup ID实现网络资源隔离?

一、cgroup ID到底是什么,为什么它能用于隔离

要理解bpf_get_current_cgroup_id的作用,先得弄清楚cgroup ID从哪来。在cgroup v2体系中,每个cgroup在内核里对应一个kernfs节点,这个节点有一个全局唯一的64位编号,也就是我们常说的cgroup ID。它本质上是对cgroup层级位置的一种数字化表达,同一个cgroup在整个生命周期内ID保持不变,除非该cgroup被销毁重建。

在容器场景下,每个容器通常由容器运行时(比如containerd、CRI-O)创建一个独立的cgroup目录,位于宿主机的/sys/fs/cgroup层级之下。以Kubernetes为例,Pod的cgroup路径一般是/sys/fs/cgroup/kubepods/pod/这样的形式。当BPF程序以cgroup挂载方式运行时,内核知道触发事件的进程正处于哪个cgroup,此时调用bpf_get_current_cgroup_id就能拿到这个64位ID,从而在map中查询该容器对应的策略配置。

这样做的好处显而易见:不需要解析进程的PID或namespace,ID本身就是稳定的标识;而且查询map的复杂度是O(1),即使宿主机上有数百个容器,也不会带来明显的性能开销。相比之下,通过PID做映射要处理PID复用问题,通过cgroup路径做字符串匹配又太慢,cgroup ID恰好在这两者之间取得了平衡。

二、编写获取cgroup ID的BPF程序

下面写一个最简单的示例,挂载类型选择BPF_PROG_TYPE_CGROUP_SKB,也就是挂在cgroup的ingress或egress钩子上。程序逻辑很简单:获取当前cgroup ID,然后在一个hash map里查询该ID是否被标记为需要丢弃流量,如果命中就返回丢弃动作。

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

// key为cgroup ID,value为策略:0放行,1丢弃
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, __u64);
    __type(value, __u32);
} cgroup_policy SEC(".maps");

SEC("cgroup/skb")
int filter_by_cgroup(struct __sk_buff *skb)
{
    // 获取触发本次网络事件的进程所属cgroup的ID
    __u64 cgid = bpf_get_current_cgroup_id();

    __u32 *policy = bpf_map_lookup_elem(&cgroup_policy, &cgid);
    if (policy && *policy == 1) {
        return 0; // 丢弃该数据包
    }
    return 1; // 放行
}

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

这段代码有几个细节值得注意。首先,bpf_get_current_cgroup_id返回的是相对于程序所挂载的cgroup根的ID。换句话说,如果程序挂在/sys/fs/cgroup这个根上,那么拿到的是容器cgroup在整个层级中的绝对ID;但如果挂在某个子目录上,返回值的语义会发生变化。这是新手最容易踩的坑之一:挂载点不同,同一个容器拿到的ID可能不一样。

其次,这个辅助函数要求内核版本在4.18以上,且程序类型必须是cgroup相关的挂载类型,比如CGROUP_SKBCGROUP_SOCKCGROUP_SOCK_ADDR等。如果你在kprobe或者tracepoint类型的程序里调用它,得到的结果可能是0,因为那些钩子的上下文里并没有明确的cgroup归属信息(此时应改用bpf_get_current_task再解析task_struct)。

三、用户态程序:挂载、填充map与路径解析

BPF程序写好后,还需要一个用户态程序负责三件事:把程序挂到正确的cgroup位置、往map里写入各容器的cgroup ID、以及在容器增删时更新map内容。这里用libbpf的C接口演示核心流程。

#include <bpf/libbpf.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    struct bpf_object *obj;
    int prog_fd, map_fd, cgroup_fd;
    __u64 cgid;
    __u32 deny = 1;

    // 打开编译好的BPF目标文件
    obj = bpf_object__open_file("filter_by_cgroup.o", NULL);
    bpf_object__load(obj);

    prog_fd = bpf_program__fd(
        bpf_object__find_program_by_name(obj, "filter_by_cgroup"));
    map_fd = bpf_object__find_map_fd_by_name(obj, "cgroup_policy");

    // 打开要挂载的cgroup v2目录
    cgroup_fd = open("/sys/fs/cgroup", O_RDONLY);
    if (cgroup_fd < 0) {
        perror("open cgroup root failed");
        return 1;
    }

    // 将程序attach到egress方向
    if (bpf_prog_attach(prog_fd, cgroup_fd,
                        BPF_CGROUP_INET_EGRESS, 0)) {
        perror("attach failed");
        return 1;
    }

    // 通过cgroup路径获取对应的ID并写入map
    // stat返回的inode编号就是cgroup ID
    struct stat st;
    if (stat("/sys/fs/cgroup/kubepods/pod-abc123/container1", &st) == 0) {
        cgid = st.st_ino;
        bpf_map_update_elem(map_fd, &cgid, &deny, BPF_ANY);
        printf("cgroup %llu now blocked\n", (unsigned long long)cgid);
    }

    return 0;
}

这个示例里用了一个小技巧:cgroup v2的目录对应的kernfs节点,其inode号与bpf_get_current_cgroup_id返回的ID是一致的。所以在用户态,只需要对目标cgroup路径调用stat,取出st_ino字段,就能得到和内核侧完全相同的ID值,直接写进map即可完成匹配。这种方式避免了依赖name_to_handle_at这类复杂接口,代码简洁且跨发行版表现稳定。

实际工程中还需要考虑动态性:容器会被频繁创建和销毁,对应的cgroup目录也随之增删。推荐的做法是用户态守护进程监听cgroup文件系统事件(可以用inotify监控/sys/fs/cgroup下的目录变化),发现新容器就查询其策略需求并更新map;容器退出时则调用bpf_map_delete_elem清理条目,防止map膨胀。也可以通过遍历BPF_MAP_TYPE_CGROUP_STORAGE或定期对账的方式兜底。

四、常见坑点与排错思路

第一个高频问题是拿到的ID永远是0。前面提到,最常见的原因是程序挂载类型不支持该辅助函数,或者宿主机还在使用cgroup v1。cgroup v1下网络相关的挂载点在net_cls子系统,ID语义完全不同,此时应升级到cgroup v2(内核命令行加上cgroup_no_v1=all或确认systemd已启用统一层级),再重新验证。可以先用mount | grep cgroup2确认是否挂载在/sys/fs/cgroup

第二个问题是不同运行时的路径差异。Docker默认把容器放在/sys/fs/cgroup/docker/<容器完整ID>下,containerd和CRI-O的布局又各不相同,Kubernetes则叠加了kubepods层级。写死路径显然不可取,更稳妥的做法是从容器运行时API或CRI接口获取容器的cgroup路径,或者直接遍历/proc/<pid>/cgroup文件反查。该文件每行格式类似0::/kubepods/pod-abc123,冒号前的0表示v2层级,后面就是相对路径,拼上根目录即可。

第三个问题涉及性能与语义的权衡。cgroup ID虽然稳定,但容器重建后ID会变化,如果把它当作长期身份标识存储到外部系统,就会出现策略错位。正确的姿势是:外部系统保存容器名或Pod标识,BPF侧的map只作为运行时缓存,由控制平面在容器生命周期事件中同步刷新。此外,对于做带宽限制的场景,建议结合BPF_MAP_TYPE_CGROUP_STORAGE按cgroup维度存储令牌桶状态,这样即使多个CPU并发处理同一容器的包,统计信息也不会互相覆盖。

总的来说,bpf_get_current_cgroup_id只是一个入口,真正让它发挥价值的是围绕cgroup层级建立的一整套标识、挂载与同步机制。理清挂载点语义、适配运行时路径差异、处理好动态更新,这三件事做到位之后,基于eBPF的容器级网络隔离方案就能在生产环境中长期稳定运行,无论是做流量丢弃、优先级标记还是带宽配额,思路都是相通的。

bpf_get_current_cgroup_idcgroup IDeBPF网络隔离修改时间:2026-09-08 00:24:42

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