GitHub user lizining1231 edited a comment on the discussion: [OSPP 2026] Triple 
协议性能分析与优化

step2.2
# 性能瓶颈分析报告(二)Triple-unary marshal 内存分配专项

## 目录

一、摘要

二、数据分析

三、代码瓶颈定位

四、瓶颈验证

五、方案设计

六、方案验证

七、预估收益与方案边界

八、参考链接

## 一、摘要

本文针对 Triple unary 发送热路径上的**结构性 per-request 分配税**,即”每次请求 `codec.Marshal` 新分配一块 
≈L 的输出切片、再 `bytes.NewBuffer` 包一层、再全量拷进传输缓冲“做分析。

从分配数据定位到该瓶颈,用pprof等工具进行验证,设计"池 + `MarshalAppend`"零分配方案——给 `Codec` 增加可选接口 
`marshalAppender`,`envelopeWriter` / `tripleUnaryMarshaler` 从既有 `bufferPool` 取 
`*bytes.Buffer` 借底层数组做追加目标,未命中(hessian2/msgpack/json/自定义 
codec)自动回退旧路径,行为零变化。预估收益:B/op 大包 **−10%\~−25%**、allocs/op 端到端 −2\~−6。

## 二、数据分析

这次优化的重点内容是内存分配,所以这边主要采用go test -bench进行测试

### 2.1 分配税与消息大小的关系(从实测分配占比看)

**定向 marshal bench 的分配绝对量(优化前,每消息)**:

| 消息    | marshal 层 B/op(优化点 A+B) | 端到端 B/op(fast path 生产入口) | marshal 层占比 |
| ----- | ----------------------- | ------------------------ | ----------- |
| 128B  | ≈216                    | ≈26.6KB                  | **<1%**     |
| 1KiB  | ≈1223                   | ≈32.1KB                  | \~3.8%      |
| 16KiB | ≈18505                  | ≈143.5KB                 | **\~12.9%** |
| 1MiB  | ≈1056870                | ≈6.53MB                  | **\~16.2%** |

一些规律:

1. **分配量随消息增大**:`codec.Marshal` 的输出切片大小 ≈ 消息字节数(按 size class 
取整),消息越大,单次分配越多,这是大包场景收益更明显的原因。
2. **小报文分配税占比不足 5%**:128B/1KiB 段,marshal 层 B/op 占总 B/op 的比例被 http2/TLS/context 
等固定开销淹没,**端到端 B/op 收益不可见**,只能从定向 bench 看。
3. **大报文分配税成为主导项**:≥16KiB 段 marshal 层占端到端 B/op 的 13%\~16%,是端到端 B/op 
中最大的单一项,**收益预估在端到端可见**。

所以这次优化主要针对的是分配税,大小包都会收益,大包下会更明显,小包相对收益不会明显。

### 2.2 相对 gRPC 的额外开销

**gRPC-Go 的** **`encoding.Codec.Marshal`** **也是每次** **`proto.Marshal`** 
**新分配的**,但区别在发送路径:

| 开销项                  | dubbo-go(Triple,优化前)   | gRPC-Go                    |
| -------------------- | ---------------------- | -------------------------- |
| `codec.Marshal` 新分配  | 每次(≈L)                 | 每次(≈L,相同)                  |
| `bytes.NewBuffer` 包装 | 每次(≈40B 小分配)           | 无(直接以 \[]byte 交 transport) |
| marshal 输出复用         | 仅包装对象进池,底层 \[]byte 不复用 | 无(每次 `proto.Marshal` 新分配)  |

### 2.3 瓶颈定位

本次要优化的点有两个:优化点 A是`codec.Marshal` 每次新分配 ≈L 输出切片并全程清零的开销
优化点 B是`bytes.NewBuffer(raw)` 每次 ≈40B 逃逸包装的开销。下表按 1MiB 档列出两者的每次开销、gRPC-Go 及上游 
connect-go是否有此开销。

<br />

