代表性面试主题

系统设计面试:如何设计云文件存储与多设备同步服务?

系统设计困难
OfferCC 编辑团队发布 更新

题干

设计一个云文件存储与多设备同步服务:系统有 5,000 万注册用户、500 万日活用户,平均每位用户保存 10 GB 逻辑数据;每天产生 1 亿个新文件版本,平均每个版本新增或修改 4 MB,峰值流量按日均的 5 倍估算。单文件最大 50 GB,在线设备应在 5 秒内看到元数据变更,元数据服务可用性目标为 99.99%。请说明 API、数据模型、分片与秒传、同步协议、一致性、冲突与删除、容量估算、故障处理和验证方案。

题干与适用场景

设计一个类似 Dropbox 或 Google Drive 的云文件存储与多设备同步服务。系统有 5,000 万注册用户、500 万 日活用户,平均每位用户保存 10 GB 逻辑数据。每天产生 1 亿个新文件版本,平均每个版本新增或修改 4 MB; 峰值流量按日均的 5 倍估算。单文件最大 50 GB,在线设备应在 5 秒内看到创建、修改、移动和删除等元数据 变更,元数据服务可用性目标为 99.99%。

题目中的规模、延迟、可用性和 4 MiB 目标分片大小都是面试设计输入,不代表任何云存储产品的公开承诺。 范围包括上传、下载、断点续传、版本恢复、多设备同步、离线编辑、冲突副本、简单分享和删除回收。多人同时 编辑同一文档的字符级协作、Office 文件语义合并、完整企业权限系统和跨区域主动多主写入不在主问题范围内。

2026 年仍有公开系统设计题目以 Dropbox、Google Drive 或通用文件同步服务为背景,要求候选人讨论大文件 分片、增量同步、版本、离线操作和冲突。对象存储官方文档也能验证分片上传的工程价值:分片可以并行上传, 单片失败只重传该片,但未完成的分片需要主动终止或生命周期清理。面试重点是把“内容传输”和“文件命名 空间状态”分开,并关闭失败恢复与数据回收的正确性环,无需背诵某家产品的内部架构。

面试官考察点

第一,看候选人能否把文件内容与元数据分层。大文件字节适合放在廉价、持久、不可变的对象存储中;文件名、 目录关系、当前版本、删除标记和同步游标需要支持条件写与有序变更。如果把 50 GB 文件直接写进关系数据库, 或者让对象存储键名承担全部目录语义,修改、移动、事务提交和恢复都会变得脆弱。

第二,看上传是否有明确提交点。客户端可以先并行上传缺失分片,但只有服务端确认全部分片存在且校验通过, 并原子更新 currentVersionId 与变更日志后,新版本才对其他设备可见。否则会出现元数据指向缺失内容, 或文件字节已经上传但用户永远看不到的悬挂状态。

第三,看同步协议是否能抵抗通知丢失、重复和乱序。推送通知只能是“有新变化”的提示,设备必须携带持久化 游标拉取权威变更。离线数天的设备、重装客户端和游标过期都要有恢复路径,不能把 WebSocket 连接当作数据 真相。

第四,看并发语义是否诚实。两个离线设备基于同一旧版本修改普通二进制文件时,通用服务无法可靠自动合并。 条件提交失败后应保留双方版本并生成冲突副本;静默采用最后写入者会丢失内容。Dropbox 的公开帮助文档也 说明,同时或离线修改可能产生 conflicted copy,需要用户人工合并。

最后,看容量、成本和回收是否成体系。候选人要区分逻辑容量与去重后的物理容量,估算版本提交和字节流量, 说明孤儿分片、历史版本、删除墓碑、配额、热点命名空间以及垃圾回收为什么不能只靠一个即时引用计数。

