跳转到内容
use-cases / a-ci-cache-thats-just-two-curl-commands / hero
PIPE · CI · 成本优化

只用两条 curl 命令的 CI 缓存

你的流水线第一个任务把 node_modules 打包并管入一个 Hoody URL。下游二十个任务 curl 同一个 URL 并解包。用 ?n=20 时,生产者会等到所有二十个 worker 都连接上,然后只推流一次——同时扇出给所有人。没有 S3 桶,没有 cache action,没有出口流量账单。

阅读 pipe 文档
use-cases / a-ci-cache-thats-just-two-curl-commands / flow

整个机制就两行 shell

没有客户端库、没有守护进程、没有 SDK。生产者把它的 tar 包流入 PUT /pipe/cache?n=20。每个 worker 从 GET /pipe/cache?n=20 把它流出。pipe 只在传输过程中持有字节——从不落盘。

PUT · 生产者每次构建运行一次

生产缓存

$tar c node_modules|zstd|curl -T -
https://hoody.com/pipe/cache?n=20
# blocks until 20 receivers connect, then streams once

tar 打包 node_modules;zstd 在线压缩;curl 把字节直接 PUT 进 pipe 路径。无临时文件、无 upload-artifact 步骤、无桶凭证。

GET · 消费者每个 worker 上运行

消费缓存

$curlhttps://hoody.com/pipe/cache?n=20
| zstd -d | tar x
# fan-out: every worker gets an identical copy

每个测试 worker GET 同一个 URL,解压,然后解包到自己的工作目录。慢 worker 对生产者施加背压,但不会卡住更快的那些。

全部协调表面?n=20

一个查询参数。生产者和消费者约定同一个 n。pipe 把上传挂起,直到正好有那么多接收方连接,然后打开闸门。

use-cases / a-ci-cache-thats-just-two-curl-commands / relief

你停止支付什么

CI 缓存以前是种税:明天就要被覆盖的存储、worker 每次拉取的出口流量、为 cache action 那些怪癖花掉的工程时间。pipe 一次性删掉这三项。

无桶。无出口账单。

没有 S3 桶,因为根本没有存储。pipe 在传输结束的瞬间忘掉这些字节,所以没什么可按 GB-月或按 GB-出收费。缓存不再是一项账目。

无 cache action 怪癖

没有 yaml 编码的 key、没有 save-cache / restore-cache 拆分、没有要调试为何缓存没在该 runner 命中。只有 curl。同样这两行在 GitHub Actions、BuildKite、Jenkins、你的笔记本或 cron 容器上都跑得起来。

分支间无 key 冲突

分支只用不同的路径就行。/pipe/cache/main、/pipe/cache/feat-x、/pipe/cache/PR-742。无需作废。无需驱逐。当分支死掉,它的路径不再被请求,这就是全部生命周期。

use-cases / a-ci-cache-thats-just-two-curl-commands / compare

成本形状,前与后

在真实工作负载上——node_modules 约 800 MB、二十个并行测试 worker、一天一百次 CI 运行——账单大头是出口流量,不是存储。

S3 + GH ACTIONS 缓存

20× 出口

二十个 worker 各自从 S3 拉缓存。一次 CI 跑里二十次下载一个 800 MB tar 包就是 16 GB 出口流量。桶本身是简单的部分——出口流量才是会复利累加的。

VS
HOODY PIPE · ?n=20

1× 传输

生产者把 800 MB 推流一次。pipe 把字节边走边扇出给所有二十个接收方。线缆里只过一次,没有按接收方计的乘子,没有存储账单。

数字仅作示意,基于一个典型 Node monorepo 缓存。实际节省取决于 tar 包大小、worker 扇出数和你的供应商在缓存区域出口流量的报价。形状——随 worker 数线性增长 vs. 常量——是不变的。

use-cases / a-ci-cache-thats-just-two-curl-commands / punchline

缓存层就是 HTTP。它一直就是。我们只是没注意到。

缓存从来都不是关于存储。它是关于不重新构建就把同一份字节送到 N 个 worker。HTTP 已经在做这件事——只要你让一个 URL 扇出到一个已知数量的接收方。桶不过是我们没有扇出能力时的变通。

字节

推流一次,扇出多份

一次 PUT,n 次 GET,完全相同的字节。背压按接收方计,所以慢 worker 不拖慢快 worker。

PUT /pipe/cache?n=20
时间

只在传输过程中存活

pipe 在传输期间持有字节,结束时忘掉。没有要驱逐的、没有要做生命周期的、没有要备份的。

TTL ≤ 5 min · 然后驱逐
路径

分支即路径

每个分支挑自己的路径。无共享 key 空间,无冲突。路径同时是缓存 key 和 URL。

/pipe/cache/[branch]
阅读 pipe API
use-cases / a-ci-cache-thats-just-two-curl-commands / replaces

这取代了什么

大多数 CI 缓存都解决同一个问题:把同一个 tar 包送到 N 个 worker。它们靠存储和出口流量来做。pipe 直接通过线缆来做。

  • AWS S3(缓存桶 + 出口流量)明天就要覆盖的存储 + 每次拉取的出口流量
  • GitHub Actions cacheYaml key、按 runner 的怪癖、10 GB 上限
  • BuildJet / Garnix CI 缓存更快,但仍按厂家、仍按字节计费
  • Bazel remote cache如果你全押 Bazel 就很棒;如果不是就太重
  • Turborepo remote cacheVercel 托管,monorepo 形状,有自己的偏好
  • Earthly satellite cache又一个守护进程、又一个桶、又一份账单
  • 自定义 rsync 缓存一台 NFS 盒子和一把所有人都忘了轮换的 SSH key
use-cases / a-ci-cache-thats-just-two-curl-commands / cta

两条 curl 命令。一个 URL。喂饱二十个 worker。

阅读 pipe 文档
use-cases / a-ci-cache-thats-just-two-curl-commands / related

阅读其他内容