IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

Loop Engineering 实践:我把 RDMA 开发库移植到 Go 语言,花费 239 块钱

鸟窝 2026-06-21 18:40:38 累计浏览 15 次
本机暂存
<p>一次几乎全自动的库开发实验:从一份 PRD 出发,15 个 issue 串成流水线,让 Agent 一路 <code>实现 → 审查 → 记录 → 发布</code>,最后我只在真机上验证。本文复盘整个过程,验证了Loop Engineering和实际的花费。</p><h2 id="0-缘起"><a href="#0-缘起" class="headerlink" title="0. 缘起"></a>0. 缘起</h2><p>我想要一个 Go 语言的 RDMA 库。</p><p>从去年我们做高性能网络的黑盒监控起,就开始尝试用 RDMA 做探测。但我们的技术栈是 Go,找了几个库,实现得不好也不稳定;换成 C 语言技术栈对团队同学来说成本太高;自己实现当时觉得挺有挑战,于是这件事就搁下了,最后还是退回到用普通 UDP 协议探测。</p><span id="more"></span><p>RDMA for Go 库市面上的选择不多:要么是某个公司内部、绑定很深的封装,要么是年久失修的 binding。而我要的东西很明确——地道的 Go API,封装 <code>libibverbs</code> + <code>librdmacm</code>(也就是 <a href="https://github.com/linux-rdma/rdma-core">rdma-core</a> 的用户态库),支持 RC&#x2F;UD 两种传输、Send&#x2F;Recv 和 RDMA Read&#x2F;Write,再配一套对标 <a href="https://github.com/linux-rdma/perftest">perftest</a> 的 <code>ib_send_bw/lat</code>、<code>ib_write_bw/lat</code>、<code>ib_read_bw/lat</code> 的 Go 版工具。</p><p>今年不同了,AI Coding 技术飞速发展,我也有信心去做移植这件事。尤其本周 Loop Engineering 这种工作方式大家讨论得很热烈,我也在自己的 <a href="https://goal.rpcx.io/index_cn.html">goal workflow</a> 里加了个 <code>loop-it</code> 技能,实现了一个轻量级的 Loop Engineering,正好拿来试试手。</p><p>还有,大家普遍担心 Loop Engineering 实践起来 token 花费太高,我也想实际看看到底花费几何。</p><p>移植RDMA到Go生态圈是个典型的「工作量不小、但每一步都不算难」的活儿。cgo 封装 verbs 是体力活:几十个 C 结构体、状态机、字节序、资源生命周期,错一个字段编译就挂。正是这种结构清晰、可分解、可验证的任务,最适合拿来做一次 Loop Engineering 实验。</p><p>所谓 Loop Engineering,核心就一句话:不要让 Agent 一口气写完一个大项目,而是把工作拆成带依赖的小单元,让它在一个可恢复、可观测的循环里逐个吃掉,每一步都有验证关卡。</p><p>![image-20260612095602515.png<img src="/2026/06/17/loop-engineering-rdma-port-to-go-239-yuan/image-20260612095602515.png" class=""></p><p>最终结果(仓库 <code>github.com/smallnest/gordma</code>):</p><ul><li><p>52 个 Go 文件,约 3981 行代码</p></li><li><p>16 个 PR(#16–#31),对应 15 个 issue + 1 个清理 PR</p></li><li><p>核心 verbs 层 ~2266 行,perftest 工具 ~1211 行,TCP 握手 ~213 行</p></li><li><p>跨平台:Linux+cgo 真实实现 &#x2F; 非 Linux 的 stub 实现(macOS 上能编译、能跑单测)</p></li></ul><p>下面是它怎么长出来的。</p><hr><h2 id="1-流水线:PRD-→-Issues-→-Loop"><a href="#1-流水线:PRD-→-Issues-→-Loop" class="headerlink" title="1. 流水线:PRD → Issues → Loop"></a>1. 流水线:PRD → Issues → Loop</h2><p>整条链路是这样的:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/prd → /to-issues → /loop-it (→ /goal → /review-it → /note-it → /ship-it) ×N</span><br></pre></td></tr></table></figure><p>![image-20260612091859384.png<img src="/2026/06/17/loop-engineering-rdma-port-to-go-239-yuan/image-20260612091859384.png" class=""></p><ul><li><strong>PRD</strong>:一份需求文档(<code>tasks/prd-rdma-go-library.md</code>),把关键决策钉死:cgo 封装、6 个工具全集、RC+UD、TCP 握手 + rdma_cm 两种建连、目标是 Mellanox&#x2F;NVIDIA 网卡 + RoCE v2、<code>-t tx-depth</code> 默认 128、延迟工具输出直方图 + min&#x2F;avg&#x2F;max&#x2F;p99。其实这些注意事项并不是我提出来的,而是 prd 这个 skill 会主动询问我,提供选项让我澄清。我最开始就一句提示词:<code>把rdma移植到Go语言</code></li><li><strong>拆 issue</strong>:PRD 被拆成 15 个 issue,并标注依赖关系。这个也是 <code>to-issues</code> 自动帮我生成的,我只告诉它帮我拆成小的、能快速实现的 issue。</li><li><strong>loop-it</strong>:一个带「检查点恢复」的自动化循环,逐个 issue 跑完整流水线。![image-20260612092201206.png<img src="/2026/06/17/loop-engineering-rdma-port-to-go-239-yuan/image-20260612092201206.png" class=""></li></ul><p>15 个 issue 的依赖图大致是一棵树:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">#1 项目骨架 (go.mod, cgo, 非 Linux stub)</span><br><span class="line"> └─ #2 设备枚举与 Context</span><br><span class="line"> ├─ #3 PD 分配 + MR 注册 (GC 安全)</span><br><span class="line"> │ └─ #5 RC QP 创建与状态迁移 (INIT/RTR/RTS)</span><br><span class="line"> ├─ #4 CQ 创建、轮询、事件通知</span><br><span class="line"> │ └─ #5 ...</span><br><span class="line"> └─ #14 跨平台 stub 验证</span><br><span class="line"> #5 ─┬─ #6 TCP 带外握手 (交换 QPN/PSN/GID/RKey)</span><br><span class="line"> ├─ #7 UD QP + Address Handle</span><br><span class="line"> └─ #8 Send/Recv + RDMA Read/Write 工作请求</span><br><span class="line"> └─ #9 rdma_cm 建连 (Dial/Listen/Accept)</span><br><span class="line"> #6,#7,#8,#9 ─→ #10 perftest 公共骨架 (参数解析/建连/统计/直方图)</span><br><span class="line"> ├─ #11 go-send_bw / go-send_lat</span><br><span class="line"> ├─ #12 go-write_bw / go-write_lat</span><br><span class="line"> └─ #13 go-read_bw / go-read_lat</span><br><span class="line"> #13,#14 ─→ #15 文档、godoc、CI</span><br></pre></td></tr></table></figure><p>loop-it 对它做拓扑排序,得到处理顺序 <code>1→2→3→4→5→6→7→8→9→14→10→11→12→13→15</code>,然后逐个推进。每个 issue 走同一套子流程:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">分支准备 → /goal 实现 → /review-it 审查 → /note-it 记录 → /ship-it 发布 → 清理 → 写检查点</span><br></pre></td></tr></table></figure><p>每完成一个 issue,状态写进 <code>.loop-state.json</code>(已加进 <code>.gitignore</code>),崩溃了能从检查点恢复。</p><hr><h2 id="2-实际怎么跑的"><a href="#2-实际怎么跑的" class="headerlink" title="2. 实际怎么跑的"></a>2. 实际怎么跑的</h2><p>前置检查先过一遍:<code>gh</code> 认证、git 仓库、工作树干净、远程可达、有没有旧的状态文件。然后初始化 15 个 issue 全为 <code>pending</code>,从 <code>#1</code> 开始。</p><p>每个 issue 的真实节奏(以 <code>#3 PD/MR</code> 为例):</p><ol><li><code>git checkout -b feat/issue-3-pd-mr</code>,写检查点 <code>in_progress</code></li><li>实现 <code>mr_linux.go</code>(cgo 真实实现)+ <code>mr_stub.go</code>(非 Linux 桩)+ <code>mr_test.go</code></li><li><code>go vet</code> &#x2F; <code>go build</code> &#x2F; <code>go test</code> 三连</li><li>commit → push → <code>gh pr create</code> → squash merge → 删分支</li><li>同步 master,写检查点 <code>shipped</code></li></ol><p>15 个 issue 一路绿灯跑完,没有一个失败或阻塞。从设备枚举到 6 个 perftest 工具,再到 CI 和文档,全部 shipped。<br>![image-20260612095723403.png<img src="/2026/06/17/loop-engineering-rdma-port-to-go-239-yuan/image-20260612095723403.png" class=""></p><p>到这里,故事看起来很完美。但完美正是需要警惕的地方。</p><hr><h2 id="3-第一个教训:流水线「跳过了审查」,而我一开始没说"><a href="#3-第一个教训:流水线「跳过了审查」,而我一开始没说" class="headerlink" title="3. 第一个教训:流水线「跳过了审查」,而我一开始没说"></a>3. 第一个教训:流水线「跳过了审查」,而我一开始没说</h2><p>复盘时我发现:「loop-it 中没有调用 review-it 啊?」</p><p>真相是:<code>/goal</code> 是交互式的 UI 斜杠命令,不是 skill。我第一次试图调起 <code>/goal</code> 时就撞上了「这是 UI 命令,不是 skill」的报错。于是 ClaudeCode 自动用手工方式替代了整条流水线:</p><table><thead><tr><th>流水线步骤</th><th>我实际做的</th></tr></thead><tbody><tr><td><code>/goal</code></td><td>自己读 PRD&#x2F;issue,直接写代码</td></tr><tr><td><code>/review-it</code></td><td>用 <code>go vet</code> + <code>go build</code> + <code>go test</code> 替代——这只是编译和单测,不是真正的代码审查</td></tr><tr><td><code>/note-it</code></td><td>完全跳过,没生成实现笔记</td></tr><tr><td><code>/ship-it</code></td><td>手工 git &#x2F; PR &#x2F; squash-merge &#x2F; close 替代</td></tr><tr><td>所以实际上 <code>review-it</code>、<code>note-it</code>、<code>ship-it</code> 这几个 skill 都没真正跑起来,CC 用了替代方法去执行的。</td><td></td></tr></tbody></table><p>![image-20260612095840690.png<img src="/2026/06/17/loop-engineering-rdma-port-to-go-239-yuan/image-20260612095840690.png" class=""></p><p>这是我的锅,实现的<code>loop-it</code>有问题。后来在执行过程中我发现了这个问题,修复了 <code>loop-it</code>。</p><hr><h2 id="4-补救:真正的审查发现了什么"><a href="#4-补救:真正的审查发现了什么" class="headerlink" title="4. 补救:真正的审查发现了什么"></a>4. 补救:真正的审查发现了什么</h2><p>最后我还是做了补救,让 CC「现在跑一次真正的审查」。这次 CC 调起了能用的 <code>/code-review</code>(high effort:多个独立角度并行找问题,再逐条对抗式验证),对已合并的全部代码做了一轮真正的 correctness 审查。</p><p>结果很扎心——8 个确认&#x2F;可信的问题,其中两个是致命的编译错误:</p><ol><li><p><strong><code>cq_linux.go</code>:<code>c.imm_data undefined</code></strong><br><code>struct ibv_wc</code> 里 <code>imm_data</code> 在一个匿名 union 里,cgo 根本无法用 <code>.imm_data</code> 访问;而且它是 <code>__be32</code>(网络字节序)。</p></li><li><p><strong><code>device_linux.go</code>:<code>ibv_query_port</code> 类型不匹配</strong><br>现代 rdma-core 把 <code>ibv_query_port</code> 做成 static inline,转发到一个收 <code>_compat_ibv_port_attr*</code> 的真实符号,cgo 直接调会类型不符。</p></li></ol><p>还有 6 个运行时&#x2F;逻辑问题,例如:</p><ol start="3"><li><strong>rdma_cm 路径的 <code>Endpoint.Peer</code> 永远没赋值</strong> → 所有走 <code>-R</code> 的 Write&#x2F;Read 都会立刻报 <code>errNoPeer</code>。</li><li><strong><code>write.go</code> 的 busy-wait 没有内存屏障</strong> → <code>for buf[last] != expect &#123;&#125;</code> 读的是 NIC 通过 RDMA 写入、Go 运行时看不见的内存,编译器可能把这次 load 提到循环外,造成死循环或读到陈旧值。</li><li><strong><code>imm_data</code> 发送侧字节序</strong> → 没 <code>htonl</code>,对端会读到字节翻转的立即数。</li><li><strong>rdma_cm 错误路径资源泄漏</strong>、<strong>errno 读取时机不对</strong>、<strong>setup 忽略了 <code>-c/-d/-i/-x</code> 参数</strong>……</li></ol><p>最刺眼的事实是:这两个编译错误意味着,整个 cgo 核心从来没在 Linux 上编译过。我全程在 macOS 上开发,走的是 stub 路径,<code>go build</code> 一路绿灯,但那只证明了「桩实现能编译」,完全没碰到真正的 verbs 代码。</p><hr><h2 id="5-修复阶段:一步步把真相逼出来"><a href="#5-修复阶段:一步步把真相逼出来" class="headerlink" title="5. 修复阶段:一步步把真相逼出来"></a>5. 修复阶段:一步步把真相逼出来</h2><p>接下来的过程,本质上是让真实环境替我做验证。这部分非常有意思,因为它展示了 Agent 的盲区如何被一台真机一点点照亮。</p><p>我还是做了一点点额外的工作,以下是我手工触发的。</p><p>第一步,质量清理(<code>/simplify</code>)。4 个角度并行审查:reuse &#x2F; simplification &#x2F; efficiency &#x2F; altitude。修了几处真问题:</p><ul><li><code>stats.go</code> 的延迟统计,原来每打印一行 summary 要 sort 3 次(<code>Min/Max/Percentile</code> 各自 sort 一遍),直方图路径甚至 5 次。改成 <code>Stats()</code> 一次排序算出 min&#x2F;avg&#x2F;max&#x2F;p99。</li><li>带宽测试的「保持 TxDepth 个请求在途」的循环,在 send&#x2F;write&#x2F;read 里三份几乎一样的拷贝 → 抽成一个 <code>runBWPipeline(cfg, cq, post)</code>,用闭包传 opcode。</li><li>4 个工具 main 里重复的 <code>-R</code>&#x2F;UD 拒绝逻辑 → 收敛成一个 <code>Config.RequireOneSidedTCP()</code>。</li><li>删掉过时的 <code>var _ = C.IBV_QPT_RC</code> cgo 占位 anchor。</li></ul><p>第二步,工程化。加了 <code>Makefile</code>(<code>vet/build/test/tools/cross/stub/integration/lint/fmt</code>,硬件相关的 <code>integration</code> 用 <code>GORDMA_HW=1</code> 闸住),加了 <code>make fmt</code> 和 <code>make lint</code>。</p><p><code>make lint</code> 一跑,golangci-lint 报了 20 个问题:16 个 errcheck(没检查的 <code>defer Close()</code> 返回值等)、2 个 unused、以及 2 个 staticcheck <code>SA5002</code>,正是 <code>write.go</code> 那个 busy-wait race。静态分析工具独立地撞上了 <code>/code-review</code> 早就指出的内存模型 bug。</p><p>这里有个小插曲值得一记:修这个 race 时我第一反应是用 <code>atomic.LoadUint8</code>——结果 Go 根本没有 8 位原子操作,编译直接挂。最后改成:定位包含该字节的 4 字节对齐 word,用 <code>atomic.LoadUint32</code> + <code>CompareAndSwapUint32</code> 只改其中一个字节(代价是要求 <code>Size &gt;= 4</code>)。Agent 也会想当然,差别只在于有没有一个会立刻打脸的编译器。</p><p>第三步,真机连环打脸。用户把代码弄到一台真正的 H20 GPU 服务器上(装了 RDMA 网卡),开始跑 <code>make</code>:</p><ul><li>第一次:<code>fatal error: rdma/rdma_cma.h: No such file or directory</code>,缺 <code>librdmacm-dev</code>。换成我的开发 docker,上面已经装好了相应依赖库。</li><li>第二次:<code>c.imm_data undefined</code> + <code>ibv_query_port</code> 类型不匹配,就是 <code>/code-review</code> 预言的那两个编译错误,这下在真机上真实地复现了。CC 按 libibverbs 的正确用法修:给 <code>imm_data</code> 写 C 辅助函数 <code>wc_imm_data()</code>(<code>ntohl</code> 转主机序);给 <code>ibv_query_port</code> 包一层 C wrapper <code>gordma_query_port()</code> 让 inline 在 C 里展开;发送侧 <code>imm_data</code> 补 <code>htonl</code>。</li><li>第三次:<code>device_linux.go:67: possible misuse of unsafe.Pointer</code>,<code>go vet</code> 嫌弃把指针存进 <code>uintptr</code> 再做算术。改用 <code>unsafe.Add</code>,让指针运算全程保持 <code>unsafe.Pointer</code> 形式。</li></ul><p>![image-20260612100035693.png<img src="/2026/06/17/loop-engineering-rdma-port-to-go-239-yuan/image-20260612100035693.png" class=""><br>因为这些都没进 Loop,所以不适合放在循环里,而是在循环之外执行的。起始后续<code>/simplify</code> 也可以加在循环中。</p><hr><h2 id="6-完整的提交时间线"><a href="#6-完整的提交时间线" class="headerlink" title="6. 完整的提交时间线"></a>6. 完整的提交时间线</h2><p>如果把 16 个功能 PR 之后的修复也算上,整条提交线是这样的(节选):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">edd44e2 docs: add badges to README</span><br><span class="line">4fa8b9c fix: use unsafe.Add to walk device list (go vet unsafe.Pointer) ← 真机第3次</span><br><span class="line">f0b0453 fix: cgo compile errors (imm_data, ibv_query_port) + imm byte order ← 真机第2次</span><br><span class="line">d253e36 Chore/simplify cleanups (#31) ← 质量清理+工程化</span><br><span class="line">f2519af docs: README usage, godoc, and CI workflow (#15) ← loop-it 最后一个 issue</span><br><span class="line">...</span><br><span class="line">d15513b 项目骨架:go.mod、cgo 构建配置、非 Linux stub (#1) ← loop-it 第一个 issue</span><br><span class="line">56514ff Initial commit</span><br></pre></td></tr></table></figure><p><code>#1</code> 到 <code>#15</code> 是 loop-it 自动跑出来的,干净利落。<code>d253e36</code> 之后的几个 fix,则是<strong>人把 Agent 拽回现实</strong>的痕迹。</p><hr><h2 id="7-成本:那-239-块钱"><a href="#7-成本:那-239-块钱" class="headerlink" title="7. 成本:那 239 块钱"></a>7. 成本:那 239 块钱</h2><p>说说标题里的钱。</p><p>我一直盯着Claude Code中钱的消耗,最后算下来一共花了 239 元。239 这个数字是按整轮交互(一份 PRD、15 个 issue 的实现、一轮 high-effort code-review、一轮 4 角度 simplify,外加多轮真机修复往返)的量级估的。我不想在这篇复盘里编一张逐项发票出来——那本身就违背了全文的主旨。</p><p>但即便按这个量级看,几个判断是成立的:</p><ul><li>这是「一个有经验的工程师几天的活儿」:cgo 封装整套 verbs、写 6 个 perftest 工具、跨平台 stub、CI、文档。按工时折算,几百块的 API 成本对应的是数千块的人力成本。</li><li>真正贵的不是「写」,是「来回」。第一遍实现其实很快、很便宜,大约100块。烧钱的是后面那些本可以避免的往返——如果一开始就在有网卡的 Linux 上开发,那两个编译错误根本不会漏到合并之后,也不会有「真机连环打脸」的三轮修复。</li><li>跳过审查省下的钱,会在后面加倍还回来。loop-it 静默降级掉的 <code>/review-it</code>,最终是用一轮独立的 <code>/code-review</code> + 多轮真机调试补回来的。省一步,赔三步。。</li></ul><hr><h2 id="附:项目信息"><a href="#附:项目信息" class="headerlink" title="附:项目信息"></a>附:项目信息</h2><ul><li>仓库:<code>github.com/smallnest/gordma</code></li><li>规模:52 个 Go 文件 &#x2F; ~3981 行</li><li>能力:Device&#x2F;Context&#x2F;PD&#x2F;MR&#x2F;CQ&#x2F;QP&#x2F;AH 全套 verbs;RC + UD;TCP 握手 + rdma_cm 两种建连;6 个 perftest 风格工具</li><li>构建:Linux+cgo 真实实现,非 Linux&#x2F;<code>CGO_ENABLED=0</code> stub 实现</li><li>状态:cgo 编译错误已在真机修复;硬件数据通路的端到端验证已在 RoCE v2 真机上验证</li></ul>

同分类推荐文章

  1. Go 实验特性详解 (2026-06-21 10:05:27)
  2. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)
  3. 傻瓜式RDMA高性能网络开发:从零跑到 400 Gb每秒 (2026-06-17 04:00:24)

查看更多 后端 文章 →

建议继续学习

  1. Go Reflect 性能 (累计阅读 14,121)
  2. 面向“接口”编程和面向“实现”编程 (累计阅读 13,886)
  3. 一种基于长连接的社交游戏服务器程序构架 (累计阅读 7,471)
  4. 从Go看,语言设计(一) (累计阅读 6,146)
  5. go-kit 入门(一) (累计阅读 4,739)
  6. 分布式存储Seaweedfs源码分析 (累计阅读 4,721)
  7. 为什么我们要使用Go语言以及如何使用它的 (累计阅读 4,560)
  8. Go 语言初步 (累计阅读 4,478)
  9. 程序员的“横向发展” (累计阅读 4,119)
  10. ZeroMQ 的模式 (累计阅读 4,040)