GO_1.24_SWISS_TABLES_ADAPTATION.md 14 KB

Go 1.24 Swiss Tables 适配文档

为什么有两个 Swiss Tables 函数?

在实现过程中,我们创建了两个函数来处理不同的追踪格式:

1. inject_header_swiss - W3C Traceparent 格式

用途:注入 W3C 标准的 traceparent header

关键特征

  • 参数类型struct span_context* (W3C 格式的 span context)
  • Header Key"traceparent" (W3C 标准)
  • Header Value 长度W3C_VAL_LENGTH (通常比 CW 格式短)
  • 字符串转换:使用 span_context_to_w3c_string() 函数
  • 内存分配:使用 write_target_data() (通用内存分配)

调用路径

inject_header() 
  → inject_header_swiss()  (Go 1.24+)

2. cw_inject_header_swiss - CW (Coroot) 内部格式

用途:注入项目内部的 CW trace header

关键特征

  • 参数类型struct apm_span_context* (CW/APM 格式的 span context)
  • Header Key"cwtrace" (项目内部格式)
  • Header Value 长度CW_HEADER_VAL_LENGTH (123 bytes,包含更多信息)
  • 字符串转换:使用 span_context_to_cw_string() 函数
  • 内存分配:使用 cw_write_target_data() (带 proc_info 的进程隔离分配)

调用路径

cw_inject_header() 
  → cw_inject_header_swiss()  (Go 1.24+)
cw_inject_header2() 
  → cw_inject_header_swiss()  (Go 1.24+)

为什么需要两个函数?

  1. 不同的追踪标准

    • W3C Traceparent:行业标准,用于跨系统追踪
    • CW Trace:项目内部格式,可能包含更多元数据
  2. 不同的数据结构

    • span_context vs apm_span_context - 不同的字段布局
    • 需要不同的序列化函数
  3. 不同的内存分配策略

    • write_target_data() - 通用分配
    • cw_write_target_data() - 进程隔离分配(带 proc_info)

代码对比

// W3C 格式
inject_header_swiss():
  - key: "traceparent" (W3C_KEY_LENGTH)
  - value: span_context_to_w3c_string()
  - alloc: write_target_data()

// CW 格式  
cw_inject_header_swiss():
  - key: "cwtrace" (CW_HEADER_KEY_LENGTH)
  - value: span_context_to_cw_string()
  - alloc: cw_write_target_data(..., proc_info)

总结

两个函数实现了相同的 Swiss Tables 注入逻辑,但:

  • 处理不同的追踪格式(W3C vs CW)
  • 使用不同的数据结构(span_context vs apm_span_context)
  • 使用不同的内存分配方法

这是为了保持向后兼容性,同时支持两种追踪格式。


Go 1.24 Swiss Tables 适配文档

问题背景

Go 1.24 引入了新的 map 实现(Swiss Tables),替代了传统的 bucket-based hash table。这导致 euspace 项目在监控 Go 1.24 应用时出现不兼容问题。

问题发现

在测试过程中发现,Go 1.24 应用的监控功能无法正常工作,具体表现为:

2025-11-04 10:13:11.68|ERROR|3162137|tls.go:291] [AttachGoTlsUprobes] STEP 12.2: Failed to get buckets offset, pid=3152597, version=go1.24.0

根本原因:Go 1.24 使用 Swiss Tables 实现,runtime.hmap 结构体不再存在 buckets 字段。

解决方案

1. 源码分析

通过查看 Go 1.24 源码(/root/code/go),我们发现:

旧实现(Go < 1.24)

  • 使用 runtime.hmap 结构体
  • 包含 buckets unsafe.Pointer 字段(指向 bucket 数组)
  • 结构体位置:src/runtime/map_noswiss.go

    type hmap struct {
    count     int
    flags     uint8
    B         uint8
    noverflow uint16
    hash0     uint32
    buckets   unsafe.Pointer  // ← 旧的字段
    oldbuckets unsafe.Pointer
    // ...
    }
    

