4. eBPF Map 数据结构:哈希表、数组、Per-CPU Map、Ring Buffer Map 与选型对比
eBPF Map 是内核与用户态程序之间共享数据的关键结构,也是 eBPF 程序状态持久化的核心载体。在《eBPF 网络观测模板制品库多团队协作》场景中,不同团队(网络、存储、安全)需要根据观测指标的特性选择最合适的 Map 类型,以平衡性能、内存与数据一致性。本章将深入剖析五种最常用的 eBPF Map 数据结构,并给出选型对比表。
4.1 哈希表(Hash Map)
哈希表是最通用的 eBPF Map,支持通过任意类型的 Key 快速查找 Value。内核使用 链地址法 解决哈希冲突,适合存储非连续、稀疏的键值对,例如按五元组(源 IP、目的 IP、协议、源端口、目的端口)聚合的网络流统计。
- 动态大小:创建时指定最大条目数,实际条目数可动态增减。
- O(1) 平均查找:哈希函数将 Key 映射到桶,链表处理冲突。
- 支持遍历:通过
bpf_map_get_next_key()遍历所有条目。 - 典型用途:连接跟踪表、DNS 缓存、自定义聚合指标。
// 定义哈希表 Map
struct bpf_map_def SEC("maps") flow_stats = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(struct flow_key), // 五元组结构体
.value_size = sizeof(struct flow_metrics), // 包数、字节数、延迟等
.max_entries = 65536,
};
4.2 数组(Array Map)
数组 Map 使用整数索引作为 Key,预分配固定大小的连续内存。访问速度极快(O(1) 且无哈希计算),但 Key 必须连续且不能动态扩展。适合存储固定数量的配置项、全局标志或预分配的统计槽位。
| 特性 | 数组 Map | 哈希表 Map |
|---|---|---|
| Key 类型 | 仅支持整数索引(0 ~ max_entries-1) | 任意固定大小数据类型 |
| 内存分配 | 创建时一次性分配,固定大小 | 动态分配,按需增长 |
| 查找速度 | 直接索引,无哈希开销 | 哈希计算 + 链表遍历(最坏 O(n)) |
| 遍历顺序 | 按索引顺序,天然有序 | 无序,依赖哈希桶分布 |
| 典型场景 | CPU 编号索引的统计数组、全局配置表 | 网络流表、动态键值存储 |
// 定义数组 Map,用于存储每个 CPU 的包计数
struct bpf_map_def SEC("maps") cpu_packet_count = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(u32), // CPU ID
.value_size = sizeof(u64), // 包计数
.max_entries = 128, // 最多支持 128 个 CPU
};
4.3 Per-CPU Map
Per-CPU Map 为每个 CPU 核心维护一份独立的 Map 副本。eBPF 程序在运行时直接操作当前 CPU 的副本,完全避免锁竞争,是高性能计数场景的首选。内核提供了 BPF_MAP_TYPE_PERCPU_HASH 和 BPF_MAP_TYPE_PERCPU_ARRAY 两种变体。
// Per-CPU 数组:每个 CPU 独立统计延迟分布
struct bpf_map_def SEC("maps") latency_hist_percpu = {
.type = BPF_MAP_TYPE_PERCPU_ARRAY,
.key_size = sizeof(u32), // 延迟桶索引
.value_size = sizeof(u64), // 计数
.max_entries = 64, // 64 个延迟桶
};
用户态读取注意事项: 读取 Per-CPU Map 时,需要遍历所有 CPU 的副本并求和,才能得到全局值。如果只关心当前 CPU 的局部状态,可以直接读取对应 CPU 的副本。
4.4 Ring Buffer Map
Ring Buffer(环形缓冲区)是 eBPF 中 高效传递事件流 的 Map 类型(BPF_MAP_TYPE_RINGBUF)。它解决了传统 perf buffer 的诸多痛点:支持可变长度数据、减少内存拷贝、支持 reserve/commit 模式,且用户态读取时无需轮询。
- 零拷贝写入:eBPF 程序通过
bpf_ringbuf_reserve()获取缓冲区指针,直接写入数据,然后bpf_ringbuf_submit()提交。 - 有序性保证:数据按提交顺序写入,用户态按相同顺序读取。
- 背压机制:当缓冲区满时,可配置丢弃新数据或覆盖旧数据。
- 多生产者单消费者:多个 eBPF 程序可同时写入,用户态单线程消费。
// 定义 Ring Buffer,用于传递网络事件
struct bpf_map_def SEC("maps") net_events = {
.type = BPF_MAP_TYPE_RINGBUF,
.max_entries = 1 << 20, // 1MB 缓冲区
};
// eBPF 程序中写入事件
SEC("kprobe/tcp_connect")
int trace_connect(struct pt_regs *ctx) {
struct event *e;
e = bpf_ringbuf_reserve(&net_events, sizeof(*e), 0);
if (!e) return 0; // 缓冲区满,丢弃
e->pid = bpf_get_current_pid_tgid() >> 32;
e->timestamp = bpf_ktime_get_ns();
bpf_ringbuf_submit(e, 0);
return 0;
}
4.5 Map 选型对比表
以下对比表帮助你在多团队协作的制品库场景中快速选择最合适的 Map 类型:
| Map 类型 | Key 灵活性 | 内存模型 | 并发安全 | 读取性能 | 典型延迟曲线交接场景 |
|---|---|---|---|---|---|
| BPF_MAP_TYPE_HASH | 高(任意固定大小) | 动态分配 | 全局锁(可能竞争) | 中等 | 按五元组聚合的流延迟统计 |
| BPF_MAP_TYPE_ARRAY | 低(仅整数索引) | 预分配固定 | 无锁(索引直接访问) | 高 | CPU 编号索引的延迟百分位桶 |
| BPF_MAP_TYPE_PERCPU_HASH | 高 | 每 CPU 独立副本 | 完全无锁 | 极高(写入) | 高并发场景的延迟直方图 |
| BPF_MAP_TYPE_PERCPU_ARRAY | 低 | 每 CPU 独立副本 | 完全无锁 | 极高(写入) | 每 CPU 的请求延迟累加 |
| BPF_MAP_TYPE_RINGBUF | N/A(事件流) | 环形缓冲区 | 多生产者无锁 | 高(批量读取) | 实时延迟事件上报给用户态 |
4.6 Map 数据流与协作关系
下图展示了在多团队协作的延迟曲线交接中,不同 Map 类型如何协同工作:
4.7 选型决策流程
在实际的延迟曲线交接项目中,可以按以下步骤选择 Map 类型:
- 判断数据模型:如果是键值对(如流 ID → 统计),优先考虑 Hash 系列;如果是固定索引(如 CPU ID → 计数),优先考虑 Array 系列。
- 评估并发写入频率:如果写入频率极高(> 1M ops/s),必须使用 Per-CPU 变体以避免锁竞争。
- 确定数据传递方式:如果需要将事件实时推送给用户态,使用 Ring Buffer;如果用户态按需拉取,使用 Hash 或 Array。
- 考虑内存开销:Per-CPU 变体会将内存放大 N 倍(N = CPU 核数),在内存受限的环境下需谨慎。
- 团队协作约定:在制品库中,不同团队应约定 Map 的命名规范、Key 结构、Value 字段定义,避免冲突。