
一台服务器上运行 60 个容器
一个裸金属服务器运行数十到数百个 Hoody 容器。KSM 和 BTRFS 去重使边际成本接近零。
你的流水线第一个任务把 node_modules 打包并管入一个 Hoody URL。下游二十个任务 curl 同一个 URL 并解包。用 ?n=20 时,生产者会等到所有二十个 worker 都连接上,然后只推流一次——同时扇出给所有人。没有 S3 桶,没有 cache action,没有出口流量账单。
没有客户端库、没有守护进程、没有 SDK。生产者把它的 tar 包流入 PUT /pipe/cache?n=20。每个 worker 从 GET /pipe/cache?n=20 把它流出。pipe 只在传输过程中持有字节——从不落盘。
tar 打包 node_modules;zstd 在线压缩;curl 把字节直接 PUT 进 pipe 路径。无临时文件、无 upload-artifact 步骤、无桶凭证。
每个测试 worker GET 同一个 URL,解压,然后解包到自己的工作目录。慢 worker 对生产者施加背压,但不会卡住更快的那些。
?n=20一个查询参数。生产者和消费者约定同一个 n。pipe 把上传挂起,直到正好有那么多接收方连接,然后打开闸门。
CI 缓存以前是种税:明天就要被覆盖的存储、worker 每次拉取的出口流量、为 cache action 那些怪癖花掉的工程时间。pipe 一次性删掉这三项。
没有 S3 桶,因为根本没有存储。pipe 在传输结束的瞬间忘掉这些字节,所以没什么可按 GB-月或按 GB-出收费。缓存不再是一项账目。
没有 yaml 编码的 key、没有 save-cache / restore-cache 拆分、没有要调试为何缓存没在该 runner 命中。只有 curl。同样这两行在 GitHub Actions、BuildKite、Jenkins、你的笔记本或 cron 容器上都跑得起来。
分支只用不同的路径就行。/pipe/cache/main、/pipe/cache/feat-x、/pipe/cache/PR-742。无需作废。无需驱逐。当分支死掉,它的路径不再被请求,这就是全部生命周期。
在真实工作负载上——node_modules 约 800 MB、二十个并行测试 worker、一天一百次 CI 运行——账单大头是出口流量,不是存储。
20× 出口
二十个 worker 各自从 S3 拉缓存。一次 CI 跑里二十次下载一个 800 MB tar 包就是 16 GB 出口流量。桶本身是简单的部分——出口流量才是会复利累加的。
1× 传输
生产者把 800 MB 推流一次。pipe 把字节边走边扇出给所有二十个接收方。线缆里只过一次,没有按接收方计的乘子,没有存储账单。
数字仅作示意,基于一个典型 Node monorepo 缓存。实际节省取决于 tar 包大小、worker 扇出数和你的供应商在缓存区域出口流量的报价。形状——随 worker 数线性增长 vs. 常量——是不变的。
缓存层就是 HTTP。它一直就是。我们只是没注意到。
缓存从来都不是关于存储。它是关于不重新构建就把同一份字节送到 N 个 worker。HTTP 已经在做这件事——只要你让一个 URL 扇出到一个已知数量的接收方。桶不过是我们没有扇出能力时的变通。
一次 PUT,n 次 GET,完全相同的字节。背压按接收方计,所以慢 worker 不拖慢快 worker。
PUT /pipe/cache?n=20pipe 在传输期间持有字节,结束时忘掉。没有要驱逐的、没有要做生命周期的、没有要备份的。
TTL ≤ 5 min · 然后驱逐每个分支挑自己的路径。无共享 key 空间,无冲突。路径同时是缓存 key 和 URL。
/pipe/cache/[branch]大多数 CI 缓存都解决同一个问题:把同一个 tar 包送到 N 个 worker。它们靠存储和出口流量来做。pipe 直接通过线缆来做。
两条 curl 命令。一个 URL。喂饱二十个 worker。