第二部分 · 6 / 10

高层实战:动手写业务

上一节列了 rdmanet 的方法。这一节回答最实际的问题: 「我现在要写自己的程序,第一步该敲什么?」 我们从零搭一个请求/响应(RPC)服务,再给一张「想做 X 用哪个方法」的速查表。

第 0 步:起手式

每个 RDMA 程序都从「判断当前环境是否支持」开始——这样代码在 macOS 上也能编译运行(只是优雅退出),见 跨平台 stub。固定模板:

import (
    "github.com/smallnest/gordma"
    "github.com/smallnest/gordma/rdmanet"
)

if !gordma.Supported() {
    log.Fatal("RDMA not supported here")   // 非 Linux / 无硬件
}
opts := []rdmanet.Option{
    rdmanet.WithDevice("mlx5_1"),       // 选网卡
    rdmanet.WithGIDIndex(3),             // RoCE v2 常用 3
}

第 1 步:服务端循环

和标准库 net 几乎一样:ListenAccept → 处理 → 关闭。RPC 服务端是一个 「收请求 → 处理 → 回响应」的循环,客户端断开时 RecvMsg 返回 io.EOF

func serve(addr string, opts []rdmanet.Option) error {
    ln, err := rdmanet.Listen(addr, opts...)
    if err != nil { return err }
    defer ln.Close()

    for {
        conn, err := ln.Accept()
        if err != nil { return err }
        go handle(conn)             // 每个连接一个 goroutine
    }
}

func handle(conn *rdmanet.Conn) {
    defer conn.Close()
    for {
        req, err := conn.RecvMsg()      // 一条完整请求
        if err == io.EOF { return }   // 客户端关闭,正常结束
        if err != nil { log.Print(err); return }

        resp := process(req)            // 你的业务逻辑
        if err := conn.SendMsg(resp); err != nil {
            log.Print(err); return
        }
    }
}

第 2 步:客户端

func call(addr string, req []byte, opts []rdmanet.Option) ([]byte, error) {
    conn, err := rdmanet.Dial(addr, opts...)
    if err != nil { return nil, err }
    defer conn.Close()

    if err := conn.SendMsg(req); err != nil {
        return nil, err
    }
    return conn.RecvMsg()             // 阻塞等响应
}

就这些——没有 MR、没有 WR、没有 CQ 轮询、没有状态机。rdmanet上一节那一大坨底层代码全收进了这四个方法里。 这个骨架就是 examples/rpc 的核心。

速查:想做 X,用哪个?

我想……参考示例
收发有边界的「消息」SendMsg / RecvMsgecho-msg, rpc
把连接当字节流(配 io.CopyRead / Writeecho-stream, file-transfer
一次发/收多条,提消息率SendBatch / RecvBatchbatch, bench-throughput
从预注册内存零拷贝发AllocBuffer + SendBufferzerocopy
发大消息(自动分片重组)SendMsg(透明处理)large-message
无连接数据报 / 一对多PacketConn + WriteTo/ReadFrompacket, ud-broadcast
带外发现 UD 对端NewRegistry + LookupAddrregistry
全双工并发收发一读 goroutine + 一写 goroutinechat
服务多个客户端Accept 循环 + 每连接一 goroutinemulti-client
不用 rdma_cm,走 TCP 握手WithHandshake()handshake-dial, connection-styles
榨干网卡的极限吞吐RawConn(见下一节)
单边 RDMA Write/Read下沉到底层 gordmaRawConncmd/go_write_bw

几条务必记住的契约

  • 消息边界保留:一次 SendMsg = 一次 RecvMsg,无论多大。别假设它像 TCP 那样可以任意切分。
  • 别混用 ReadRecvMsg:一个 Conn 只选一种接收风格(字节流适配器会缓存半条消息)。
  • 并发契约:一个 Conn 上「一个并发读 + 一个并发写」安全;不要多个 goroutine 同时 SendMsg
  • Close 唤醒阻塞的 RecvMsg(返回 io.EOF),用它做优雅退出。
  • 两端设备要对齐WithDevice/WithGIDIndex 两端必须指向同一张物理网络、同一 RoCE 版本(见 RoCE 与 GID)。

下一步怎么调优

骨架跑通后,性能不够再按需加 Option——不用一开始就堆:

  • 吞吐不够?→ 试 SendBatch / 调 WithQueueDepth(见 性能选项)。
  • 延迟敏感?→ WithPollMode(rdmanet.PollBusy)
  • 大消息频繁?→ 调大 WithBufferSize 减少分片。
  • 还要更快、能接受自己管内存?→ 上 RawConn
下一步 Conn 够写绝大多数业务了。确实要逼近网卡线速时,看下一节 RawConn 高速读写
gordma 教程