回答前需要澄清的问题

  • 同步哪些内容? 本题同步普通文件和目录;不提供字符级实时协作。
  • 一致性目标是什么? 上传成功的设备需要读到自己的新版本;其他在线设备在 5 秒内最终看到元数据变化。
  • 离线多久仍需增量恢复? 先假设变更日志保留 30 天;游标更旧时返回命名空间快照再继续增量。
  • 如何处理并发修改? 同一 baseVersionId 只允许一个版本成为当前版本,后到者保留为冲突副本。
  • 删除能否立即物理清理? 不能。先写墓碑并保留 30 天,以支持离线设备同步和用户恢复。
  • 是否做跨用户全局去重? 默认只在同一账户或租户内去重,避免泄露内容是否存在,也减少加密与删除耦合。
  • 分享范围多大? 主设计支持文件或目录的只读分享链接;复杂组织权限放到追问。
  • 加密与恶意文件怎么办? 传输和静态存储加密,短期签名 URL,上传后异步恶意软件扫描;端到端加密不在主范围。
  • 多区域怎么写? 每个命名空间有一个主写区域;对象可跨区域复制,元数据跨区域异步灾备。
  • 秒传是否一定命中? 不假设固定去重率。逻辑配额按用户文件大小计费,物理节省单独观测。

30 秒回答框架

“我会把文件切成约 4 MiB 的不可变分片放入对象存储,把目录、当前版本、分片清单、墓碑和有序变更放进 强一致元数据层。客户端只上传缺失分片,再用 baseVersionId 和幂等键原子提交版本。设备持久化同步游标, 通知只触发 /changes?cursor= 拉取,所以丢通知不会丢数据;并发离线提交失败时保留冲突副本。每天 1 亿 版本的 5 倍峰值约 6,000 次提交/s,400 TB 日均写入的峰值约 25 GB/s。删除先保留墓碑,分片经宽限期和 清单二次对账后再回收。”

分步骤深入解答

先定义五个不变量:

  1. 已发布版本只能引用已经存在且校验通过的分片。
  2. 只有 baseVersionId 等于当前版本时,提交才能替换 currentVersionId。
  3. 每个命名空间的变更序号严格递增,客户端游标只向前推进。
  4. 删除先产生墓碑;在同步和恢复窗口结束前,不物理删除仍可能被引用的数据。
  5. 垃圾回收只删除经过宽限期和清单对账后仍无引用的分片。

第一步:拆分客户端、元数据和内容平面。

客户端包含文件监视器、本地索引、持久化操作日志、分片器和同步引擎。进程崩溃后,它从本地日志恢复“哪些 分片已上传、哪个提交仍未确认”,而不是重新扫描并上传全部文件。服务端包含 API 网关、认证与配额、元数据 服务、上传协调器、对象存储、命名空间变更日志、通知服务、下载 CDN、恶意文件扫描和垃圾回收任务。

元数据按 namespaceId 路由。个人空间是一个命名空间,共享目录也可以成为独立命名空间,从而让权限、 有序变更和热点隔离保持在同一边界。每个命名空间先指定一个主写区域,跨可用区同步复制元数据;下载可从 就近 CDN 或对象副本读取。这样避免跨区域两个主节点同时接受相互冲突的目录更新。

第二步:定义数据模型。

text
FileEntry(id, namespaceId, parentId, name, type, currentVersionId, deletedAt)
FileVersion(id, fileId, baseVersionId, size, manifestHash, createdBy, createdAt)
VersionChunk(versionId, ordinal, chunkHash, size)
UploadSession(id, fileId, baseVersionId, state, expiresAt, idempotencyKey)
Change(namespaceId, seq, entityId, operation, versionId, createdAt)

目录结构使用稳定 fileId 和 parentId,移动或改名只更新元数据,不复制文件内容。FileVersion 不可变; VersionChunk 按顺序组成文件清单。manifestHash 用于校验清单,而不是替代每个分片的校验。 UploadSession 记录会话状态和幂等键。Change.seq 是命名空间内单调序号,删除也作为事件写入。

目录下的名称唯一性用 (namespaceId, parentId, normalizedName) 条件约束维护。大小写规则必须在产品定义中 固定,否则 Windows、macOS 和 Linux 客户端会对同名文件得出不同结论。

第三步:设计可恢复的分片上传。

text
POST /files/{fileId}/upload-sessions
  { baseVersionId, size, chunks[], idempotencyKey }
  -> { uploadSessionId, missingChunks[], signedUrls[] }

PUT {signedChunkUrl}
  Content-Checksum: ...

POST /upload-sessions/{uploadSessionId}/commit
  { manifestHash, idempotencyKey }
  -> { fileVersionId, changeSeq }

