惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog
博客园 - 三生石上(FineUI控件)
博客园 - 【当耐特】
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
宝玉的分享
宝玉的分享
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
爱范儿
爱范儿
小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
S
SegmentFault 最新的问题
博客园 - Franky
博客园_首页
T
Tailwind CSS Blog
雷峰网
雷峰网
罗磊的独立博客

鸟窝

鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 LLM 究竟是如何工作的? Go 实验特性详解
Loop Engineering 实践:我把 RDMA 开发库移植到 Go 语言,花...
smallnest · 2026-06-17 · via 鸟窝

一次几乎全自动的库开发实验:从一份 PRD 出发,15 个 issue 串成流水线,让 Agent 一路 实现 → 审查 → 记录 → 发布,最后我只在真机上验证。本文复盘整个过程,验证了Loop Engineering和实际的花费。

0. 缘起

我想要一个 Go 语言的 RDMA 库。

从去年我们做高性能网络的黑盒监控起,就开始尝试用 RDMA 做探测。但我们的技术栈是 Go,找了几个库,实现得不好也不稳定;换成 C 语言技术栈对团队同学来说成本太高;自己实现当时觉得挺有挑战,于是这件事就搁下了,最后还是退回到用普通 UDP 协议探测。

RDMA for Go 库市面上的选择不多:要么是某个公司内部、绑定很深的封装,要么是年久失修的 binding。而我要的东西很明确——地道的 Go API,封装 libibverbs + librdmacm(也就是 rdma-core 的用户态库),支持 RC/UD 两种传输、Send/Recv 和 RDMA Read/Write,再配一套对标 perftestib_send_bw/latib_write_bw/latib_read_bw/lat 的 Go 版工具。

今年不同了,AI Coding 技术飞速发展,我也有信心去做移植这件事。尤其本周 Loop Engineering 这种工作方式大家讨论得很热烈,我也在自己的 goal workflow 里加了个 loop-it 技能,实现了一个轻量级的 Loop Engineering,正好拿来试试手。

还有,大家普遍担心 Loop Engineering 实践起来 token 花费太高,我也想实际看看到底花费几何。

移植RDMA到Go生态圈是个典型的「工作量不小、但每一步都不算难」的活儿。cgo 封装 verbs 是体力活:几十个 C 结构体、状态机、字节序、资源生命周期,错一个字段编译就挂。正是这种结构清晰、可分解、可验证的任务,最适合拿来做一次 Loop Engineering 实验。

所谓 Loop Engineering,核心就一句话:不要让 Agent 一口气写完一个大项目,而是把工作拆成带依赖的小单元,让它在一个可恢复、可观测的循环里逐个吃掉,每一步都有验证关卡。

这是我的锅,实现的loop-it有问题。后来在执行过程中我发现了这个问题,修复了 loop-it


4. 补救:真正的审查发现了什么

最后我还是做了补救,让 CC「现在跑一次真正的审查」。这次 CC 调起了能用的 /code-review(high effort:多个独立角度并行找问题,再逐条对抗式验证),对已合并的全部代码做了一轮真正的 correctness 审查。

结果很扎心——8 个确认/可信的问题,其中两个是致命的编译错误:

  1. cq_linux.goc.imm_data undefined
    struct ibv_wcimm_data 在一个匿名 union 里,cgo 根本无法用 .imm_data 访问;而且它是 __be32(网络字节序)。

  2. device_linux.goibv_query_port 类型不匹配
    现代 rdma-core 把 ibv_query_port 做成 static inline,转发到一个收 _compat_ibv_port_attr* 的真实符号,cgo 直接调会类型不符。

还有 6 个运行时/逻辑问题,例如:

  1. rdma_cm 路径的 Endpoint.Peer 永远没赋值 → 所有走 -R 的 Write/Read 都会立刻报 errNoPeer
  2. write.go 的 busy-wait 没有内存屏障for buf[last] != expect {} 读的是 NIC 通过 RDMA 写入、Go 运行时看不见的内存,编译器可能把这次 load 提到循环外,造成死循环或读到陈旧值。
  3. imm_data 发送侧字节序 → 没 htonl,对端会读到字节翻转的立即数。
  4. rdma_cm 错误路径资源泄漏errno 读取时机不对setup 忽略了 -c/-d/-i/-x 参数……

最刺眼的事实是:这两个编译错误意味着,整个 cgo 核心从来没在 Linux 上编译过。我全程在 macOS 上开发,走的是 stub 路径,go build 一路绿灯,但那只证明了「桩实现能编译」,完全没碰到真正的 verbs 代码。


5. 修复阶段:一步步把真相逼出来

接下来的过程,本质上是让真实环境替我做验证。这部分非常有意思,因为它展示了 Agent 的盲区如何被一台真机一点点照亮。

我还是做了一点点额外的工作,以下是我手工触发的。

