4. eBPF Map 数据结构:哈希表、数组、LRU、percpu 类型,以及 Map 在租户隔离中的数据存储角色

eBPF Map 是内核与用户态程序之间、以及不同 eBPF 程序之间共享数据和状态的核心机制。在租户隔离场景中,Map 承担着隔离策略存储、运行时状态追踪和控制平面与数据平面联动的关键角色。本章深入剖析四种最常用的 Map 类型——哈希表、数组、LRU 和 percpu 变体,并阐明它们在多租户架构中的数据存储职责。

4.1 四种核心 Map 类型对比

Map 类型 内核类型标识 键值特性 典型租户隔离用途 性能特征
哈希表 (Hash) BPF_MAP_TYPE_HASH 动态键值对,键长度可变 租户 ID → 策略规则映射 O(1) 平均查找,适合随机访问
数组 (Array) BPF_MAP_TYPE_ARRAY 固定索引,预分配连续内存 租户索引 → 计数器/统计 O(1) 直接索引,无锁读
LRU 哈希 (LRU Hash) BPF_MAP_TYPE_LRU_HASH 最近最少使用淘汰,有容量上限 连接跟踪、会话缓存 O(1) 操作 + 淘汰开销
Per-CPU 哈希 (Per-CPU Hash) BPF_MAP_TYPE_PERCPU_HASH 每个 CPU 独立副本,无锁累加 租户级包计数、字节统计 极高并发,消除缓存行争用
选型原则: 哈希表适合不规则键的查找;数组适合固定数量的租户槽位;LRU 适合有容量限制的热数据缓存;Per-CPU 变体是高频统计场景的首选,能避免多核竞争导致的性能退化。

4.2 Map 在租户隔离中的数据存储角色

在控制平面与数据平面联动的架构中,Map 充当了策略下发通道和运行时状态仓库的双重角色。控制平面通过 bpf_map_update_elem 系统调用写入隔离规则,数据平面的 eBPF 程序在每次包处理时通过 bpf_map_lookup_elem 读取规则并执行隔离决策。

4.2.1 核心存储模型

  • 租户策略表 (Hash Map): 键为租户 ID(如 32 位整数或 UUID),值为策略位图或规则结构体。控制平面增删改查,数据平面只读。
  • 租户统计表 (Per-CPU Array): 数组索引对应租户编号,每个 CPU 维护独立计数器。数据平面原子累加,控制平面汇总读取。
  • 连接跟踪表 (LRU Hash): 键为五元组,值为租户 ID 和状态。自动淘汰过期连接,防止内存泄漏。
  • 全局配置表 (Array): 固定索引 0 存放全局开关、默认动作等元配置,所有租户共享。
隔离关键点: 每个 eBPF 程序在加载时通过 SEC("maps") 段声明 Map 的访问权限。租户 A 的 eBPF 程序无法直接访问租户 B 的 Map 条目——除非通过共享 Map 显式授权。这种基于 Map 的访问控制是 eBPF 租户隔离的第一道防线。

4.3 租户隔离场景下的 Map 联动架构

下图展示了控制平面通过 Map 接口与数据平面 eBPF 程序交互的完整链路,以及四种 Map 类型在其中的具体部署位置。

eBPF Map 在租户隔离中的联动架构 eBPF Map 在租户隔离中的联动架构 控制平面 (用户态) 租户管理服务 策略编排引擎 监控采集器 bpf_map_update_elem / lookup 数据平面 (内核 eBPF) XDP / TC 程序 包处理流水线 隔离决策引擎 bpf_map_lookup_elem / update eBPF Map 存储层 哈希表 (Hash Map) 租户 ID → 策略规则 Per-CPU 数组 (Per-CPU Array) 租户索引 → 统计计数器 LRU 哈希 (LRU Hash) 五元组 → 租户连接状态 数组 (Array Map) 固定索引 → 全局配置 控制平面写入 → 数据平面读取

4.4 关键操作与隔离语义

4.4.1 控制平面接口调用

控制平面通过 bpf() 系统调用操作 Map,典型流程如下:

