# 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) ### 代码对比 ```c // 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` ```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` ```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 系统) ```go // 尝试获取 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 格式 **实现逻辑**: ```c 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 注入函数都添加了版本检测 ```c // 在 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 系统上: - `uint64`、`uintptr`、指针: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 日志: ```bash # 方法 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](https://abseil.io/about/design/swisstables) - 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. ✅ 调试支持:详细的日志输出 代码已实现并可以编译,下一步需要实际测试验证功能是否正常工作。