第二部分 · 7 / 10

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_bwRawConn 目前低一截,但差距随机器状态在 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

ConnRawConn
消息边界保留(SendMsg=RecvMsg不保留(你自己分帧)
流控/背压内置信用流控无,自己控制 in-flight 数
缓冲区库托管(bounce)自己注册 MR
大消息分片透明自己处理
吞吐(64KB)~20+ Gb/s~232 Gb/s
定位写业务、要 net 风格压测 / 极限吞吐数据面

一句话:先用 Conn,确实需要榨干网卡时再上 RawConn。两者都基于同一套底层 gordma 资源,随时可以下沉到 QP()/CQ() 自己驱动。

下一步 RawConn 也走握手建连。下一节系统讲 两种连接方式(rdma_cm 与 TCP 握手)。
gordma 教程