lizining1231 opened a new issue, #3750: URL: https://github.com/apache/dubbo-go/issues/3750
### 摘要 一次 RPC 除了业务消息,还会携带链路追踪标识、请求编号、租户信息等 metadata。Triple 客户端在发送前,将 context Header 与 invocation attachment 合并为请求使用的 Header。这一步会在每次调用中重复执行。 目前合并路径存在两类可减少的复制:字段先复制到临时 Header,再复制到最终 context;旧 outgoing Header 随 extra data 被复制后,又被新 Header 替换。 | 业务字段数量 | B/op | allocs/op | | --- | ---: | ---: | | 0 | 1,248 | 15 | | 4 | 1,904 | 33 | | 16 | 7,040 | 91 | 业务字段从 4 个增加到 16 个,每次合并多分配约 5 KiB,增加 58 次分配。四字段、128 B 小报文的独立客户端 profile 中,这条合并调用栈占客户端累计分配的 9.70%,值得作为分配优化点处理。 issue以减少 `B/op`、`allocs/op` 为主要目标,分别通过局部 benchmark 和完整客户端 RPC 评估改动。 ### 相关代码 #### 合并阶段创建中间副本 [protocol/triple/triple_invoker.go:185-212](https://github.com/apache/dubbo-go/blob/develop/protocol/triple/triple_invoker.go#L185-L212) ```go // 旧 Header 的 map 和值切片先被复制到临时 Header。 header := cloneOutgoingHeader(tri.ExtractFromOutgoingContext(ctx)) // 多值 attachment 也会创建临时值切片。 header[lowerKey] = append([]string(nil), strs...) // 临时 Header 的值随后再次复制到最终 context。 return tri.NewOutgoingContext(ctx, header), nil ``` 这里摘录三个相关步骤。同名 attachment 会覆盖临时 Header 中的旧值,旧字段即使随后被替换,也已经发生一次值切片复制。 #### context 构造复制旧组后再替换 [protocol/triple/triple_protocol/header.go:132-160](https://github.com/apache/dubbo-go/blob/develop/protocol/triple/triple_protocol/header.go#L132-L160) ```go for key, vals := range data { // Context headers use canonical keys so http.Header.Get/Values work as expected. header[http.CanonicalHeaderKey(key)] = append([]string(nil), vals...) } // 已有 extra data 时,所有组都会被复制,包括旧 outgoing。 extraData = cloneExtraData(extraData) // 刚复制的旧 outgoing 在这里被新 Header 替换。 extraData[headerOutgoingKey] = header ``` 最终 Header 的复制用于隔离调用方输入,应保留。 可以减少的是最终保存之前的中间值切片,以及随后被替换的旧组副本。 ### 建议优化 1. **延后值切片复制**:合并阶段仍创建独立临时 map,旧 Header 值和多值 attachment 暂时作为只读输入。完成同名覆盖后,由 `NewOutgoingContext` 复制最终保留的值切片。 2. **跳过被替换组**:给内部 `cloneExtraData` 增加替换目标参数,复制时跳过该组。outgoing、incoming context 构造分别传入自己的组名,其他组继续深拷贝。 3. **单独评估键名规范化**:先保持现有小写转换与 `CanonicalHeaderKey` 的顺序;只有在验证大小写、无效键名及 `Get/Values` 行为一致后,再考虑减少重复处理。 前两项可以减少中间切片、旧组 map 与值切片的分配 ### 防御性设计与相关约束 设计约束 - **旧数据隔离**:临时 map 独立创建;共享输入切片期间只读取,不修改元素、不追加值。最终 context 对保留字段复制,合并后修改输入或结果各自独立。 - **覆盖与多值**:旧 Header 先写入,attachment 后覆盖整组值;保留多值数量、重复值及顺序。 - **键名处理**:沿用现有大小写转换与无效键名处理。原 map 中的大小写等价键仍按现有遍历规则覆盖,显式 attachment 最后覆盖。 - **错误与专用字段**:继续接受 `string`、`[]string`,保留其他类型的错误格式。timeout 先写入专用 context,`traceparent` 按原字段规则处理;错误路径保持旧 Header 原值。 - **context 各组**:仅跳过指定的被替换组,其他组保持 map 和值切片独立。覆盖空数据、缺少目标组、两组并存及非预期 extra data 类型。 - **空值与追加**:保留 nil Header、nil/空切片及空字符串的既有行为。公开 `AppendToOutgoingContext` 的追加规则和奇数参数 panic 保持原样。 - **并发**:支持多个调用只读复用同一输入 context,各次合并结果独立;使用 `-race` 检查共享写入。 需要补充的单元测试 | 测试 | 功能 | | --- | --- | | 合并结果的反向修改与切片追加 | 为旧 Header 和多值 attachment 的切片预留额外容量。合并后修改结果中的值元素、追加值,确认输入保持原值;再向输入追加值,确认已保存的结果不变,防止延后复制留下共享底层数组。 | | 多值覆盖与重复值保留 | 用包含重复项的多值 attachment 替换旧 Header 的多个值,确认旧值全部移除,新值的数量、重复项和顺序保持一致。 | | 类型错误路径的数据隔离 | 在已有 Header 的 context 上提供合法字段、timeout 和非法 attachment,覆盖 nil 接口、整数及非字符串切片。确认错误包含对应键与类型要求,旧 Header 保持原值,返回 context 保留先写入的 timeout;不固定多个非法键的报错先后。 | | 空 Header 与零长度值切片 | 覆盖 nil/空 Header、nil 值切片、非 nil 空切片以及类型为 `[]string` 的 nil attachment。确认结果可提取、对应键按原规则保留,零长度值复制后的 nil 状态与原 `append` 行为一致。 | | 非标准键名的转换 | 输入空键、带空格和无效字符的键,检查最终 map 键名及 `Get/Values` 结果符合原小写转换和 `CanonicalHeaderKey` 规则,防止简化规范化时改变行为。 | | 原 Header 中的大小写等价键 | 在提取出的 Header 中直接加入两个大小写等价键。无 attachment 时允许原遍历规则产生的结果;有同名 attachment 时确认最终由 attachment 覆盖。 | | 辅助复制的跳过目标与额外组 | 表驱动覆盖空 map、目标组不存在、只有目标组、多个组、额外组及 nil Header 组。确认仅跳过指定组,其他组复制为独立 map 和值切片,修改结果不影响输入。 | | nil 和非预期 extra data | 使用类型正确但为 nil 的 extra data map,以及内部 context 键下的非预期类型;分别构造 incoming、outgoing context,确认按原规则建立可写的新 Header,提取结果正确。 | | 空数据替换已有 Header | 分别用 nil/空 Header 替换已有 incoming、outgoing,确认旧目标组字段被清空,另一组仍保留且与旧 context 独立,防止跳过复制后误保留旧组。 | | 新旧 context 的独立追加 | 对合并前后的 context 分别追加同名值,确认追加仅影响被操作的一侧,多值顺序正确;零参数调用保留原数据。 | 现有测试继续作为回归检查:`Test_mergeAttachmentToOutgoing` 已覆盖普通类型、多值顺序、混合大小写覆盖、空字符串、timeout 和 traceparent;`CopiesHeaderValues`、`DoesNotMutatePreviousContext` 与 `ConcurrentContextReuse` 已覆盖输入修改隔离、连续调用和并发复用。context 两组的替换隔离、请求 Header 追加以及奇数参数 panic 也已有测试,以上清单只补充它们尚未覆盖的边界。 ### 复现方式 ```bash go tool pprof -peek=mergeAttachmentToOutgoing client-cpu.pb.gz go tool pprof -no_browser -http=127.0.0.1:8110 client-cpu.pb.gz go tool pprof -sample_index=alloc_space -peek=mergeAttachmentToOutgoing client-alloc.pb.gz go tool pprof -no_browser -sample_index=alloc_space -http=127.0.0.1:8111 client-alloc.pb.gz ``` ### 参考链接 - [grpc-go metadata 分配问题 #8860](https://github.com/grpc/grpc-go/issues/8860):同类 metadata 读取分配问题及函数级 benchmark。 - [grpc-go 相关改进 PR #9282](https://github.com/grpc/grpc-go/pull/9282):按键读取 metadata 的改进,参考其减少不必要复制的思路。 - [grpc-go metadata 用法](https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-metadata.md):追加、替换及 context API 语义。 - [Go HTTP Header 键名处理](https://pkg.go.dev/net/http#CanonicalHeaderKey):标准和无效键名的规范化规则。 -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