新实现(Go 1.24+)

  • 使用 internal/runtime/maps.Map 结构体
  • 包含 dirPtr unsafe.Pointer 字段(替代 buckets
  • 结构体位置:src/internal/runtime/maps/map.go

    type Map struct {
    used      uint64      // offset 0
    seed      uintptr     // offset 8
    dirPtr    unsafe.Pointer  // offset 16 ← 新的字段
    dirLen    int         // offset 24
    globalDepth uint8
    globalShift uint8
    writing   uint8
    clearSeq  uint64
    }
    

2. 数据结构差异

小 Map(dirLen == 0)

  • dirPtr 直接指向一个 group
  • group 结构:

    +------------------+
    | ctrlGroup (8B)   |  ← 控制字,每个字节表示一个 slot 的状态
    +------------------+
    | slot[0] (40B)    |  ← key (16B) + elem (24B)
    | slot[1] (40B)    |
    | ...              |
    | slot[7] (40B)    |
    +------------------+
    

大 Map(dirLen > 0)

  • dirPtr 指向 directory(table 指针数组)
  • 需要访问 directory → table → group → slot

3. 实现适配

3.1 获取偏移量(Go 层)

文件ebpftracer/tls.go

修改点

  1. 尝试获取 runtime.hmap.buckets(向后兼容)
  2. 如果失败,尝试获取 internal/runtime/maps.Map.dirPtr
  3. 如果 DWARF 查找失败,使用硬编码偏移量 16(64-bit 系统)

    // 尝试获取 buckets 字段
    bucketsOff, ok2 := tracer.GetOffset(tracer.NewID("std", "runtime", "hmap", "buckets"), path)
    
    if !ok2 {
    // Go 1.24+ Swiss Tables
    // 尝试获取 maps.Map.dirPtr
    swissFields := []struct {
        pkg        string
        structName string
        field      string
    }{
        {"internal/runtime/maps", "Map", "dirPtr"},
        {"internal.runtime.maps", "Map", "dirPtr"},
        {"maps", "Map", "dirPtr"},
    }
        
    for _, sf := range swissFields {
        swissOff, swissOk := tracer.GetOffset(tracer.NewID("std", sf.pkg, sf.structName, sf.field), path)
        if swissOk {
            bucketsOff = swissOff
            ok2 = true
            break
        }
    }
        
    // 如果还是失败,使用硬编码(基于源码分析)
    if !ok2 && major >= 1 && minor >= 24 {
        bucketsOff = 16  // used(8) + seed(8) + dirPtr(16)
        ok2 = true
    }
    }
    

结果:成功获取到 dirPtr 偏移量(16),并正确设置到 proc_info->buckets_ptr_pos

3.2 Header 注入实现(eBPF 层)

文件ebpftracer/ebpf/utrace/go/net/client.probe.bpf.c

新增函数

  1. inject_header_swiss() - W3C traceparent 格式
  2. cw_inject_header_swiss() - CW 格式

实现逻辑

static __always_inline long inject_header_swiss(...) {
    // 1. 读取 used 计数(maps.Map 的第一个字段)
    u64 used = 0;
    bpf_probe_read_user(&used, sizeof(used), headers_ptr);
    
    // 2. 检查 map 是否已满
    if (used >= 8) return -1;
    
    // 3. 读取 dirLen(offset 24)判断 map 类型
    s32 dirLen = 0;
    bpf_probe_read_user(&dirLen, sizeof(dirLen), headers_ptr + 24);
    
    // 4. 如果是小 map(dirLen == 0),读取 dirPtr
    void *group_ptr = NULL;
    if (dirLen == 0) {
        bpf_probe_read_user(&group_ptr, sizeof(group_ptr), 
                           headers_ptr + buckets_ptr_pos);
    } else {
        // 大 map 暂不支持
        return -1;
    }
    
    // 5. 读取 control word(group 的前 8 字节)
    u64 ctrls = 0;
    bpf_probe_read_user(&ctrls, sizeof(ctrls), group_ptr);
    
    // 6. 查找第一个空 slot(ctrl byte == 0x80)
    u8 slot_idx = 0;
    for (u8 i = 0; i < 8; i++) {
        u8 ctrl_byte = (ctrls >> (i * 8)) & 0xFF;
        if (ctrl_byte == 0x80) {
            slot_idx = i;
            break;
        }
    }
    
    // 7. 计算 slot 偏移量
    // group 布局:ctrlGroup(8) + slots[8]
    // 每个 slot:key(16) + elem(24) = 40 bytes
    u64 slot_offset = 8 + (slot_idx * 40);
    void *slot_ptr = group_ptr + slot_offset;
    
    // 8. 写入 key(go_string_ot)
    char key[W3C_KEY_LENGTH] = "traceparent";
    void *key_str_ptr = write_target_data(key, W3C_KEY_LENGTH);
    struct go_string_ot key_str = {.str = key_str_ptr, .len = W3C_KEY_LENGTH};
    bpf_probe_write_user(slot_ptr, &key_str, sizeof(key_str));
    
    // 9. 写入 value(go_slice_ot)
    char val[W3C_VAL_LENGTH];
    span_context_to_w3c_string(propagated_ctx, val);
    // ... 创建 go_string_ot 和 go_slice_ot
    void *elem_ptr = slot_ptr + 16;  // 在 key 之后
    bpf_probe_write_user(elem_ptr, &val_slice, sizeof(val_slice));
    
    // 10. 更新 control byte
    u8 new_ctrl = 0x5A;  // 占位符,应该是 H2 hash
    u64 ctrl_mask = 0xFFULL << (slot_idx * 8);
    ctrls = (ctrls & ~ctrl_mask) | (((u64)new_ctrl) << (slot_idx * 8));
    bpf_probe_write_user(group_ptr, &ctrls, sizeof(ctrls));
    
    // 11. 更新 used 计数
    used += 1;
    bpf_probe_write_user(headers_ptr, &used, sizeof(used));
    
    return 0;
}

3.3 版本路由

修改点:所有 header 注入函数都添加了版本检测

// 在 inject_header(), cw_inject_header(), cw_inject_header2() 中
if (proc_info->version >= GO_VERSION(1, 24, 0)) {
    return inject_header_swiss(...);  // 或 cw_inject_header_swiss(...)
}
// 否则使用旧的实现

3.4 调试日志

所有关键步骤都添加了 bpf_printk 日志:

  • 函数入口:START
  • 读取的值:used, dirLen, group_ptr, ctrls
  • 查找结果:Found empty slot at index X
  • 计算的值:slot_idx, slot_offset, slot_ptr
  • 内存分配:key_str_ptr, val_str_ptr
  • 写入操作:每个步骤的成功/失败状态
  • 更新操作:control word 和 used count 的更新
  • 函数完成:SUCCESS

技术细节

Control Byte 格式

Swiss Tables 使用 control byte 标记每个 slot 的状态:

  • 0x80 (0b10000000) = 空 slot
  • 0xFE (0b11111110) = 已删除(tombstone)
  • 0x00-0x7F = 已使用,低 7 位是 H2 hash

Slot 布局(map[string][]string)

slot[i] = {
    key:  go_string_ot {  // offset 0
        str: *char,        // 8 bytes
        len: int64         // 8 bytes
    },
    elem: go_slice_ot {  // offset 16
        array: *go_string_ot,  // 8 bytes
        len:   int64,          // 8 bytes
        cap:   int64           // 8 bytes
    }
}
// 总大小:40 bytes(可能需要对齐到 48 bytes)

内存对齐

在 64-bit 系统上:

  • uint64uintptr、指针:8 字节对齐
  • maps.Map 结构体:字段按顺序排列,无 padding
    • used (0): 8 bytes
    • seed (8): 8 bytes
    • dirPtr (16): 8 bytes
    • dirLen (24): 4 bytes (int)
    • ...

文件修改清单

Go 层修改

  1. ebpftracer/tls.go
    • 添加 maps.Map.dirPtr 字段查找逻辑
    • 添加硬编码 fallback(Go 1.24+)
    • 允许在 buckets 失败时继续(只要 goid 成功)

eBPF 层修改

  1. ebpftracer/ebpf/utrace/go/net/client.probe.bpf.c

    • 新增 inject_header_swiss() 函数
    • 新增 cw_inject_header_swiss() 函数
    • 修改 inject_header() - 添加版本路由
    • 修改 cw_inject_header() - 添加版本路由
    • 修改 cw_inject_header2() - 添加版本路由
    • 修改 cw_inject_header_half() - 添加版本检查(暂不支持)
  2. ebpftracer/ebpf/utrace/go/net/server.probe.bpf.c

    • 修改所有读取 buckets_ptr_pos 的地方,添加版本检查

已知限制

  1. 仅支持小 Map:当前实现只支持 dirLen == 0 的情况(小 map,≤8 个元素)

    • HTTP headers 通常是空或很小的 map,所以这个限制可以接受
    • 如果需要支持大 map,需要实现 directory/table 访问逻辑
  2. Slot 大小假设:当前使用 40 字节(string 16 + []string 24)

    • 实际可能需要对齐到 48 字节
    • 需要根据测试结果调整
  3. H2 Hash:当前使用固定值 0x5A 作为 control byte

    • 正确实现应该计算 key 的 hash,并使用 H2 位(低 7 位)
    • 对于 HTTP headers 注入,这个占位符可能足够
  4. 并发安全:Swiss Tables 有 writing 标志用于检测并发写入

    • 当前实现没有检查这个标志
    • 可能需要添加检查以确保安全

测试验证

成功指标

  1. ✅ 成功获取 maps.Map.dirPtr 偏移量(16)
  2. ✅ 代码编译通过
  3. ⏳ 运行时 header 注入成功(需要实际测试)
  4. ⏳ 注入的 header 能被正确读取(需要实际测试)

调试方法

查看 eBPF 日志:

# 方法 1:使用 bpftool
sudo bpftool prog tracelog

# 方法 2:使用 trace_pipe
sudo cat /sys/kernel/debug/tracing/trace_pipe | grep inject_header_swiss

关键日志点:

  • inject_header_swiss: START - 函数开始
  • inject_header_swiss: used=X - map 当前元素数
  • inject_header_swiss: dirLen=X - map 类型
  • inject_header_swiss: group_ptr=0x... - group 指针
  • inject_header_swiss: Found empty slot at index X - 找到的空 slot
  • inject_header_swiss: SUCCESS - 注入成功

参考资源

  1. Go 1.24 源码/root/code/go/src/internal/runtime/maps/

    • map.go - Map 结构体定义
    • group.go - Group 结构体定义
    • table.go - Table 结构体定义
  2. 相关文档

    • Abseil Swiss Tables - Swiss Tables 设计原理
    • Go 1.24 release notes - map 实现变更说明

后续优化建议

  1. 实现正确的 H2 hash 计算

    • 使用 Go 的 hash 算法计算 key 的 hash
    • 提取低 7 位作为 H2 值
  2. 支持大 Map

    • 实现 directory 访问逻辑
    • 处理 table 查找和 group 访问
  3. 优化 Slot 大小检测

    • 通过 DWARF 信息获取实际的 slot 大小
    • 或通过运行时探测确定
  4. 添加并发安全检查

    • 检查 writing 标志
    • 处理并发写入冲突
  5. 性能优化

    • 减少不必要的内存读取
    • 优化 control word 操作

总结

通过分析 Go 1.24 源码,理解了 Swiss Tables 的数据结构,实现了适配的 header 注入逻辑。主要工作包括:

  1. ✅ 识别问题:runtime.hmap.buckets 字段不存在
  2. ✅ 源码分析:理解 Swiss Tables 结构
  3. ✅ 实现适配:新的 header 注入函数
  4. ✅ 版本路由:自动选择正确的实现
  5. ✅ 调试支持:详细的日志输出

代码已实现并可以编译,下一步需要实际测试验证功能是否正常工作。