代表性面试主题

系统设计面试:如何设计远程编译的分布式构建缓存?

系统设计困难
Offer.cc 编辑团队发布 更新

题干

公司有 500 名开发者、每天 20,000 个 CI 任务和 10,000 个构建目标。请设计支持远程编译的分布式构建缓存,要求快速命中、正确失效、多租户隔离、垃圾回收,并能在缓存不可用时优雅降级。

题目与背景

假设每个任务约执行 25 个 action,平均新增输出为 300 MB。重复构建经常共享输入,但错误命中可能让错误工具链或秘密参与编译的产物进入发布流程。缓存是优化项:未命中或缓存故障应回退到执行,错误命中则属于正确性事故。回答要把 action 结果查询与不可变产物存储分开,并说明远程执行如何改变信任和容量模型。

面试官考察什么

  • 能否把影响确定性结果的输入完整纳入 action key。
  • 能否分离可变的 action 元数据与不可变的内容寻址存储(CAS)。
  • 能否做到原子写入、摘要校验,并阻止跨租户缓存投毒。
  • 能否明确容量、垃圾回收、可观测性、故障降级和迁移。
  • 能否区分构建缓存与通用键值缓存:正确性优先于接受旧读。

建议先问的澄清问题

先问构建是否 hermetic、支持哪些操作系统和架构,以及是否必须远程执行。确认保留时长、租户与仓库边界、最大产物大小、命中率目标、数据驻留要求,以及秘密或私有源码是否可能进入输出。确认结果能否跨分支、工具链版本共享,还是只能在 commit 与平台组合内共享;也要确认 CI 是否写入而开发机只读。

30 秒回答框架

我会把规范化后的 action 描述纳入哈希,包括命令、工具链和平台身份、声明输入的 Merkle 根、相关环境和构建配置。action cache 将 key 映射到输出摘要和结果元数据,CAS 按摘要保存不可变 blob。读取时校验元数据和每个 blob 后再落盘。成功的本地或远程执行先上传并校验 blob,最后发布 action 结果,避免部分产物变成命中。通过命名空间、认证写入、配额和沙箱阻止租户泄露。缓存失败返回未命中并受控执行,同时用指标和抽样干净重建发现错误命中。

分步深入

1. 定义 key 与存储边界

规范化 action 命令、编译器和链接器版本、平台、声明输入、相关 flag、白名单环境变量和外部依赖锁文件。用 Merkle 根表示输入树。排除秘密和不确定时间戳;非 hermetic action 应标记为不可缓存或限制在明确范围内。action 结果独立保存输出文件名、摘要、大小、退出码,以及可选的 stdout/stderr 摘要。CAS 对象不可变,只由摘要寻址。

2. 设计命中、未命中和发布路径

读取时按租户和 action key 命名空间路由到复制的元数据服务,取得结果后并行拉取缺失的 CAS blob。落盘前校验大小与摘要。未命中时在本地或沙箱 worker 执行。用幂等摘要写入上传已校验 blob,再以条件写入一次性提交 action 结果,确保引用的 blob 全部存在。并发写入可以上传同一个 blob,但只有完整结果可见。blob 或元数据不匹配时应当作为未命中并告警,不能返回成功命中。

3. 加入分布、隔离与安全

action 结果查询可用一致性哈希或元数据分片图,并在故障域之间复制。大体积 CAS 放入对象存储或分片 blob 层,热点元数据放入低延迟存储。每次请求认证并授权仓库和租户命名空间,开发机默认只读。传输和存储加密,执行远程 action 时使用沙箱和租户配额。除非策略明确允许,不跨租户去重;摘要不能绕过授权。

4. 规划容量和生命周期

题设负载是每天 20,000 个任务 × 25 个 action,即每天 500,000 次查询,平均约 5.8 QPS。按 20 倍突发估算约 120 QPS,还未计入并行 blob 读取。若 10% 的 action 产生新的 300 MB 输出,未压缩入口约为每天 1.5 TB;压缩和去重会降低实际存储,但应预留多 TB 对象容量和带宽余量。垃圾回收从仍存活的 action 结果和 manifest 出发,沿摘要引用关系遍历,经过宽限期后再按大小配额、LRU 或年龄清理。可达 blob 不能删除,并要按租户公平执行配额。

5. 让故障和正确性可观测

区分缓存超时、权限失败、blob 缺失、摘要不匹配和后端不可用。缓存超时可以执行;持续故障需要准入控制、本地缓存优先和有限重试,避免 CI 重试风暴。按仓库、action 类别、平台和工具链记录命中率、查询延迟、blob 带宽、上传中止、驱逐、损坏和租户拒绝。定期清空本地缓存做干净重建,比较执行日志或产物摘要,以发现不确定性和错误命中。

强回答示例

我会提供两个服务:认证的 action-cache 索引和不可变 CAS。action key 覆盖规范化命令、编译器与平台身份、声明输入 Merkle 根、白名单环境、flag 以及锁定的外部依赖。命中返回输出摘要,客户端校验并下载 blob 后再落盘。未命中进入沙箱执行,幂等上传并校验 blob,所有引用存在后才发布结果。action 元数据按租户和仓库复制,CAS 跨故障域分片或放入对象存储。缓存故障时可回退执行;写入只给可信 CI,并设置配额、加密和默认禁止跨租户复用。容量按约 120 峰值 QPS 和多 TB 存储规划,再验证命中率、摘要错误、不确定 action、并发写入、工具链升级、GC 可达性和故障恢复。

常见错误

  • 只哈希源码,遗漏编译器、flag、平台、环境或锁定依赖。
  • 把 action 结果和输出 blob 放进一个可变记录,导致部分发布。
  • 不检查命名空间授权就跨租户复用摘要。
  • 把缓存不可用设计成构建全停,而不是受控未命中和回退。
  • 使用一个全局 LRU,未追踪存活 action 结果引用就删除 blob。
  • 把 stdout 或 stderr 数量当成命中率,应该记录执行策略和明确的命中计数。
  • 没有干净构建、跨机器复现和不确定 action 测试就声称命中率很高。

追问与回答

编译器工具链升级,但命令行没有变化怎么办?

工具链身份必须进入 action key,通常使用固定摘要或版本化执行镜像。迁移时可以双读旧命名空间用于回滚,但写入应进入新命名空间并单独统计未命中。不能只因源码和 flag 相同就复用旧产物。

缓存被投毒后如何恢复?

停止不可信写入,隔离受影响命名空间,并根据审计日志定位错误 action 结果及其可达 blob。失效 action 索引,用可信执行重建产物,再重新填充。恢复期间缓存应保持可选,并保留证据供事故复盘。

action 在执行时下载未锁定依赖怎么办?

这是非 hermetic action:在依赖固定且下载字节纳入输入闭包前标记为不可缓存。紧急情况下可以设置仓库范围和短 TTL 的明确例外,但不能把它当作确定性复用。

如何从本地缓存迁移且不让 CI 中断?

先以 shadow 模式读取远程缓存,比较本地和远程 key 及输出摘要,再让少量仓库启用远程命中。保留本地执行和本地缓存回退,写入仅开放给可信 CI,命中率、延迟和错误命中检查通过后逐步扩大。

公开来源

同类题目

相关面试工具

用 Solve 整理系统设计回答

从澄清需求开始,展开规模、架构、组件选择和取舍。

查看工具