系统设计 14:图片社交:上传成功之后还差一次发布提交
客户端收到对象上传成功,并不代表动态列表中已经有一张可展示的图片。缩略图可能未生成,图片可能未通过检查,元数据事务也可能失败。候选图片社交服务把“字节已经保存”和“用户能够看见”拆成两个状态,半成品只能停留在上传会话内。
Instagram 在这里表示上传、处理和展示的题型,不表示公开资料之外的真实产品架构。范围包括原图、固定尺寸缩略图、私密相册和动态展示;图像推荐模型、直播和全功能社交关系留给后续章节。
把发布作为一个明确的接口
POST /uploads 创建会话,返回上传 ID、对象键、允许的大小和类型,以及限时上传凭据。POST /uploads/{id}/complete 提交实际长度和校验信息,服务端读取对象元数据并检查是否符合会话约束。POST /photos/{id}/publish 只有在必要缩略图与内容检查完成后才允许状态从 PROCESSING 变成 READY。可以把最后一步由后台自动触发,但状态边界仍然保留。
数据表包括 uploads(id, owner, object_key, expires_at, state),photos(id, owner, original_key, state, visibility, version) 与 variants(photo_id, profile, object_key, checksum, state)。变体表对 (photo_id, profile, version) 唯一,处理重试不能生成多个对外可见版本。展示查询只返回 READY,并在返回媒体凭据前校验当前权限。
状态机可以按 PENDING → PROCESSING → READY 演进,校验失败进入 REJECTED,用户删除进入 DELETED。不能用“对象键不为空”代替就绪状态,因为原图存在只说明一个阶段完成。发布事务把照片标为就绪并写动态事件,消费者以后重投依靠照片 ID 与版本去重,避免重复动态。
上传接口还要限制总配额、并行会话数和会话有效期。仅限制一张图片大小无法防止用户创建百万个永不完成的会话。图片解码后像素总数可能远超压缩字节大小,处理器需要内存、CPU 与像素上限;不能把所有用户上传内容放进没有资源隔离的解码进程。
带宽与派生图比 API QPS 更早增长
教学假设每天上传 100 万张,平均原图 4 MB,三份缩略图各 0.2 MB,平均上传率约 11.57 张/秒,峰值系数 8,峰值约 92.59 张/秒。客户端原图上传峰值带宽约 370.37 MB/s;原图与变体日新增合计 4.6 TB。保留 30 天、两份副本为 276 TB,元数据、日志和历史版本另计。
如果每天展示 5 亿张缩略图,单图 0.2 MB,日出口有效载荷约 100 TB;平均约 1.16 GB/s,峰值按 5 倍则为 5.79 GB/s。展示带宽远大于上传 API 的请求规模,因此正文经对象存储和 CDN 交付,应用服务处理会话、权限和列表,不转发所有原图字节。
原图平均值从 4 MB 降为 2 MB,只减少原图存储与上传带宽;缩略图展示成本没有同比下降。若客户端由一次显示 10 张变成自动预取 50 张,即使用户停留时间不变也可能把下载量放大五倍。缩略图规格、预取数量和缓存命中率属于容量输入,应和后端分片一起评审。
初版可以让应用服务接收小文件并保存在同一持久文件系统,以减少上传签名、跨存储校验和回收复杂度;当实际网络吞吐或存储增长成为瓶颈时,再转为客户端直传。直传减少 API 的字节负载,但绝不能免除完成阶段的所有权和对象校验。
半成品不可见的路径
flowchart LR
C[移动端] --> U[上传会话API]
U --> M[(元数据与会话)]
C -->|受限凭据直传| O[(原图对象)]
C -->|complete| U
U --> Q[处理任务]
Q --> W[校验 解码 缩略图]
W --> O
W -->|记录变体完成| M
M --> P[发布事务]
P --> F[只查询READY的动态列表]
F --> G[权限检查后媒体交付]
对象上传完成后再提交元数据时,任何网络错误都可能造成结果未知。客户端重试 complete 使用同一会话 ID;服务端查询既有状态,已经完成则返回原结果,仍处理中则返回可轮询状态。不能因为超时重新创建一个照片 ID,把一次用户操作变成两条动态。
处理任务针对不可变原图版本运行。工作节点写缩略图到确定性版本键,校验成功后记录变体完成;协调器检查必需的规格集合齐全,才允许发布。某个可选高分辨率规格失败时是否允许先发布低分辨率,是产品策略,需要在必需集合中表达,不能靠“队列没有错误”间接判断。
sequenceDiagram
participant C as 客户端
participant O as 对象存储
participant D as 元数据事务
participant F as 动态列表
C->>O: 上传原图成功
C->>D: complete 会话
D->>D: 更新状态后注入失败
Note over D: 事务回滚为PENDING
F->>D: 查询READY
D-->>F: 不返回半成品
C->>D: 同会话重试
D->>D: 核验对象与变体后提交READY
F->>D: 再次查询READY
D-->>F: 返回一张完整照片
孤儿对象不能看到就删
孤儿回收先寻找超过宽限期且没有有效元数据引用的对象,再二次核对当前引用后删除。PENDING 会话仍然引用原图,不能只用“动态列表中不存在”作为孤儿判断,否则慢上传或失败重试的用户会失去已经上传的字节。回收还必须考虑任务中的变体、未提交分片以及对象版本。
宽限期应长于允许的上传会话和后台处理窗口,但不能无限延长。回收器扫描候选、删除与发布可能竞争,需要对会话施加状态转换,例如先原子标记 EXPIRED、拒绝之后的发布,再异步删对象。若发布事务与回收标记不共享权威状态,二次查询之后仍存在竞态,单纯多查一次不能得到严格保证。
权限收紧与照片删除必须进入展示和媒体两条路径。列表过滤只能阻止新的入口,旧媒体 URL 若长期有效仍可访问。对私密媒体用限时凭据会产生最长到期窗口;要求更快撤销时需要授权代理或边缘授权。已经被下载的图片不可由服务端删除,产品承诺应止于服务控制的访问和副本。
真实本地事务里的失败注入
examples/system-design/labs/14/upload.py 创建真实原图文件和 SQLite PENDING 记录,在更新 READY 后、事务提交前抛出异常。回滚后动态查询结果为空,原图文件仍存在。实验再放入一个无引用孤儿文件,回收只删孤儿,保留仍被待完成会话引用的原图;恢复提交后只出现一张照片。
1 | |
正常组退出 0,负例把所有“未 READY 文件”视为孤儿,识别到会误删可恢复会话后退出 2。日志与环境保存在 examples/system-design/evidence/14/。文件内容是标记字节,不是真实图像,未运行图片解码器、内容审核、对象云 API 或并发 GC;观察只覆盖回滚可见性和引用保护,不能宣称缩略图质量已经验证。
生产恢复首先保证展示查询仍按状态过滤,再恢复处理任务,最后恢复回收。若积压过大,暂停接收新的大图、缩减可选变体,比让半成品进入动态更可控。监控会话最老年龄、原图到 READY 的分位延迟、变体失败率和未引用对象字节数,不能只看上传 200 比例。
45 分钟设计中,前五分钟明确发布条件,八分钟估算图片与出口,十二分钟画直传和发布,十五分钟讨论双写失败及回收竞争,最后复核权限。追问“对象存储成功后为什么还要 complete”对应可信校验与状态推进;追问“数据库成功、通知丢了怎么办”对应事务内事件记录及重放,而非重新上传原图。
参考资料
- Amazon S3:Multipart upload overview:分片完成与终止是独立操作,未完成上传需要管理;本文不把本地文件模拟成真实 S3 测试。
- SQLite Transaction:实验使用的提交与回滚边界。