GET /files/{fileId}/download-manifest
  -> { fileVersionId, chunks[], signedUrls[] }

客户端默认切成约 4 MiB 分片并计算强哈希。服务端在账户或租户范围查询已存在的分片,只为缺失分片发放 短期、限对象、限操作的签名 URL。客户端可有限并行上传,失败只重试单片。AWS 和阿里云的官方分片上传 文档都采用“初始化、上传各片、完成”的会话模型,并提醒未完成分片会持续占用存储,因此上传会话必须过期, 后台也要主动终止长期未完成的会话。

固定大小分片简单、易于并行,适合大多数文件。若大量工作负载是在文件开头插入少量字节,固定边界会让后续 所有分片哈希变化;这时可对大文件采用内容定义分块,以更多 CPU 和实现复杂度换取更好的差量命中。本题先 用固定分片,依据真实改动模式再升级,不能把复杂算法当作默认答案。

第四步:把版本提交做成唯一可见性边界。

提交接口先按 idempotencyKey 查找既有结果,再校验分片大小、哈希、权限和配额。随后在一个元数据事务中:

  1. 锁定或条件读取 FileEntry.currentVersionId。
  2. 确认它仍等于请求的 baseVersionId。
  3. 写入不可变 FileVersion 与 VersionChunk 清单。
  4. 更新 currentVersionId。
  5. 写入下一条 Change.seq 和事务 outbox。

对象字节先于元数据事务完成,因此事务不会指向不存在的分片。事务提交后通知发送失败也不影响正确性: outbox 会重试,而设备还会通过游标补拉。若提交响应丢失,客户端用同一幂等键重试并得到原 fileVersionId, 不会生成多个逻辑版本或重复扣减配额。

第五步:用游标完成多设备同步。

text
GET /changes?namespaceId={id}&cursor={lastSeq}&limit=1000
  -> { changes[], nextCursor, hasMore }

设备把 lastSeq 与本地文件索引放进同一持久化事务。收到“命名空间有变化”的通知后,它循环拉取直到 hasMore=false,按序应用创建、更新、移动和墓碑,然后才提交新游标。重复事件由 seq 去重;通知乱序 不影响拉取结果;通知完全丢失时,前台唤醒和周期性检查仍会发现新序号。

若游标早于 30 天保留窗口,服务返回 cursor_expired 和一致的命名空间快照位置。客户端先下载快照,核对 本地未提交操作,再从快照水位继续增量,不能直接删除所有本地文件。通知是性能优化,游标日志才是同步协议。

第六步:处理冲突、删除和版本恢复。

设备 A、B 都从版本 10 离线编辑。A 先以 baseVersionId=10 提交版本 11。B 再提交时条件更新失败,服务 保留 B 上传的内容,创建类似“文件名(B 的冲突副本)”的新条目或冲突版本,并向命名空间写一条变化。普通 二进制文件不做静默自动合并;文本或特定文档的合并能力应作为独立产品功能。

删除只把 deletedAt 和墓碑事件写入元数据。在线设备收到后移入回收站,离线设备重新上线也能得知删除。 30 天恢复窗口内,历史版本和分片引用仍然有效。窗口结束后,清理器先计算所有活跃版本清单,再标记候选 无引用分片,等待宽限期并二次对账后才删除。即时引用计数可能因事务重试、延迟事件或修复任务出错,不能 单独成为不可逆删除依据。

第七步:估算容量并处理热点。

5,000 万 × 10 GB = 500 PB 逻辑存储。副本、历史版本和去重会改变物理容量,但题目没有提供这些比例, 所以不能给出伪精确结果。每天 1 亿 × 4 MB = 400 TB 逻辑写入,日均约 4.6 GB/s,5 倍峰值约 25 GB/s。版本提交日均约 100,000,000 / 86,400 ≈ 1,157/s,峰值取整约 6,000/s。

若每次变更平均提示 3 台在线设备,通知峰值可到约 18,000 次/s,但通知可合并成“某命名空间有新变化”, 不必逐文件可靠投递。元数据按 namespaceId 哈希分片,同一命名空间的有序写落在同一主分片。大型共享 空间可能成为热点,可限制单目录操作速率、批量合并通知,并在确有证据时把文件记录按 fileId 子分片, 同时保留独立的命名空间序号生成器。

