关键决策回顾
回头看”光影”的 6 个月开发历程,技术方案的选型并不是一路顺风的。很多决策当时纠结了很久——甚至有几个选择,我后来发现并非最优解。
这一章,我想诚实地回顾每一个关键决策:当时考虑了什么、放弃了什么、结果如何。
背景:用户上传一张图片后,是先审核内容再生成缩略图,还是先生成缩略图再审核?
方案 A:先审后处理(最终选择)
用户上传 → 审核 → 通过 → 生成缩略图 → 可见
方案 B:先处理后审核
用户上传 → 生成缩略图 → 审核 → 通过后可见
方案 C:边审边处理
用户上传 → 审核 + 处理并行 → 汇合 → 可见
但这个决策有一个副作用:用户上传后需要等审核完成(200ms1s)再等处理完成(13s),总共 2~4 秒才能看到图片。我通过在审核通过后立即返回”上传成功”、后台异步处理缩略图来缓解这个问题。
背景:图片上传可以走服务器中转,也可以让客户端直接传到 OSS。
方案 A:客户端直传 OSS(最终选择)
客户端 → OSS(通过 STS 临时凭证)
方案 B:服务器中转
客户端 → 应用服务器 → OSS
方案 C:服务器中转 + 流式转发
客户端 → 应用服务器(流式转发)→ OSS
直传的风险:用户可能绕过审核,直接访问 OSS 上的原图。我通过以下措施防护:
背景:现代图片格式有 WebP 和 AVIF 两种选择。
方案 A:WebP 为主 + AVIF 辅助(最终选择)
方案 B:AVIF 为主 + WebP 兜底
方案 C:只用 WebP
方案 D:只用 JPEG(不转码)
前端实现:
事后评估:这个决策基本正确,但有一个遗憾——AVIF 的编码速度确实太慢。如果当时选择”AVIF 异步生成 + WebP 同步返回”,用户体验会更好。
背景:审核和处理是异步流程,可以用消息队列解耦,也可以直接在代码里调用。
方案 A:消息队列(RabbitMQ)(最终选择)
上传服务 → RabbitMQ → 审核服务 → RabbitMQ → 处理服务
方案 B:直接 HTTP 调用
上传服务 → HTTP 调用审核服务 → HTTP 调用处理服务
方案 C:数据库轮询
上传服务写入数据库 → 审核服务轮询数据库 → 处理服务轮询数据库
队列配置细节:
背景:缩略图可以预先全部生成,也可以在 CDN 边缘实时裁剪。
方案 A:预生成固定尺寸 + CDN 边缘裁剪兜底(最终选择)
方案 B:完全预生成所有尺寸
方案 C:完全依赖 CDN 边缘裁剪
背景:图片存储路径可以有多种命名方式。
方案 A:日期 + 哈希(最终选择)
originals/2024/06/a3/d4e5f6...jpg
方案 B:自增 ID
originals/00001.jpg, originals/00002.jpg, ...
方案 C:用户 ID + 时间戳
originals/user_123/1717286400.jpg
方案 D:UUID
originals/550e8400-e29b-41d4-a716-446655440000.jpg
┌───────────────────┬──────────────────────┬────────────────────────────────┐
│ 决策问题 │ 最终选择 │ 核心理由 │
├───────────────────┼──────────────────────┼────────────────────────────────┤
│ 审核/处理顺序 │ 先审后处理 │ 省处理资源,防违规进 CDN │
│ 上传链路 │ 客户端直传 OSS │ 省带宽,快,支持分片 │
│ 图片格式 │ WebP 主力 + AVIF 辅助│ 97% 支持率 + 渐进增强 │
│ 异步通信 │ RabbitMQ 消息队列 │ 解耦,缓冲,可扩展 │
│ 缩略图策略 │ 预生成 + 边缘裁剪 │ 95% 场景预生成,5% 边缘兜底 │
│ 文件命名 │ 日期 + 内容哈希 │ 可管理 + 无热点 + 可去重 │
│ CDN 选型 │ 阿里云 CDN │ 国内节点多,与 OSS 同厂商 │
│ 存储方案 │ 阿里云 OSS │ 无需运维,按需付费 │
│ 监控方案 │ Prometheus+Grafana │ 开源免费,生态丰富 │
│ 数据库 │ PostgreSQL │ JSON 支持好,事务可靠 │
└───────────────────┴──────────────────────┴────────────────────────────────┘
决策模式总结
回看这些决策,我发现了三个模式:
| 模式 | 体现在哪些决策里 | 换到别的项目还能不能复用 |
|---|---|---|
| 先定”谁来做”,再定”怎么做” | 客户端直传 OSS、先审后处理、OSS 加自建混合处理 | 能。技术选型前先回答”这件事该由谁承担” |
| 易变与不变分开 | 压缩参数、审核阈值、生命周期规则、迁移规则全部配置化 | 能。判据是”这个值一年后还会不会变” |
| 让系统自己暴露问题 | SSIM 抽样合格率、容量告警、规则执行计数、人审改判率 | 能。每一个”等用户投诉”的地方都该配一个指标 |
模式一:先确定”谁来做”,再决定”怎么做”
回头看那些看起来最”技术”的决策,其实都有一个业务问题在前面。
上传这一项,技术上有服务端中转、客户端直传、分片直传几种做法。真正决定的是”服务器有没有必要碰这张 8.7 MB 的文件”——答案是没有,于是直传。审核和处理这一项,技术上有同步和异步两种做法。真正决定的是”用户需不需要等压缩跑完”——答案是不需要,于是异步。图片处理这一项,技术上有 OSS 内置和自建两种做法。真正决定的是”OSS 的动作集合够不够用”——答案是常规变换够用、特殊变换不够,于是混合。
把”谁来做”定下来之后,“怎么做”往往只剩两三个选项。 大部分纠结不出来,是因为在还没回答”谁来做”的时候就开始比技术方案了。
模式二:把会变的东西和不会变的东西分开
回头看那些”当时觉得对、后来要改”的决策,共同点是把易变的东西写进了不易变的地方。
- 压缩质量参数写死在代码里 → 改成配置加二分搜索;
- 审核阈值写死在代码里 → 改成配置加灰度;
- 生命周期规则在控制台点出来的 → 改成 Terraform 管理的配置文件;
- 存储类型迁移规则写死在定时任务里 → 改成规则表加单向下沉。
反过来,那些确定不会变的东西就适合写死在代码里:图片的命名规则(日期加哈希)、CDN 缓存键的设计原则(只让影响内容的参数进 key)、审核记录的字段结构。
判据很简单:这个值一年后还会不会变? 会变,就别写进代码。
模式三:让系统自己暴露问题,而不是等人去查
这一条是我付出代价最多的。
早期我靠”用户投诉”发现问题——UI 截图被压糊,是设计师在群里说的;老王抱怨照片发糊,是在群里吐槽的;存储账单疯涨,是自己打开控制台看到的。每一个都靠”有人恰好注意到了”。
后来我把每条链路都补上了自动发现:压缩链路记录了 SSIM 抽样合格率,存储链路配了容量告警和规则执行计数,审核链路记了人审改判率,成本链路每月自动出对比表。从”出问题才知道”变成”指标不对就知道”,这才是这半年最大的变化。
三个模式串起来,其实是一句话:先问对问题,再把答案固化成配置,最后让它自己被监控着。
不是所有决策都是最优的。诚实地记录那些我后悔的选择:
| 当时的决定 | 付出的代价 | 现在怎么做 |
|---|---|---|
用 Vary: Accept 做格式协商 | 缓存被分裂,命中率随之下降,回源请求数上涨 | 格式写进 URL 路径,前端用 <picture> 声明候选 |
硬编码 quality=80 | UI 截图文字发虚,风景照浪费带宽 | 粗分类加二分搜索,找满足目标 SSIM 的最小质量 |
| 一条 OSS 规则管所有冷热 | 头像被归档、派生图被删后无法重建 | OSS 规则只执行”到点就办”,业务判断放应用层 |
| 靠用户投诉发现问题 | 问题拖好几天才被发现,垃圾文件持续堆积 | 每条链路配指标,指标不对就报警 |
后悔 1:用 Vary: Accept 做格式协商
当时的做法:服务端按请求的 Accept 头返回 AVIF 或 WebP,CDN 配 Vary: Accept 来做缓存。
为什么错:浏览器的 Accept 字符串组合远比我想象的多——不同浏览器、不同版本、不同系统各不相同。一个 URL 在边缘被拆成三四份缓存,缓存被分裂,命中率随之下降,回源请求数也跟着涨。
现在会怎么做:格式写进 URL 路径(xxx_400.webp),前端用 <picture> 声明候选。缓存对象和请求对象一一对应,命中率回到了 96%。
教训:任何让缓存键分裂的机制,代价都要按”分裂出的份数”来算,而不是按”理论上最多几种”来算。
后悔 2:用固定的质量参数处理所有图片
当时的做法:generate_thumbnails 里硬编码 quality=80。
为什么错:UI 截图被压到文字发虚,风景照又在浪费带宽——同一个 q80,UI 截图要 q90 才够,风景照 q72 就够了。
现在会怎么做:先用简单规则做粗分类,再用二分搜索找满足目标 SSIM 的最小质量参数。100 张热门图跑下来,平均体积从 78 KB 降到 52 KB,SSIM 达标率从 98% 提到 100%。
教训:全局默认值一定是为平均值调的,而平均值从来不代表任何一个具体的用户。
后悔 3:让 OSS 规则承担它承担不了的判断
当时的做法:想用一条生命周期规则解决所有冷热问题。
为什么错:OSS 规则只认”多少天没访问”,不认”这是头像”、不认”这是付费作品”、也不认”这张派生图还能不能重建”。结果是头像被归档(用户天天在用),某批派生图被删掉之后无法重建(永久 404)。
现在会怎么做:OSS 规则只做”到点就办”,业务判断全部交给应用层——分层决定放应用层,OSS 规则负责执行。
教训:把工具的能力边界想清楚,比把工具用起来更重要。 一个通用工具看起来什么都能做,正是最容易误用的地方。
后悔 4:靠用户投诉发现问题
当时的做法:用户说”我的图变糊了”,我才去查压缩参数;账单涨了,我才知道有垃圾文件。
为什么错:从用户开口到我定位问题,中间往往隔着好几天,这几天里每个新上传的用户都在遇到同样的问题。而垃圾文件是持续堆积的,晚一天就多一天的存储费。
现在会怎么做:每条链路都有对应指标——压缩看 SSIM 抽样合格率,存储看日增长和规则执行次数,审核看人审改判率,成本看单位成本。指标不对就报警,不等用户开口。
教训:监控的本质不是”收集数据”,而是”在用户发现之前先发现”。
最后
这四个后悔指向同一件事:我一开始把系统当成”一堆功能的集合”,后来才把它当成”一条有反馈回路的链路”。
前面列的那三个决策模式,其实都是这四个后悔的反面。决策模式是”做对了什么”,后悔是”做错了什么”,但它们的解法是同一个:把判断从人的直觉里拿出来,交给规则;把规则的结果从日志里拿出来,交给指标。
发现内容有问题?
告诉我们哪里需要改进。
