GitHub user lizining1231 closed the discussion with a comment: [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. **规划内的回归测试**(约 14 个):
- **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 不一致;
### 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` 回退链,语义与慢路径等价。
## 八、参考链接
- [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`;行号随版本漂移,不锁定)
- 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]