第八步:关闭故障、安全和验证环。

  • 上传中断:客户端查询会话并只补传缺失分片;过期会话由生命周期任务终止。
  • 分片完成但提交失败:分片暂时成为孤儿,宽限期后由清单对账回收。
  • 提交成功但通知失败:事务 outbox 重试,设备游标拉取仍可恢复。
  • 提交成功但响应丢失:同一幂等键返回原结果。
  • 并发离线写:baseVersionId 条件失败,保存冲突副本。
  • 分片损坏:上传和下载都校验强哈希,错误内容不能进入已发布清单。
  • 游标过期:下载快照,再从快照水位继续增量。
  • 主区域不可写:停止该命名空间写入或按明确 RPO/RTO 故障转移,不能让两个区域同时成为主写者。

安全上,签名 URL 必须短期有效并绑定账户、对象、大小与操作;服务端在提交前重新做权限和配额校验。跨用户 全局秒传容易形成“某内容是否存在”的侧信道,也会让每用户加密密钥与删除权相互耦合,因此默认只在租户内 去重。核心指标包括上传与提交 p99、同步滞后、游标过期、冲突副本数、孤儿分片字节、去重命中、垃圾回收 候选与实际删除差异、热点命名空间、校验失败和恢复成功率。

验证要覆盖 50 GB 文件断点续传、每个上传阶段崩溃、重复提交、通知丢失与乱序、两个离线设备并发修改、 删除后旧设备上线、校验和损坏、主分片切换、过期游标、配额边界和垃圾回收误删保护。最重要的端到端断言是: 任何客户端可见的 fileVersionId 都能下载并校验出完整内容,而且仍在恢复窗口内的版本不会被回收。

高质量示范回答

“我先把系统拆成内容平面和元数据平面。文件内容按约 4 MiB 切成不可变分片,存入对象存储;目录、稳定 fileId、当前版本、版本清单、墓碑和命名空间变更序号放进支持条件写的元数据层。移动和改名只改元数据, 历史版本不覆盖。

上传时客户端计算分片哈希,并用 baseVersionId 和幂等键创建会话。服务端只返回租户内缺失分片的短期 签名 URL。所有分片上传并校验后,提交事务确认当前版本没有改变,写入新版本和清单,更新 currentVersionId,同时追加 Change.seq 与 outbox。分片先完成、元数据后发布,保证可见版本不引用缺失 内容;响应丢失时同一幂等键返回原结果。

同步以持久化游标为准。通知只说命名空间可能有变化,设备调用 /changes?cursor= 顺序拉取并在应用后推进 游标,所以通知丢失、重复和乱序都不会丢数据。游标超过 30 天保留窗口时,用一致快照重建,再从快照水位 继续。两个设备从同一版本离线修改时,后到提交不能覆盖新版本,而是保留为冲突副本。

容量上,逻辑存储是 500 PB;每天新增或修改 400 TB,平均约 4.6 GB/s、5 倍峰值约 25 GB/s。每天 1 亿 版本平均约 1,157 次提交/s,峰值约 6,000/s。元数据按命名空间分片,每个命名空间一个主写区域;对象下载 可经 CDN 和跨区域副本扩展。

删除先写墓碑并保留 30 天。历史版本到期后,垃圾回收根据全部活跃清单标记候选,等待宽限期并二次对账, 不能因一次引用计数为零就立即删除。最后我会故障注入每个上传和提交阶段,验证通知丢失、离线冲突、过期 游标、校验损坏、主节点切换和 GC 安全,并持续检查所有可见版本都能完整下载。”

