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

一、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_SKB、CGROUP_SOCK、CGROUP_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