第一步,质量清理(/simplify)。4 个角度并行审查:reuse / simplification / efficiency / altitude。修了几处真问题:

  • stats.go 的延迟统计,原来每打印一行 summary 要 sort 3 次(Min/Max/Percentile 各自 sort 一遍),直方图路径甚至 5 次。改成 Stats() 一次排序算出 min/avg/max/p99。
  • 带宽测试的「保持 TxDepth 个请求在途」的循环,在 send/write/read 里三份几乎一样的拷贝 → 抽成一个 runBWPipeline(cfg, cq, post),用闭包传 opcode。
  • 4 个工具 main 里重复的 -R/UD 拒绝逻辑 → 收敛成一个 Config.RequireOneSidedTCP()
  • 删掉过时的 var _ = C.IBV_QPT_RC cgo 占位 anchor。

第二步,工程化。加了 Makefilevet/build/test/tools/cross/stub/integration/lint/fmt,硬件相关的 integrationGORDMA_HW=1 闸住),加了 make fmtmake lint

make lint 一跑,golangci-lint 报了 20 个问题:16 个 errcheck(没检查的 defer Close() 返回值等)、2 个 unused、以及 2 个 staticcheck SA5002,正是 write.go 那个 busy-wait race。静态分析工具独立地撞上了 /code-review 早就指出的内存模型 bug。

这里有个小插曲值得一记:修这个 race 时我第一反应是用 atomic.LoadUint8——结果 Go 根本没有 8 位原子操作,编译直接挂。最后改成:定位包含该字节的 4 字节对齐 word,用 atomic.LoadUint32 + CompareAndSwapUint32 只改其中一个字节(代价是要求 Size >= 4)。Agent 也会想当然,差别只在于有没有一个会立刻打脸的编译器。

第三步,真机连环打脸。用户把代码弄到一台真正的 H20 GPU 服务器上(装了 RDMA 网卡),开始跑 make

  • 第一次:fatal error: rdma/rdma_cma.h: No such file or directory,缺 librdmacm-dev。换成我的开发 docker,上面已经装好了相应依赖库。
  • 第二次:c.imm_data undefined + ibv_query_port 类型不匹配,就是 /code-review 预言的那两个编译错误,这下在真机上真实地复现了。CC 按 libibverbs 的正确用法修:给 imm_data 写 C 辅助函数 wc_imm_data()ntohl 转主机序);给 ibv_query_port 包一层 C wrapper gordma_query_port() 让 inline 在 C 里展开;发送侧 imm_datahtonl
  • 第三次:device_linux.go:67: possible misuse of unsafe.Pointergo vet 嫌弃把指针存进 uintptr 再做算术。改用 unsafe.Add,让指针运算全程保持 unsafe.Pointer 形式。

因为这些都没进 Loop,所以不适合放在循环里,而是在循环之外执行的。起始后续/simplify 也可以加在循环中。


6. 完整的提交时间线

如果把 16 个功能 PR 之后的修复也算上,整条提交线是这样的(节选):

1
2
3
4
5
6
7
8
edd44e2 docs: add badges to README
4fa8b9c fix: use unsafe.Add to walk device list (go vet unsafe.Pointer) ← 真机第3次
f0b0453 fix: cgo compile errors (imm_data, ibv_query_port) + imm byte order ← 真机第2次
d253e36 Chore/simplify cleanups (#31) ← 质量清理+工程化
f2519af docs: README usage, godoc, and CI workflow (#15) ← loop-it 最后一个 issue
...
d15513b 项目骨架:go.mod、cgo 构建配置、非 Linux stub (#1) ← loop-it 第一个 issue
56514ff Initial commit

#1#15 是 loop-it 自动跑出来的,干净利落。d253e36 之后的几个 fix,则是人把 Agent 拽回现实的痕迹。


7. 成本:那 239 块钱

说说标题里的钱。

我一直盯着Claude Code中钱的消耗,最后算下来一共花了 239 元。239 这个数字是按整轮交互(一份 PRD、15 个 issue 的实现、一轮 high-effort code-review、一轮 4 角度 simplify,外加多轮真机修复往返)的量级估的。我不想在这篇复盘里编一张逐项发票出来——那本身就违背了全文的主旨。

但即便按这个量级看,几个判断是成立的:

  • 这是「一个有经验的工程师几天的活儿」:cgo 封装整套 verbs、写 6 个 perftest 工具、跨平台 stub、CI、文档。按工时折算,几百块的 API 成本对应的是数千块的人力成本。
  • 真正贵的不是「写」,是「来回」。第一遍实现其实很快、很便宜,大约100块。烧钱的是后面那些本可以避免的往返——如果一开始就在有网卡的 Linux 上开发,那两个编译错误根本不会漏到合并之后,也不会有「真机连环打脸」的三轮修复。
  • 跳过审查省下的钱,会在后面加倍还回来。loop-it 静默降级掉的 /review-it,最终是用一轮独立的 /code-review + 多轮真机调试补回来的。省一步,赔三步。。

附:项目信息

  • 仓库:github.com/smallnest/gordma
  • 规模:52 个 Go 文件 / ~3981 行
  • 能力:Device/Context/PD/MR/CQ/QP/AH 全套 verbs;RC + UD;TCP 握手 + rdma_cm 两种建连;6 个 perftest 风格工具
  • 构建:Linux+cgo 真实实现,非 Linux/CGO_ENABLED=0 stub 实现
  • 状态:cgo 编译错误已在真机修复;硬件数据通路的端到端验证已在 RoCE v2 真机上验证