| 优化点                              | 每次开销(1MiB 档)                               
                    | gRPC-Go 是否有                                  | 上游 
connect-go 是否已消除                              |
| -------------------------------- | 
-------------------------------------------------------------- | 
-------------------------------------------- | 
------------------------------------------------ |
| `codec.Marshal` 输出切片(优化点 A)      | 每次 unary **新分配 ≈L**(1MiB → ≈1,048,576 B)+ 
全程清零(memclr ≈53µs/次) | **有**:`proto.Marshal` 同样每次新分配 ≈L + 清零,无任何复用  | 
**已消除**:`marshalAppend` 把结果追加进池 buffer,0 分配 0 清零 |
| `bytes.NewBuffer(raw)` 包装(优化点 B) | 每次 unary **≈40B 小分配**(`*bytes.Buffer` 
逃逸到堆),与消息大小无关            | **无**:marshal 输出 `[]byte` 直接交 transport,不做包装 | 
**已消除**:envelopeWriter 用池内 buffer 包装,无逃逸小分配      |

## 三、代码瓶颈定位

### 3.1 `codec.Marshal` 每次新分配(优化点 A)

[protocol/triple/triple\_protocol/codec.go#L111-L117](https://github.com/apache/dubbo-go/blob/develop/protocol/triple/triple_protocol/codec.go#L111-L117):

```go
func (c *protoBinaryCodec) Marshal(message any) ([]byte, error) {
    protoMessage, ok := message.(proto.Message)
    ...
    return proto.Marshal(protoMessage) // proto.Marshal 内部 o.marshal(nil, 
...):以 nil 开头,必然分配
}
```

`proto.Marshal` 恒以 nil 起步,输出切片只能新分配(大小 ≈L,按 size class)。

### 3.2 包装 + 传输写拷贝(优化点 B)

[protocol/triple/triple\_protocol/envelope.go#L69-L95](https://github.com/apache/dubbo-go/blob/develop/protocol/triple/triple_protocol/envelope.go#L69-L95)(gRPC
 wire)与 
[protocol/triple/triple\_protocol/protocol\_triple.go#L504-L531](https://github.com/apache/dubbo-go/blob/develop/protocol/triple/triple_protocol/protocol_triple.go#L504-L531)(Triple
 wire 慢路径)结构相同:

```go
raw, err := w.codec.Marshal(message)  // ① 优化点 A:全新 []byte,B/op≈L
buffer := bytes.NewBuffer(raw)        // ② 优化点 B:*bytes.Buffer 对象(≈40B,逃逸)
defer w.bufferPool.Put(buffer)        //    归还的只是包装对象;底层 []byte 每次新分配,不随包装进池复用
envelope := &envelope{Data: buffer}
return w.Write(envelope)              //    └── 一次写拷贝:Data 全量拷进 writer(io.Copy)
```

## 四、验证瓶颈

所有 profile 均用**固定迭代次数**(`-benchtime=Nx`)或**按事件采样**(memprofile),保证 per-op 
可比、不被吞吐污染。

### 4.1 内存分配 profile(memprofile,按事件采样)

`-memprofile -memprofilerate=1 -benchtime=3000x`(固定 3000 次),优化前 1MiB unary 
marshal:

<img width="1919" height="952" alt="image" 
src="https://github.com/user-attachments/assets/8ea222b0-1042-4885-be97-e5f0083720a1";
 />


可以观察到:

- **分配几乎 100% 集中在** **`proto.MarshalOptions.marshal`**,调用链 
`tripleUnaryMarshaler.Marshal → protoBinaryCodec.Marshal → proto.Marshal → 
MarshalOptions.marshal`;
- 3000 次固定迭代累计分配 ≈3.1GB,其中 `MarshalOptions.marshal` 占 **99.92%**;
- 火焰图上的巨型分配热点可以印证章节 2.3 优化点 A 是发送路径上唯一的大分配来源。

### 4.2 alloc pprof(累计分配字节对比)

同 3000 次固定迭代,`-sample_index=alloc_space`:

| 指标(3000 次累计)                  | 占比     |
| ----------------------------- | ------ |
| 分配总量                          | ≈3.1GB |
| 热路径分配(MarshalOptions.marshal) | 99.92% |

优化后热路径 0 分配(benchmem allocs 2→0、B/op 1,056,891→353,见交叉验证表);memprofile 累计(3000 
次)仍采到 ≈4.3MB 非热路径分配,来自 bench 进程级采样窗口内的消息构造与池首轮扩容等一次性成本,无法精确拆分 per-op,不作为热路径收益证据

### 4.3 CPU profile(固定迭代 30000x,per-op 可比)

`-benchtime=30000x` 固定两端 op 数,火焰图宽度 = per-op 时间占比:

| 函数                                 | 占比 / per-op      |
| ---------------------------------- | ---------------- |
| `unicode/utf8.ValidString`         | 40.9% / **84µs** |
| `runtime.memmove`                  | 28.1% / **58µs** |
| `runtime.memclrNoHeapPointers`(清零) | 23.5% / **53µs** |

- `ValidString`(UTF-8 校验)per-op 成本两端几乎相同(84 vs 
85µs),是序列化固有成本;它在优化后"占比更大"是因为其他成本消失后成为绝对主体,**不是变慢**;
- **省下的是** **`memclr`** **53µs + 部分** **`memmove`** **34µs ≈ 87µs**,与实测 
206→115µs 的 \~91µs 差距数据相符

### 交叉验证的总结

| 采集内容                           | 测试结果                                         
                    | 结论                    |
| ------------------------------ | 
---------------------------------------------------------------- | 
--------------------- |
| memprofile 分配热点(按事件)           | 分配 99.92% 集中于 
`MarshalOptions.marshal`(≈1MB/次 × 3000 = 3.1GB) | 优化点 A 是唯一大分配来源        |
| alloc pprof(固定 3000 次)         | 累计 3.1GB;热路径占比 99.92%          | 分配字节开销与占比巨大 
           |
| CPU profile(固定 30000 次,per-op) | `memclr` 53µs 、`memmove` 58;`ValidString` 
85µs  | 省下 ≈87µs = 实测延迟差的全部来源 |

结论:**优化核心是去除开销"每次 marshal 新分配 1MiB + 清零"**。

## 五、方案设计

### 5.1 问题定位

源码定位将分配税锁定在 `envelopeWriter.Marshal` / `tripleUnaryMarshaler.Marshal` 每次调用中的 
`codec.Marshal` 新分配(优化点 A)与 `bytes.NewBuffer(raw)` 包装(优化点 B)。两条 wire(gRPC / 
Triple-connect)共用同一批 marshaler 类型,改动一次客户端发送与 gRPC 双端发送同时受益;

### 5.2 设计目标

- **零分配**:稳态(池命中 + cap 足够)下 marshaler 层每消息 0 分配;
- **兼容性**:不应改变任何原有行为,未命中 `marshalAppender` 的 
codec(hessian2/msgpack/json/自定义/generic wrapper)自动回退旧路径,wire 
字节、错误链、压缩/`sendMaxBytes` 判断顺序与阈值完全不变;
- **最小侵入**:用可选接口(type assert)而非改造 `Codec` 接口本身,第三方自定义 codec 编译与运行零影响。
- **protobuf-go 零分配保证**:`MarshalAppend` 直接把调用方 `b` 传给编码器,而 `Marshal` 恒以 nil 
起步;cap 判定在 lib 层 `MarshalOptions` 门控(生成代码提供 `Size` 与 append 编码函数、`sizecache` 缓存 
size),`cap(b)-len(b) >= size` 时跳过 `make+copy` 走纯 append,**零分配条件①:生成消息 + 自备缓冲 
cap 足够**;

### 5.3 方案要点

1. **codec 层**:新增可选接口 `marshalAppender { MarshalAppend(dst []byte, m any) 
([]byte, error) }`,`protoBinaryCodec` 实现为 
`proto.MarshalOptions{}.MarshalAppend(dst, protoMessage)`;
2. **envelopeWriter.Marshal**:开头 type assert,命中则从 `bufferPool.Get()` 取 
`*bytes.Buffer`,以 `buffer.Bytes()` 为 dst 调 `MarshalAppend`;若扩容(`cap(raw) > 
buffer.Cap()`)把新数组整块换入 buffer 进池,否则 `buffer.Write(raw)` 仅修正长度(0 分配);
3. **tripleUnaryMarshaler.Marshal**:同款改造,未压缩分支把 `buffer.Bytes()` 交给 
`m.write`,压缩分支保持 `*bytes.Buffer`。

### 5.4 回滚与防御性设计

本方案通过对逐行改动做防御性 review 收敛出的设计约束。风险等级:中等偏低(可选接口 + 分支 + 天然回退,风险面小于 fast path 
阶段的配置开关方案),以下是一些落地时需要注意的点:

1. **天然回退(零配置零开关)**:`marshalAppender` 是可选接口,任何不实现它的 codec 自动走 `Marshal` 
旧路径,无需配置、无需发布灰度开关即可整体回退——风险面比 fast path 阶段(需 `unary-fast-path` 开关)更小;
2. **pool 不变量(Get 即空、从 0 写入)**:快路径正确性依赖"`bufferPool.Get()` 每次返回 Reset 过的 
buffer、`MarshalAppend(buffer.Bytes())` 恒以 len 0 起步"。该不变量未被 language-level 
强制,一旦未来有人改用未 Reset 的池或在 MarshalAppend 前改动 buffer 长度,`cap(raw) > buffer.Cap()` 
分支与 `buffer.Write(raw)` 的"修正长度"逻辑会静默错乱,需以用例锁定(见回归测试-池不变量);
3. **wire 字节一致性**:`MarshalAppend` 与 `Marshal` 由同一 protobuf 
编码器输出,语义保证相同;仍加"新路径输出 == 旧路径输出"对拍测试锁定,矩阵覆盖空消息/512B 边界/8MiB±1 × 压缩开关 × 双 
wire(见回归测试-wire 对拍);
4. **buffer 归还**:`write()` 内 `io.Copy` 同步消费完才 `defer Put`,buffer 不可能在被读时归还;已有 
fast path 的 transport 恰好一次归还纪律可复用。池内残留旧明文只驻留在数组、不随信封写出(信封只写 
`Data.Len()`),不含线上泄漏,但"读写均在归还前完成"依赖同步语义,不因未来改动破坏;
5. **快路径作用域边界(防静默失效)**:`marshalAppender` 仅 `protoBinaryCodec` 
实现;`protoWrapperCodec`/`hessian2Codec`/`msgpackCodec`/`protoJSONCodec`/`tripleServerCodecSession`
 均不走快路径。**Triple 服务端响应(IDL 与非 IDL 均如此)**:wire codec 为 proto 时被包成 
`tripleServerCodecSession`(protocol\_triple.go#L187-L193),恒不命中快路径,服务端发送恒走慢路径。真实收益面
 = Triple 客户端发送 + gRPC 双端,不主张整体零分配——收益面按此边界声明;
6. **gRPC/grpc-web 回归面**:`envelopeWriter` 同时服务 gRPC 
wire(`protocol_grpc.go`),codec 为 `protoBinaryCodec` 时 gRPC 路径同样命中快路径。wire 
字节等价得以保持,但最易被忽略的回归面是 gRPC 而非 triple,对拍用例须在双 wire 各跑一组;
7. **单级池的内存权衡**:`cap(raw) > buffer.Cap()` 时新数组整块换入池;`maxRecycleBufferSize=8MiB` 
兜底丢弃,但 512B\~8MiB 区间无 size 
分级,一次大消息后大缓冲滞留,会被后续小消息复用,抬高连接常驻内存,属"分配换内存"的既有权衡,经考虑本次方案中不引入分级池;
8. **快慢路径逻辑重复(防漂移)**:慢路径(Marshal)与快路径(MarshalAppend)各自维护 
`sendMaxBytes`/`compressMinBytes`/压缩头判断,将来改动只改一半会造成行为不一致,用契约用例锁定二者对同一输入的错误码与副作用一致;
9. **backupCodec 回退链等价**:快路径先在 `MarshalAppend` 失败、再回退 
`marshalWithFallback(backupCodec,...)`;慢路径先在 `Marshal` 
失败。二者回退触发点不同,但应语义等价(主==backup 不二次回退、无死循环、错误码 `CodeInternal`、backup 为 nil 
直接返回),需用例锁定;
10. **规划内的回归测试**(约 8 个):
    - **wire 字节对拍(最高优先级)**:同消息快/慢路径逐字节断言写给 `io.Writer` 的 socket 输出(含 5 字节 
prefix)一致;矩阵覆盖 `空/1B/511B/512B(边界)/513B/1KiB/8MiB±1` × `压缩关闭/开启(含 
compressMinBytes 边界)` × `triple 与 gRPC 双 wire`;
    - **池不变量与边界**:Get 后 `Len()==0`;`nil` 消息走 `write(nil)` 分支、空 proto 输出零长度信封、恰 
512B 与恰 `compressMinBytes` 边界无 panic;
    - **backupCodec 回退**:主 codec 失败→回退 backup 输出正确;主==backup 不二次回退、无死循环、错误码 
`CodeInternal`;backup 为 nil 直接返回错误;快慢路径各一组;
    - **压缩阈值与 sendMaxBytes 一致**:快慢路径对超限消息均返回 `CodeResourceExhausted`,压缩头 
`tripleUnaryHeaderCompression` 均被设置;
    - **>8MiB 丢弃与残留**:8MiB+1 消息后该缓冲不被复用;大消息后紧接 1B 消息输出不受池内残留影响;
    - **并发安全(`-race`)**:多 goroutine 共享同一 `envelopeWriter`/`bufferPool` 并发 
`Marshal`,无数据竞争、无同一 `*bytes.Buffer` 双归还、输出各自正确;
    - **类型守卫**:断言 wrapper/hessian2/msgpack/json/`tripleServerCodecSession` 不实现 
`marshalAppender`,锁定作用域边界,防未来误给包装 codec 加 `MarshalAppend` 造成 wire 不一致;
    - **快路径 codec 错误守卫**:防止快路径吞错/panic。验证 `MarshalAppend` 收到非 `proto.Message` 
返回 `errNotProto`,快路径(envelopeWriter/tripleUnaryMarshaler)转为 `CodeInternal` 且不 
panic,与慢路径 `Marshal` 行为一致。


### 5.5 与上游 connect-go 的对齐与差异适配

与其v1.20.0对比,分"可直接对齐"与"必须本地化适配"两类:

**与上游相同的点**

1. 
`buffer_pool.go`:本地与上游逐行一致(`initialBufferSize=512`、`maxRecycleBufferSize=8MiB`、Get/Put
 逻辑全同),无改动;
2. `protoBinaryCodec.MarshalAppend`:本地实现 
`proto.MarshalOptions{}.MarshalAppend(dst, protoMessage)` 与上游一致;
3. `envelopeWriter.marshalAppend`:`pool.Get → MarshalAppend(buffer.Bytes()) → 
cap 比较 → 换入或 Write → Write(envelope)` 方法体与上游逐行一致。

**与上游不同的点(本地化适配,不可整段拉取)**

1. `marshalAppender` 接口:上游内嵌 `Codec`(`type marshalAppender interface { Codec; 
MarshalAppend([]byte, any) ... }`),本地未内嵌(功能等价,建议对齐以保持逐行一致);
2. `envelopeWriter.Marshal` 的 appender 分支:本地保留 `backupCodec` fallback(上游无 
backupCodec,慢路径直接 `w.marshal()`);**整段替换上游会丢失 dubbo 特有的 backup codec 兜底语义**,故只对齐 
`marshalAppend` 方法体,分支结构保持本地;
3. `tripleUnaryMarshaler.Marshal`(protocol\_triple.go):上游无此类型(unary 走 
envelopeWriter),为 fast path 阶段自研类型,改造只能本地实现;
4. `protoJSONCodec.MarshalAppend`:上游已实现(JSON 也走零分配),本地未加(可选:加上则 JSON 
场景同样受益,不加行为不变)。

## 六、方案验证与实施

### 6.1 生产落地改动点

方案以**可选接口 + 分支**方式落地,共 3 个文件,均为增量改动:

| 文件                   | 改动                                                     
      | 是否影响非 proto codec |
| -------------------- | 
------------------------------------------------------------ | 
----------------- |
| `codec.go`           | 新增 `marshalAppender` 接口 + 
`protoBinaryCodec.MarshalAppend`   | 否(可选接口)           |
| `envelope.go`        | `envelopeWriter.Marshal` 增加 appender 分支 + 
`marshalAppend` 助手 | 否(type assert 回退) |
| `protocol_triple.go` | `tripleUnaryMarshaler.Marshal` 同款改造 + `marshalAppend` 
助手     | 否(type assert 回退) |

新增 `marshal_perf_bench_test.go` 作为验收证据。改动前后组件职责与调用关系对照:

| 组件                             | 改造前                                          
                                  | 改造后                                         
       | 变化                         |
| ------------------------------ | 
------------------------------------------------------------------------------ 
| -------------------------------------------------- | 
-------------------------- |
| `Codec` 接口                     | 仅 `Marshal`/`Unmarshal`                      
                                  | 不变(`marshalAppender` 为独立可选接口)               
       | 第三方 codec 零影响              |
| `protoBinaryCodec`             | `Marshal` 每次新分配                              
                                  | 新增 `MarshalAppend`(追加到外部缓冲)                 
       | 新增方法,`Marshal` 保留          |
| `envelopeWriter.Marshal`       | `codec.Marshal` → `bytes.NewBuffer` → 
`Write`                                  | 断言 `marshalAppender`,命中走 
`marshalAppend`(池 buffer) | 命中走 appender 分支,未命中自动回退旧路径 |
| `tripleUnaryMarshaler.Marshal` | `codec.Marshal` → `bytes.NewBuffer` → 
`Write`(与 `envelopeWriter.Marshal` 结构相同) | 断言 `marshalAppender`,命中走 
`marshalAppend`(池 buffer) | 命中走 appender 分支,未命中自动回退旧路径 |
| `bufferPool`                   | 归还时仅包装对象进池,底层 \[]byte 不复用                    
                                  | 扩容结果整块进池,稳态复用底层数组                           
       | 复用语义增强                     |

#### 6.1.1 流程图

**改造前(每消息)**:

```mermaid
flowchart TB
    A["Send(msg)"] --> B["codec.Marshal(message)<br/>① 优化点A 全新[]byte ≈L"]
    B --> C["bytes.NewBuffer(raw)<br/>② 优化点B 小分配 ≈40B"]
    C --> D["envelope{Data: buffer}"]
    D --> E["w.Write(envelope)<br/>io.Copy 一次写拷贝"]
    classDef bad fill:#ffcdd2,stroke:#b71c1c;
    class B,C bad;
```

**改造后(稳态:池命中 + cap 足够)**:

```mermaid
flowchart TB
    A2["Send(msg)"] --> B2["bufferPool.Get: *bytes.Buffer"]
    B2 --> C2["MarshalAppend 追加进池 buffer 底层数组<br/>0 alloc"]
    C2 --> D2{"cap 不足?"}
    D2 -- 否 --> E2["envelope{Data: buffer}"]
    D2 -- 是(首轮) --> F2["扩容一次, 结果进池<br/>amortized 趋 0"]
    F2 --> E2
    E2 --> G2["w.Write(envelope)<br/>压缩判断用 Len, 同步消费后归还池"]
    classDef good fill:#c8e6c9,stroke:#1b5e20;
    class B2,C2,D2 good;
```

#### 6.1.3 调用序列对比(同一 unary 请求)

- **改造前**:`Marshal`(新分配 \[]byte)→ `bytes.NewBuffer`(包装)→ `Write`(`io.Copy` 
一次写拷贝)→ 归还包装对象(底层 \[]byte 丢弃)
- **改造后**:`MarshalAppend`(追加进池 buffer,0 alloc)→ `Write`(`io.Copy` 一次写拷贝,次数不变)→ 
消费完归还池;省的是每消息分配 + 清零

调用序列形态不变,仅"分配+包装"收敛为"复用池 buffer",且 `MarshalAppend` 与 `Marshal` 
输出字节一致(同一编码器),wire 不变。

### 6.2 关键代码片段

```go
// codec.go:可选接口 + proto 实现(对齐 connect-go codec.go:108-114)
type marshalAppender interface {
    MarshalAppend(dst []byte, message any) ([]byte, error)
}
func (c *protoBinaryCodec) MarshalAppend(dst []byte, message any) ([]byte, 
error) {
    protoMessage, ok := message.(proto.Message)
    if !ok {
        return nil, errNotProto(message)
    }
    return proto.MarshalOptions{}.MarshalAppend(dst, protoMessage)
}
```

```go
// envelope.go:envelopeWriter.Marshal 增加 appender 分支(语义对齐 connect-go 
envelope.go:181-204)
func (w *envelopeWriter) marshalAppend(message any, appender marshalAppender) 
*Error {
    buffer := w.bufferPool.Get()
    defer w.bufferPool.Put(buffer)
    raw, err := appender.MarshalAppend(buffer.Bytes(), message)
    if err != nil {
        return errorf(CodeInternal, "marshal message: %w", err)
    }
    if cap(raw) > buffer.Cap() { // 扩容发生:新数组整块换入池
        *buffer = *bytes.NewBuffer(raw)
    } else { // 未扩容:仅修正长度,0 分配
        buffer.Write(raw)
    }
    envelope := &envelope{Data: buffer}
    return w.Write(envelope)
}
```

`tripleUnaryMarshaler.Marshal` 同款改造(未压缩分支把 `buffer.Bytes()` 交给 `m.write`,压缩分支保持 
`*bytes.Buffer`,判断顺序与阈值不变)。

## 七、预估收益与方案边界

### 7.1 生产中影响因素

方案收益(协议层 marshal 净收益)是优化基点,生产集成后受以下要素影响:

1. **端到端摊薄**(最主要):协议内 A/B 为定向 marshal bench(见章节四)净收益;端到端叠加框架栈/网络/服务端调用链耗时,B/op 
收益保留、延迟收益被摊薄(本方案主打分配而非延迟);
2. **gzip 压缩开关**(指标口径):用户开启压缩时,压缩路径自身临时分配会掩盖 B/op 账面收益;分配与延迟收益不受影响;
3. **业务负载与并发饱和**(收敛):真实调用带业务耗时,marshal 层占比被业务耗时摊薄;纯序列化场景(业务≈0)才接近定向档位;
4. **GC 与长跑**(双向):生产长跑下 bufferPool 复用使 B/op 收益稳定;GC 清池后首轮重建属池化常态,amortized 趋 0;
5. **大报文带宽天花板**(收敛):1MiB 段受服务端带宽限制,延迟收益收敛,但**内存峰值/GC 压力下降是确定收益**(本方案定位为 
allocation hotspot,非 latency bottleneck);
6. **非 proto codec 占比**(口径):hessian2/msgpack/json/自定义 codec 走旧路径无收益,收益面覆盖 
proto(Triple 主力序列化)。

### 7.2 生产预期区间(预测)

| 指标            | 预估区间             | 预估依据                                       
                |
| ------------- | ---------------- | 
---------------------------------------------------------- |
| B/op(1MiB 档)  | **−10% \~ −25%** | 消除每消息 1 次 O(L) 分配 + 包装       |
| B/op(16KiB 档) | −2% \~ −5%       | 分配税占比(\~12.9%)低于 1MiB 档(\~16.2%),放大系数更小    
    |
| B/op(≤1KiB)   | −1% \~ −4%       | 分配税占总 B/op 不足 5%,可能被 http2/TLS/context 
固定开销淹没                |
| allocs/op     | −2 \~ −6         | 端到端被 \~160 固定 alloc 淹没;          |
| 延迟(小包)        | 端到端不可测(被淹没)      | 受固定开销摊薄 |
| 内存峰值 / GC 压力  | 确定下降             | 每请求少一块 L 大小短期垃圾,allocation hotspot 维度的主要收益 
                |

### 7.3 方案的边界

1. `marshalAppender` 只有 `protoBinaryCodec` 实现。hessian2 / msgpack / json / 自定义 
codec **不实现**,type assert 失败自动走旧 `Marshal` 路径。
2. Triple 服务端响应被 `tripleServerCodecSession` 包裹,恒不命中快路径,走慢路径。
3. 收益面 = **Triple 客户端发送 + gRPC 双端**(`envelopeWriter` 同时服务 gRPC wire)。
4. 快路径 `MarshalAppend` 失败时仍走 `backupCodec` 回退链,语义与慢路径等价。
5. 快路径`marshalAppender` 仅 `protoBinaryCodec` 实现,hessian2/msgpack/json/generic 
wrapper 走旧路径,收益面只覆盖 proto;

## 八、参考链接

- [connect-go](https://github.com/connectrpc/connect-go) 上游 `marshalAppender` / 
`marshalAppend` 
实现(本方案对标,v1.20.0):[codec.go#L56-L66](https://github.com/connectrpc/connect-go/blob/v1.20.0/codec.go#L56-L66)、[codec.go#L108-L114](https://github.com/connectrpc/connect-go/blob/v1.20.0/codec.go#L108-L114)、[envelope.go#L150-L153](https://github.com/connectrpc/connect-go/blob/v1.20.0/envelope.go#L150-L153)、[envelope.go#L181-L204](https://github.com/connectrpc/connect-go/blob/v1.20.0/envelope.go#L181-L204)
- 
[google.golang.org/protobuf](https://github.com/protocolbuffers/protobuf-go):[proto/encode.go](https://github.com/protocolbuffers/protobuf-go/blob/master/proto/encode.go)(`MarshalAppend`
 追加语义、cap 门控在 `MarshalOptions` 
层)、[internal/impl/encode.go](https://github.com/protocolbuffers/protobuf-go/blob/master/internal/impl/encode.go)(纯
 append 编码、`sizecache`;行号随版本漂移,不锁定)
生产落地PR 503 https://github.com/connectrpc/connect-go/pull/503
- grpc-go(官方 
v1.64.1):[shared\_buffer\_pool.go#L23-L100](https://github.com/grpc/grpc-go/blob/v1.64.1/shared_buffer_pool.go#L23-L100)(`SharedBufferPool`
 分级池 
16B/256B/4KB/64KB/1MB,**仅用于收包解析**,见文件头部注释)、[encoding/gzip/gzip.go#L38-L100](https://github.com/grpc/grpc-go/blob/v1.64.1/encoding/gzip/gzip.go#L38-L100)(`poolCompressor`/`poolDecompressor`
 
压缩器对象池)、[internal/transport/http\_util.go#L392-L434](https://github.com/grpc/grpc-go/blob/v1.64.1/internal/transport/http_util.go#L392-L434)(`writeBufferPoolMap`
 帧写缓冲池);注意 dubbogo fork v1.42.10 为单级池基线,分级池属 v1.63+ 新增,引用时按版本区分

***



GitHub link: 
https://github.com/apache/dubbo-go/discussions/3673#discussioncomment-18268020

----
This is an automatically sent email for [email protected].
To unsubscribe, please send an email to: 
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to