// 1. 创建哈希表 Map,最大 1024 个条目
int map_fd = bpf_create_map(BPF_MAP_TYPE_HASH, 
                            sizeof(uint32_t),   // 键:租户 ID
                            sizeof(struct policy), // 值:策略结构体
                            1024, 0);

// 2. 为租户 ID=1001 写入策略
uint32_t tenant_id = 1001;
struct policy pol = { .allow = 1, .rate_limit = 1000 };
bpf_map_update_elem(map_fd, &tenant_id, &pol, BPF_ANY);

// 3. 读取租户统计(Per-CPU 数组需要汇总所有 CPU)
uint32_t cpu;
uint64_t total_packets = 0;
for (cpu = 0; cpu < nr_cpus; cpu++) {
    uint64_t val;
    bpf_map_lookup_elem(stat_fd, &tenant_id, &val);
    total_packets += val;
}

4.4.2 数据平面读取示例 (C 伪代码)

// XDP 程序中的隔离决策
int xdp_tenant_isolation(struct xdp_md *ctx) {
    uint32_t tenant_id = resolve_tenant(ctx); // 从包中提取租户 ID
    struct policy *pol = bpf_map_lookup_elem(&tenant_policy_map, &tenant_id);
    if (!pol) {
        // 未找到租户策略,执行默认动作
        return XDP_DROP;
    }
    if (!pol->allow) {
        return XDP_DROP;
    }
    // 更新 Per-CPU 统计
    uint64_t *cnt = bpf_map_lookup_elem(&tenant_stats, &tenant_id);
    if (cnt) {
        __sync_fetch_and_add(cnt, 1);
    }
    return XDP_PASS;
}
隔离陷阱: 使用 BPF_F_RDONLY 或 BPF_F_WRONLY 标志创建 Map 可以限制数据平面程序的写入权限。在多租户场景中,建议将策略表设为数据平面只读,防止恶意 eBPF 程序篡改隔离规则。

4.5 Per-CPU 类型在租户隔离中的优势

在多核系统中,多个 CPU 同时处理不同租户的包。如果使用普通哈希表或数组存储计数器,每次更新都需要原子操作或自旋锁,导致严重的缓存行 bouncing。Per-CPU 变体为每个 CPU 分配独立的内存副本,更新操作完全无锁,仅在控制平面汇总时遍历所有 CPU 副本。

对比维度 普通哈希/数组 Per-CPU 哈希/数组
更新操作 原子 CAS 或锁保护 无锁,直接写本地副本
缓存行争用 高(多核写同一地址) 无(每个核独立缓存行)
读取汇总 直接读取 需遍历所有 CPU 求和
适用场景 低频更新、高频读取 高频更新、低频汇总
租户隔离收益 中等 高(避免租户间性能干扰)

4.6 Map 生命周期与租户资源管理

每个 Map 在内核中占用固定内存,租户数量增长时必须合理规划:

  • 预分配 vs 动态增长: 数组类型在创建时即分配全部内存,适合已知最大租户数;哈希表按需分配,适合租户数量动态变化的场景。
  • LRU 淘汰策略: 连接跟踪表使用 LRU 哈希,当租户连接数超过上限时自动淘汰最久未使用的条目,防止单个租户耗尽 Map 容量。
  • Map 固定大小: 创建后无法动态扩容,需在加载时根据 max_entries 参数预留足够空间。控制平面应监控 Map 使用率,接近上限时告警。
  • 租户删除清理: 租户下线时,控制平面需遍历 Map 删除该租户的所有条目,避免残留数据影响新租户。
最佳实践: 在大型多租户系统中,建议为每个租户分配独立的 Map 实例(如 tenant_1001_policy),而非所有租户共享同一个 Map。虽然增加管理复杂度,但能提供更强的故障隔离——一个租户的 Map 损坏不会影响其他租户。

4.7 本章小结

eBPF Map 是租户隔离架构的数据中枢。哈希表提供灵活的键值存储,数组保证固定索引的低延迟访问,LRU 变体自动管理缓存容量,Per-CPU 类型消除多核竞争。控制平面通过 bpf() 系统调用写入策略和读取统计,数据平面在内核热路径中高效查询和更新。正确选择和配置 Map 类型,是构建高性能、强隔离的多租户 eBPF 系统的基石。