常见错误

  • 把文件字节放进元数据库 → 大对象拖累备份、复制和事务 → 元数据保存引用,内容进入不可变对象存储。
  • 上传完第一片就创建可见文件 → 其他设备可能读到残缺版本 → 全部分片验证后再原子提交元数据。
  • 把 WebSocket 通知当同步真相 → 丢包或离线会永久漏变化 → 通知只触发基于游标的权威拉取。
  • 提交不带 baseVersionId → 离线设备静默覆盖较新版本 → 使用条件提交,失败时保留冲突副本。
  • 失败重试生成新会话和新幂等键 → 重复版本、重复配额和孤儿分片增加 → 稳定幂等键找回原结果。
  • 默认跨用户全局秒传 → 暴露内容存在性并耦合加密、删除 → 默认限制在账户或租户范围。
  • 删除后立刻清理分片 → 离线同步、版本恢复和延迟事务会引用已删内容 → 墓碑、保留期、宽限期和二次对账。
  • 用固定去重率推算物理容量 → 工作负载未知导致伪精确 → 报告 500 PB 逻辑量,物理节省用监控校准。
  • 每个文件变化都可靠推送给每台设备 → 通知成本和重试状态爆炸 → 合并为命名空间提示,由客户端拉增量。
  • 直接做跨区域主动多主 → 同名、移动和当前版本冲突难以收敛 → 每个命名空间先保持单主写入。

追问及应对

追问一:固定分片和内容定义分块怎么选?

固定 4 MiB 左右的分片实现简单、可预测,适合追加、局部覆盖和多数媒体文件。若用户常在文件开头插入内容, 固定边界会让后续哈希全部变化;内容定义分块能找回相同内容,但消耗更多 CPU,边界算法也需要稳定版本化。 先用固定分片上线并测量“修改后可复用字节比例”,只有真实收益足以覆盖复杂度时再对特定大文件启用内容定义 分块。

追问二:如何保证移动目录时同步顺序正确?

移动操作在元数据事务中更新 parentId,并写入一个命名空间序号。客户端严格按 seq 应用;如果一次移动 涉及大量子孙文件,不逐个重写路径,因为路径由父链推导。跨命名空间移动不能假装是单条本地更新,可以建成 可重试的复制加删除工作流,并在 UI 中显示处理中。

追问三:如何支持端到端加密和秒传?

客户端端到端加密后,服务端通常只能看到密文。若每个用户使用不同密钥,相同明文不会产生相同密文, 跨用户去重基本失效;强行做收敛加密又会引入内容确认攻击和密钥风险。应明确产品优先级:高隐私模式接受 较低去重,租户托管密钥模式可在租户边界内去重,不能同时承诺无条件全局秒传和强端到端保密。

追问四:一个超大共享目录成为热点怎么办?

先合并通知、限制批量操作、缓存只读目录页,并测量热点来自序号生成、名称唯一约束还是列表查询。文件元数据 可按 fileId 子分片,但命名空间变更仍需要一个有序水位;可以把多个内部事件批量分配序号段或写入分区日志, 再提供稳定的对外游标。不能为了横向扩展放弃客户端可恢复的顺序语义。

追问五:垃圾回收怎样避免误删?

GC 不直接相信实时引用计数。它先从仍在保留期内的所有版本清单构建“活跃集合”,把不在集合中的分片标记为 候选,等待超过最大事务、复制和恢复延迟的宽限期,再用最新清单二次对账后删除。删除任务幂等并记录审计; 发现清单缺失或对账差异时宁可延迟回收。对象存储未完成的 multipart parts 还需要独立的会话过期和 abort 策略。

追问六:如何做跨区域灾难恢复?

每个命名空间正常只有一个 home region 接受元数据写,其他区域异步复制日志和对象。故障时先确认原主不再 写入,再把灾备副本提升到新纪元并切换路由;RPO 取决于日志复制落后,RTO 取决于检测和提升流程。若要求 RPO=0,就需要跨区域同步多数派并接受更高写延迟。不能让旧主恢复后直接继续写,必须校验命名空间领导纪元。

追问七:如何证明同步不会漏数据?

建立状态模型,生成上传、提交、移动、删除、冲突和重试序列,断言客户端应用到序号 N 后的状态等于服务端 快照 N。端到端测试随机丢弃、复制和打乱通知,但不篡改变更日志;设备仍应通过游标收敛。再在“应用变更但 尚未保存游标”和“保存本地内容但进程崩溃”等边界注入故障,确认重放幂等,最终所有设备达到相同版本图。

公开来源

同类题目

相关面试工具

用 Solve 整理系统设计回答

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

查看工具