题干与适用场景
设计一个类似 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。删除先保留墓碑,分片经宽限期和 清单二次对账后再回收。”
分步骤深入解答
先定义五个不变量:
- 已发布版本只能引用已经存在且校验通过的分片。
- 只有
baseVersionId等于当前版本时,提交才能替换currentVersionId。 - 每个命名空间的变更序号严格递增,客户端游标只向前推进。
- 删除先产生墓碑;在同步和恢复窗口结束前,不物理删除仍可能被引用的数据。
- 垃圾回收只删除经过宽限期和清单对账后仍无引用的分片。
第一步:拆分客户端、元数据和内容平面。
客户端包含文件监视器、本地索引、持久化操作日志、分片器和同步引擎。进程崩溃后,它从本地日志恢复“哪些 分片已上传、哪个提交仍未确认”,而不是重新扫描并上传全部文件。服务端包含 API 网关、认证与配额、元数据 服务、上传协调器、对象存储、命名空间变更日志、通知服务、下载 CDN、恶意文件扫描和垃圾回收任务。
元数据按 namespaceId 路由。个人空间是一个命名空间,共享目录也可以成为独立命名空间,从而让权限、 有序变更和热点隔离保持在同一边界。每个命名空间先指定一个主写区域,跨可用区同步复制元数据;下载可从 就近 CDN 或对象副本读取。这样避免跨区域两个主节点同时接受相互冲突的目录更新。
第二步:定义数据模型。
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 客户端会对同名文件得出不同结论。
第三步:设计可恢复的分片上传。
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 查找既有结果,再校验分片大小、哈希、权限和配额。随后在一个元数据事务中:
- 锁定或条件读取
FileEntry.currentVersionId。 - 确认它仍等于请求的
baseVersionId。 - 写入不可变
FileVersion与VersionChunk清单。 - 更新
currentVersionId。 - 写入下一条
Change.seq和事务 outbox。
对象字节先于元数据事务完成,因此事务不会指向不存在的分片。事务提交后通知发送失败也不影响正确性: outbox 会重试,而设备还会通过游标补拉。若提交响应丢失,客户端用同一幂等键重试并得到原 fileVersionId, 不会生成多个逻辑版本或重复扣减配额。
第五步:用游标完成多设备同步。
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。端到端测试随机丢弃、复制和打乱通知,但不篡改变更日志;设备仍应通过游标收敛。再在“应用变更但 尚未保存游标”和“保存本地内容但进程崩溃”等边界注入故障,确认重放幂等,最终所有设备达到相同版本图。