RawConn 高速读写
Conn 的 net 风格很好用,但为「保留消息边界 + 流控 + 易用」付出的固定开销,
让它的吞吐封顶在远低于网卡线速的水平。RawConn 把这些都剥掉,直接暴露
「注册内存 + 投递 WR + 自己轮询 CQ」,让你以贴近 perftest 的速度收发。
为什么需要它
Conn 把每条消息封帧、做信用流控、经 bounce buffer 拷贝,再通过一个后台 poller goroutine
把完成事件交回应用 goroutine。这些都是它语义的必需品,但叠加起来限制了吞吐——在 64KB 大包上实测约
20+ Gb/s,远没喂满高速网卡。
RawConn 面向「就是要极限吞吐」的场景:同一个 goroutine 里 post + busy-poll,
无封帧、无流控、无 handoff。实测同一张 400G 网卡上,64KB 大包吞吐能到
232 Gb/s,是 Conn 的约 10 倍。对比同机的底层 perftest 工具:
Go 版 go_send_bw 峰值可达 ~392 Gb/s(追平 C 版 ib_send_bw);
RawConn 目前低一截,但差距随机器状态在 1.05×~1.76× 间波动、某些轮次几乎持平——
瓶颈落在完成到达(poll)而非 cgo 提交,详见
CGO 开销与原生 RDMA 的差距。
RawConn 把易用性换成了速度,所以它不替你做这些:不保留消息边界(一次 Send 就是一个工作请求)、
不做流控(你得自己保证 in-flight 的 Send 不超过对端已投递的 Recv,否则 RNR)、不托管缓冲区(自己注册 MR、构造 SGE)。
三种角色的 API
| 类别 | 方法 | 说明 |
|---|---|---|
| 建连 | DialRaw / ListenRaw / Accept | 走 TCP 带外握手(交换对端 RKey/地址,故支持单边 Write/Read) |
| 内存 | RegisterMemory(size) | 注册一块本地+远端可读写的 MR |
| 原语 | PostSend / PostRecv / Poll | 薄包装,零额外开销 |
| 批量 | PostSendBatch / PipelineBatch | 一次 cgo 调用提交多个 WR,提升消息率 |
| 吞吐循环 | Pipeline / RecvDrain | 内置的主动/被动流水线,开箱即用 |
| 逃生舱 | QP() / CQ() / PD() / PeerRKey() / PeerAddr() | 需要时下沉到底层 gordma |
发送方:内置流水线
最省事的用法是 PipelineBatch:保持 txDepth 个 signaled WR in-flight,每个完成就补一个,
直到发满 iters 条。这正是 perftest 打满线速的那套循环:
rc, _ := rdmanet.DialRaw(addr,
rdmanet.WithDevice("mlx5_1"),
rdmanet.WithGIDIndex(3),
rdmanet.WithQueueDepth(128))
defer rc.Close()
mr, _ := rc.RegisterMemory(size * txDepth)
defer mr.Close()
rc.PipelineBatch(iters, txDepth, func(wrID uint64) gordma.SendWR {
slot := int(wrID) % txDepth
return gordma.SendWR{
WRID: wrID,
Opcode: gordma.OpSend,
SGList: []gordma.SGE{gordma.SGEFromMR(mr, slot*size, size)},
Signaled: true,
}
})
接收方:RecvDrain
被动端(Send/Recv 的接收侧)用 RecvDrain:预投 txDepth 个 recv,收到一个补一个:
rc, _ := ln.Accept()
mr, _ := rc.RegisterMemory(size * txDepth)
rc.RecvDrain(iters, txDepth, func(wrID uint64) gordma.RecvWR {
slot := int(wrID) % txDepth
return gordma.RecvWR{WRID: wrID, SGList: []gordma.SGE{gordma.SGEFromMR(mr, slot*size, size)}}
})
单边 Write / Read
因为 RawConn 走 TCP 握手,对端的 RKey 和远端地址已经交换好,可以直接做单边操作——
在 WR 里换 Opcode、填 PeerAddr()/PeerRKey() 即可,对端 CPU 不参与:
rc.PostSend(gordma.SendWR{
WRID: 1,
Opcode: gordma.OpWrite, // 或 OpRead
SGList: []gordma.SGE{gordma.SGEFromMR(mr, 0, size)},
RemoteAddr: rc.PeerAddr(),
RKey: rc.PeerRKey(),
Signaled: true,
})
批量提交:PostSendBatch
每次 PostSend 是一次 Go→C 的 cgo 跨界。小包高频时,这个固定开销会成为消息率瓶颈。
PostSendBatch 用 C 侧的 WR 链表把 N 个工作请求一次 ibv_post_send 提交,
把 cgo 边界从「每 WR 一次」降到「每批一次」。实测 1KB 小包消息率因此提升约一个数量级。
PostSendBatch 只接受快路径形态:每个 WR 单 SGE、统一 opcode(SEND/WRITE/READ)、
无 inline/UD/立即数。更复杂的请求用逐个 PostSend。
怎么选:Conn vs RawConn
Conn | RawConn | |
|---|---|---|
| 消息边界 | 保留(SendMsg=RecvMsg) | 不保留(你自己分帧) |
| 流控/背压 | 内置信用流控 | 无,自己控制 in-flight 数 |
| 缓冲区 | 库托管(bounce) | 自己注册 MR |
| 大消息分片 | 透明 | 自己处理 |
| 吞吐(64KB) | ~20+ Gb/s | ~232 Gb/s |
| 定位 | 写业务、要 net 风格 | 压测 / 极限吞吐数据面 |
一句话:先用 Conn,确实需要榨干网卡时再上 RawConn。两者都基于同一套底层
gordma 资源,随时可以下沉到 QP()/CQ() 自己驱动。