问题与范围
平台每天接收 1000 万张原始图片,客户端请求不同尺寸、裁剪区域和输出格式。请设计从原图存储、转换任务、派生图缓存到 CDN 返回的系统。原图必须可重新处理;图片编辑器的图层协作、内容审核模型和专业色彩校准不在范围内。题设数字用于容量推导,不代表行业基准。
面试官在考察什么
重点是区分原图、转换请求和派生资产的身份,并说明异步处理与低延迟读取如何共存。高质量回答会处理确定性缓存键、重复任务合并、队列背压、像素和解压炸弹、对象访问权限、格式协商以及原图删除后的派生清理。
回答前要澄清的问题
- 是否允许任意宽高、裁剪和滤镜?最大像素、文件大小和处理时间是多少?
- 输出需要哪些格式、质量等级、色彩空间和动画支持?
- 首次请求必须同步返回,还是可以返回
202与任务状态? - 原图和派生图的保留期、租户隔离和跨区域要求是什么?
- 处理结果是否公开可读,还是每个 URL 都需要签名授权?
30 秒回答框架
“我会把原图内容寻址存储,把规范化的转换参数编码为派生键。读取先查 CDN 和派生对象;未命中时以幂等键提交任务,按原图和租户分片进入队列。工作进程在隔离环境内限制像素、内存、CPU、解压和输出大小,写入临时对象后原子发布。失败任务带版本和重试预算,重复请求共享同一任务。访问层执行签名 URL、格式协商和缓存控制,删除原图时异步清理派生资产。”
分步深入设计
上传 API 只负责鉴权、校验摘要和写入原图对象存储。元数据记录 asset_id、租户、内容哈希、媒体类型、宽高、帧数、色彩信息、扫描状态和保留策略。对象键不直接暴露用户文件名;内容哈希可用于跨请求去重,但租户授权仍必须独立校验。对原图做魔数和解码验证,不能只信任扩展名或 Content-Type。
转换请求先规范化参数:宽高上限、裁剪坐标、缩放算法、质量、旋转、背景色和输出格式都转成稳定序列。派生键可表示为 hash(originalbytes, normalizedtransform, processor_version);处理器升级时版本变化,避免新旧算法覆写同一对象。参数拒绝负数、NaN、极大比例和递归滤镜。
读路径依次检查 CDN、派生对象索引和任务去重表。命中可用派生图就返回带 Cache-Control、ETag 和内容类型的响应。未命中时,若产品允许延迟,创建 PENDING 任务并返回状态 URL;常见小图可以在严格预算内同步处理。唯一约束保护 (tenantid, derivedkey),多个相同请求共享任务,不重复消耗计算。
队列按租户和原图哈希分区,工作进程按像素成本而不是请求数计量。租户配额、全局并发上限和优先级队列共同提供背压;一张超大图不能占满所有进程。任务带租约和处理器版本,工作进程崩溃后可重试。指数退避只针对可恢复错误,参数错误和不支持格式直接失败并记录原因。
处理沙箱限制 CPU、内存、临时磁盘、解压比、帧数和输出尺寸,禁止访问内网。解码后重新计算实际像素和帧数,防止压缩炸弹。写入临时对象并校验摘要、尺寸和格式后,再通过条件写或版本化对象原子发布;客户端永远看不到半成品。对 SVG、ICC 配置和元数据做明确策略,避免脚本执行、路径穿越和隐私泄露。
缓存策略按派生键长期缓存,原图删除或权限变更通过资产事件触发派生索引和 CDN 失效。公开 URL 与私有 URL 分开:私有资产使用短期签名 URL,签名覆盖租户、派生键、过期时间和允许的响应头。Vary 只包含确实影响输出的协商维度,避免 User-Agent 或任意查询参数制造缓存爆炸。
监控原图写入成功率、转换队列年龄、按像素成本的吞吐、缓存命中率、重复任务合并率、p95 转换延迟、失败分类、沙箱资源峰值、派生对象增长和 CDN 5xx。对账任务比较原图元数据、任务终态、派生索引和对象列表,清理孤儿对象。故障注入覆盖工作进程被杀、对象存储超时、重复消息、处理器升级、CDN 失效失败和租户超额。
高质量示范回答
“我会把原图作为不可变、可寻址的资产,把规范化转换参数和处理器版本组成派生键。请求先查 CDN 和派生对象;未命中时通过唯一约束创建共享任务,返回已存在任务或状态 URL。队列按租户和像素成本调度,工作进程在隔离沙箱内限制解压比、内存、CPU、帧数和输出大小,临时写入后校验并原子发布。
私有访问使用覆盖租户、派生键和过期时间的签名 URL,公开响应设置正确的 ETag、Cache-Control 和内容类型。原图删除事件清理派生索引和 CDN。指标覆盖命中率、队列年龄、p95 延迟、资源峰值和失败分类;对账发现孤儿对象。这样相同请求不会重复计算,恶意图片不能耗尽容量,处理器升级也不会污染旧派生结果。”
常见错误
- 用文件名作为身份 → 改名或同名上传会覆盖资产 → 使用内容哈希和资产 ID。
- 把所有参数直接拼 URL → 参数顺序不同导致缓存碎片 → 先规范化再生成派生键。
- 每次未命中都新建任务 → 热门图片形成计算风暴 → 按租户和派生键幂等合并。
- 只限制文件字节数 → 压缩炸弹仍可展开成巨量像素 → 限制解码后像素、帧数和解压比。
- 同步处理所有请求 → 慢任务阻塞读取链路 → 异步队列与受控同步快路径并存。
- 直接覆盖最终对象 → 客户可能读取半成品 → 临时对象校验后原子发布。
- 签名只覆盖 URL → 参数或响应头可被篡改 → 签名租户、派生键、过期时间和策略。
- 删除原图不清理派生图 → 私有数据继续可访问 → 用资产事件驱动索引和 CDN 清理。
追问与回答
追问一:为什么缓存键要包含处理器版本?
同一参数在不同算法、库或安全补丁下可能产生不同字节。版本纳入派生键可让新处理器并行生成结果,再按策略淘汰旧版本,不会静默覆盖正在使用的对象。
追问二:如何处理动画图片?
把帧数、总时长和输出策略纳入参数与成本模型。限制最大帧数和总像素;需要首帧缩略图时生成独立派生键,避免每次读取都解码完整动画。
追问三:何时返回 202?
当转换成本不可预测、输出较大或队列有积压时返回 202、任务 ID 和重试建议。小尺寸常见格式可在严格 CPU 和时间预算内同步完成,超时就转为异步任务。
追问四:如何防止缓存投毒?
派生键只由服务端规范化参数生成,不接受客户端指定对象键。响应类型、长度和摘要在发布前校验;私有缓存绑定租户和签名,禁止跨租户复用。
追问五:原图替换后旧 URL 怎么办?
原图采用不可变版本 ID。替换产生新资产或新版本事件,旧派生图按保留策略继续可读或失效;不能让同一 URL 在缓存中悄悄变成另一张图片。
追问六:如何保证费用公平?
按解码像素、输出像素和滤镜复杂度计费,而非只按请求数。租户令牌桶、并发上限和每日预算控制昂贵任务;超过预算返回明确的配额状态并保留审计记录。