关键决策回顾
那些让我纠结到凌晨三点的选择
回头看”光影”的 6 个月开发历程,技术方案的选型并不是一路顺风的。很多决策当时纠结了很久——甚至有几个选择,我后来发现并非最优解。
这一章,我想诚实地回顾每一个关键决策:当时考虑了什么、放弃了什么、结果如何。
决策 1:为什么先审后处理?
背景:用户上传一张图片后,是先审核内容再生成缩略图,还是先生成缩略图再审核?
方案 A:先审后处理(最终选择)
用户上传 → 审核 → 通过 → 生成缩略图 → 可见
方案 B:先处理后审核
用户上传 → 生成缩略图 → 审核 → 通过后可见
方案 C:边审边处理
用户上传 → 审核 + 处理并行 → 汇合 → 可见DECISION_1 = {
'问题': '审核与处理的顺序',
'选择': '方案 A:先审后处理',
'理由': [
'1. 节约处理资源:违规图片不生成缩略图(每张图 5 个缩略图 = 5 次处理)',
'2. 防止违规内容进入 CDN:审核不通过的图片不会出现在任何缓存中',
'3. 简化状态机:图片状态只有 上传中→审核中→处理中→就绪',
],
'替代方案': {
'方案 B(先处理后审核)': {
'优点': '用户等待时间更短(审核和处理并行)',
'缺点': [
'违规图片也会生成缩略图,浪费处理资源',
'违规图片可能短暂出现在 CDN 缓存中',
'审核不通过后需要额外清理缩略图和 CDN 缓存',
],
},
'方案 C(并行)': {
'优点': '最快完成',
'缺点': [
'实现复杂度高(需要协调两个异步任务的结果)',
'如果审核不通过,需要取消正在进行的处理任务',
'对于"光影"的规模,收益不大',
],
},
},
'结果': '正确决策。实际运行中约 2% 的图片审核不通过,'
'节省了 2% × 5 = 10% 的处理资源',
}但这个决策有一个副作用:用户上传后需要等审核完成(200ms1s)再等处理完成(13s),总共 2~4 秒才能看到图片。我通过在审核通过后立即返回”上传成功”、后台异步处理缩略图来缓解这个问题。
决策 2:为什么客户端直传 OSS?
背景:图片上传可以走服务器中转,也可以让客户端直接传到 OSS。
方案 A:客户端直传 OSS(最终选择)
客户端 → OSS(通过 STS 临时凭证)
方案 B:服务器中转
客户端 → 应用服务器 → OSS
方案 C:服务器中转 + 流式转发
客户端 → 应用服务器(流式转发)→ OSSDECISION_2 = {
'问题': '上传链路设计',
'选择': '方案 A:客户端直传 OSS',
'理由': [
'1. 带宽成本:图片不经过服务器,节省服务器带宽',
'2. 服务器压力:上传是带宽密集型操作,不走服务器可以减少实例数',
'3. 速度:客户端直连 OSS 比经过服务器更快(少一跳)',
'4. 分片上传:OSS SDK 原生支持大文件分片上传',
],
'替代方案': {
'方案 B(服务器中转)': {
'优点': [
'服务器可以在上传时立即处理(压缩、校验、打水印)',
'更容易控制上传权限(不需要 STS)',
'可以做上传限速',
],
'缺点': [
'服务器带宽成本高(ECS 带宽比 OSS 贵 3~5 倍)',
'服务器是单点(上传高峰时可能成为瓶颈)',
'大文件上传可能超时(服务器需要完整接收再转发)',
],
},
'方案 C(流式转发)': {
'优点': '不需要完整接收再转发,内存占用更小',
'缺点': '实现复杂,仍然消耗服务器带宽',
},
},
'结果': '正确决策。直传让上传服务只需要处理 STS 签发和回调,'
'3 台低配服务器即可支撑日均 450 张上传',
}直传的风险:用户可能绕过审核,直接访问 OSS 上的原图。我通过以下措施防护:
# 直传安全措施
SECURITY_MEASURES = {
'STS 临时凭证': {
'有效期': '15 分钟',
'权限': '仅允许 PUT 到 uploads/ 前缀',
'限制': '单文件最大 20 MB',
},
'Bucket 策略': {
'原图目录': '私有读写(需要签名 URL 才能访问)',
'缩略图目录': '公共读(通过 CDN 分发)',
},
'上传回调': {
'机制': 'OSS 上传完成后回调应用服务器',
'校验': '验证文件类型、大小、上传者身份',
},
}决策 3:为什么选 WebP 作为主力格式?
背景:现代图片格式有 WebP 和 AVIF 两种选择。
方案 A:WebP 为主 + AVIF 辅助(最终选择)
方案 B:AVIF 为主 + WebP 兜底
方案 C:只用 WebP
方案 D:只用 JPEG(不转码)DECISION_3 = {
'问题': '图片格式选型',
'选择': '方案 A:WebP 为主 + AVIF 辅助',
'理由': [
'1. WebP 浏览器支持率 97%(2024 年),几乎全覆盖',
'2. WebP 比 JPEG 小 30~50%,效果显著',
'3. AVIF 比 WebP 再小 30%,但支持率只有 92%,且编码速度慢 5~10 倍',
'4. 渐进增强:先保证所有人都能看到图(WebP),再给支持的浏览器更好的体验(AVIF)',
],
'替代方案对比': {
'AVIF 为主': {
'优点': '体积最小,画质最好',
'缺点': [
'编码速度慢(单张 5MB 照片转 AVIF 需要 3~5 秒,WebP 只要 0.5 秒)',
'浏览器支持率 92%(2024 年初),8% 的用户看到 JPEG 兜底',
'服务端需要更强的 CPU(处理服务成本增加)',
'CDN 边缘处理对 AVIF 支持不完善',
],
},
'只用 WebP': {
'优点': '简单,一套格式走天下',
'缺点': '放弃了 AVIF 带来的额外 30% 体积优化',
},
'不转码(JPEG)': {
'优点': '零处理成本,零延迟',
'缺点': '体积大 2~3 倍,CDN 流量费翻倍',
},
},
'实际方案': (
'<picture> 标签做格式协商:\n'
'AVIF → WebP → JPEG\n'
'浏览器按优先级选择第一个支持的格式'
),
}前端实现:
<!-- 渐进增强的 <picture> 标签 -->
<picture>
<source
srcset="https://cdn.guangying.com/thumbs/avif/2024/06/a3/abc.avif"
type="image/avif"
/>
<source
srcset="https://cdn.guangying.com/thumbs/webp/2024/06/a3/abc.webp"
type="image/webp"
/>
<img
src="https://cdn.guangying.com/thumbs/jpeg/2024/06/a3/abc.jpeg"
alt="摄影师作品"
loading="lazy"
/>
</picture>事后评估:这个决策基本正确,但有一个遗憾——AVIF 的编码速度确实太慢。如果当时选择”AVIF 异步生成 + WebP 同步返回”,用户体验会更好。
决策 4:为什么用消息队列而不是直接调用?
背景:审核和处理是异步流程,可以用消息队列解耦,也可以直接在代码里调用。
方案 A:消息队列(RabbitMQ)(最终选择)
上传服务 → RabbitMQ → 审核服务 → RabbitMQ → 处理服务
方案 B:直接 HTTP 调用
上传服务 → HTTP 调用审核服务 → HTTP 调用处理服务
方案 C:数据库轮询
上传服务写入数据库 → 审核服务轮询数据库 → 处理服务轮询数据库DECISION_4 = {
'问题': '异步流程的通信方式',
'选择': '方案 A:消息队列(RabbitMQ)',
'理由': [
'1. 解耦:上传服务不需要知道审核和处理的细节',
'2. 缓冲:如果审核/处理服务宕机,消息队列会暂存任务',
'3. 可扩展:每个服务可以独立扩容',
'4. 可观测:队列深度是很好的监控指标',
],
'替代方案': {
'方案 B(HTTP 直接调用)': {
'优点': '实现简单,不需要额外的基础设施',
'缺点': [
'紧耦合:任何一个服务宕机,整个链路中断',
'没有缓冲:突发流量直接冲击下游服务',
'没有重试机制(需要自己实现)',
'难以监控(没有一个集中的地方看任务状态)',
],
},
'方案 C(数据库轮询)': {
'优点': '不需要额外组件',
'缺点': [
'延迟高(轮询间隔 1~5 秒)',
'数据库压力大(每秒 N 次查询)',
'多实例竞争同一个任务(需要分布式锁)',
],
},
},
'结果': '正确决策。RabbitMQ 在"光影"的规模下完全够用,'
'月费 150 元换来的是整个链路的可靠性',
}队列配置细节:
# RabbitMQ 交换机和队列配置
QUEUE_CONFIG = {
'exchange': 'image-events',
'queues': {
'upload-events': {
'binding_key': 'image.uploaded',
'consumers': ['audit-service'],
'dlx': 'upload-dead-letter', # 死信队列
},
'audit-events': {
'binding_key': 'image.audited.*',
'consumers': ['process-service', 'notification-service'],
'dlx': 'audit-dead-letter',
},
'process-events': {
'binding_key': 'image.processed',
'consumers': ['notification-service'],
'dlx': 'process-dead-letter',
},
},
'retry_policy': {
'max_retries': 3,
'delay_seconds': [30, 300, 3600], # 30s → 5min → 1h
},
}决策 5:为什么用 CDN 边缘裁剪而不是预生成所有尺寸?
背景:缩略图可以预先全部生成,也可以在 CDN 边缘实时裁剪。
方案 A:预生成固定尺寸 + CDN 边缘裁剪兜底(最终选择)
方案 B:完全预生成所有尺寸
方案 C:完全依赖 CDN 边缘裁剪DECISION_5 = {
'问题': '缩略图生成策略',
'选择': '方案 A:预生成固定尺寸 + 边缘裁剪兜底',
'理由': [
'1. 预生成保证了常用尺寸的访问速度(CDN 缓存命中)',
'2. 边缘裁剪解决了"新需求尺寸"的问题(不需要重新处理历史图片)',
'3. 渐进策略:先上线预生成,验证后再开通边缘裁剪',
],
'替代方案': {
'方案 B(完全预生成)': {
'优点': 'CDN 缓存命中率最高,不依赖边缘处理能力',
'缺点': [
'每次新增尺寸需要重新处理全量图片',
'存储空间浪费(很多尺寸可能永远不会被请求)',
'响应新需求的周期长(全量处理需要数小时)',
],
},
'方案 C(完全边缘裁剪)': {
'优点': '灵活,任何尺寸都能实时生成,零存储浪费',
'缺点': [
'首次访问慢(边缘节点需要回源获取原图再裁剪)',
'CDN 边缘处理有额外费用',
'参数注入攻击风险(用户随意输入尺寸可能消耗资源)',
],
},
},
'结果': '正确决策。5 个预生成尺寸覆盖了 95% 的场景,'
'边缘裁剪处理了 5% 的特殊需求(如邮件模板、社交媒体分享卡片)',
}决策 6:为什么文件命名用日期+哈希而不是自增 ID?
背景:图片存储路径可以有多种命名方式。
方案 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.jpgDECISION_6 = {
'问题': '图片文件命名策略',
'选择': '方案 A:日期 + 内容哈希',
'格式': 'originals/{year}/{month}/{hash_prefix}/{content_hash}.{ext}',
'示例': 'originals/2024/06/a3/d4e5f67890abcdef.jpg',
'理由': [
'1. 日期前缀:便于按时间范围扫描和管理(生命周期规则)',
'2. 哈希前缀:OSS 内部按前缀分片,避免热点目录',
'3. 内容哈希:相同内容的图片自动去重(省存储)',
'4. 无序性:无法通过 URL 猜测其他图片的路径(安全)',
],
'替代方案': {
'方案 B(自增 ID)': {
'优点': '简单,有序',
'缺点': [
'容易遍历(知道 00001 就能猜到 00002)',
'单目录文件过多(影响 OSS 列举性能)',
],
},
'方案 C(用户 ID)': {
'优点': '便于按用户管理',
'缺点': [
'大用户的目录会变成热点',
'用户删除账号后不方便批量处理',
],
},
'方案 D(UUID)': {
'优点': '全局唯一,无序',
'缺点': '无法按时间管理,文件名太长',
},
},
'结果': '正确决策。日期+哈希在管理性、性能和安全性之间取得了很好的平衡',
}所有决策一览
┌───────────────────┬──────────────────────┬────────────────────────────────┐
│ 决策问题 │ 最终选择 │ 核心理由 │
├───────────────────┼──────────────────────┼────────────────────────────────┤
│ 审核/处理顺序 │ 先审后处理 │ 省处理资源,防违规进 CDN │
│ 上传链路 │ 客户端直传 OSS │ 省带宽,快,支持分片 │
│ 图片格式 │ WebP 主力 + AVIF 辅助│ 97% 支持率 + 渐进增强 │
│ 异步通信 │ RabbitMQ 消息队列 │ 解耦,缓冲,可扩展 │
│ 缩略图策略 │ 预生成 + 边缘裁剪 │ 95% 场景预生成,5% 边缘兜底 │
│ 文件命名 │ 日期 + 内容哈希 │ 可管理 + 无热点 + 可去重 │
│ CDN 选型 │ 阿里云 CDN │ 国内节点多,与 OSS 同厂商 │
│ 存储方案 │ 阿里云 OSS │ 无需运维,按需付费 │
│ 监控方案 │ Prometheus+Grafana │ 开源免费,生态丰富 │
│ 数据库 │ PostgreSQL │ JSON 支持好,事务可靠 │
└───────────────────┴──────────────────────┴────────────────────────────────┘决策模式总结
回看这些决策,我发现了三个模式:
DECISION_PATTERNS = {
'模式 1:渐进增强优于一步到位': (
'WebP 先行 + AVIF 后补;预生成先行 + 边缘裁剪后补。'
'不追求一步到位的最优解,而是先跑起来再迭代。'
),
'模式 2:托管服务优于自建': (
'OSS > 自建存储;云 CDN > 自建 CDN;云审核 > 自建模型。'
'小团队的时间应该花在业务逻辑上,不是基础设施。'
),
'模式 3:解耦优于简单': (
'消息队列 > 直接调用;STS 临时凭证 > 固定密钥。'
'解耦带来的可扩展性和可靠性,远远超过额外的复杂度。'
),
}那些”如果重来”的决策
不是所有决策都是最优的。诚实地记录那些我后悔的选择:
REGRETS = [
{
'决策': '最初用了本地磁盘存储图片',
'问题': '服务器磁盘满了,迁移到 OSS 花了一整天',
'教训': '一开始就用对象存储,不要自建文件存储',
'影响': '中等(迁移期间有 2 小时不可用)',
},
{
'决策': '审核只做了图片鉴黄,没做 OCR 文字审核',
'问题': '有用户上传带违规文字的图片,被投诉后紧急加了 OCR',
'教训': '内容审核要全面,不要只做最明显的',
'影响': '低(快速修复了)',
},
{
'决策': '一开始缩略图只生成了 JPEG 格式',
'问题': '后来加 WebP 支持时,需要重新处理所有 5 万张图片',
'教训': '格式支持要提前规划,一开始就生成多格式',
'影响': '中等(处理花了 2 天,期间部分图片无 WebP)',
},
{
'决策': '监控告警上线太晚(第 4 个月才加)',
'问题': '前 4 个月出了好几次故障,都是用户反馈了才知道',
'教训': '监控要和第一个功能一起上线',
'影响': '高(影响了用户信任)',
},
]本节小结
✅ 我学到了什么:
- 每个技术选型都有取舍,关键是理清核心需求和约束条件
- “光影”的决策模式:渐进增强、托管优先、解耦设计
- 先审后处理、直传 OSS、消息队列——这些选择在事后看都是正确的
- 最大的遗憾是监控上线太晚,应该在第一天就搭建
⚠️ 踩过的坑:
- 一开始用本地磁盘存储,差点丢数据
- 缩略图只生成 JPEG,后来加 WebP 时需要全量重新处理
- 没有做文字审核,被用户投诉后才补上
🎯 下一步:最后一节——从所有决策中提炼出的设计原则,以及”光影”从 0 到 1 的完整数据回顾。
我的思考
思考 1
如果”光影”面向的是海外市场(欧美用户为主),这些技术决策会有哪些不同?
海外市场的技术选型会有显著差异:
云服务商:
- 国内:阿里云(OSS + CDN + 内容安全)
- 海外:AWS(S3 + CloudFront)或 Cloudflare(R2 + CDN)
CDN 选型:
- 国内:阿里云 CDN(国内 2800+ 节点)
- 海外:Cloudflare(全球 300+ 城市,免费计划无限流量)
→ 这意味着 CDN 流量费可能为 0!
图片格式:
- 海外 WebP 支持率更高(>98%)
- AVIF 支持率也更高(Safari 16+ 已支持)
- 可以更激进地使用 AVIF
内容审核:
- 海外没有强制鉴黄要求
- 但需要遵守 GDPR(欧盟)的数据隐私法规
- 用户有权删除所有数据("被遗忘权")
- 需要更完善的数据删除机制
存储 Region:
- 主要用户在北美和欧洲
- S3 部署在 us-east-1 + eu-west-1
- CloudFront 自动就近分发
成本差异:
- Cloudflare 免费计划让 CDN 成本降到 0
- S3 存储费略高于 OSS($0.023/GB vs ¥0.12/GB ≈ $0.017/GB)
- 但带宽费更贵(AWS 数据传出 $0.09/GB)
- 综合:海外运营成本可能更低(得益于 Cloudflare 免费流量)思考 2
回顾所有决策,如果只能给一个新项目一条建议,你会说什么?
监控先行,其他靠后。
为什么?
1. 没有监控,你不知道系统是否正常
- CDN 命中率下降了?你不知道
- 存储空间快满了?你不知道
- 图片处理失败了?你不知道
- 用户看到的是错误图片?你不知道
2. 没有监控,你无法做决策
- WebP 转换节省了多少流量?不知道
- 冷热分层的收益有多大?不知道
- 缓存命中率是多少?不知道
- 没有 data,所有优化都是拍脑袋
3. 没有监控,你无法发现后悔的决策
- 如果第 1 天就有存储监控,我就能在垃圾堆积前发现问题
- 如果第 1 天就有 CDN 监控,我就能在用户抱怨前发现延迟
- 如果第 1 天就有错误监控,我就能在批量处理失败时第一时间知道
最佳实践:
- 第一行代码上线前,先搭好监控
- 监控什么:错误率、延迟、流量、存储
- 告警什么:任何超过阈值的情况
- "光影"最大的遗憾不是选错了技术,而是太晚看到数据思考 3
消息队列选 RabbitMQ 还是 Kafka?对”光影”这个规模,有没有必要换?
“光影”的规模用 RabbitMQ 完全够用,不需要换 Kafka。
对比:
RabbitMQ:
- 适合中小规模(日处理 < 1000 万条消息)
- 功能丰富(优先级队列、延迟消息、死信队列)
- 部署简单(单节点即可)
- 运维成本低
- "光影"日均消息量:450 × 3(上传→审核→处理)= 1350 条
→ RabbitMQ 处理这个量级绰绰有余
Kafka:
- 适合超大规模(日处理 > 1 亿条消息)
- 吞吐量极高(百万级 TPS)
- 持久化能力强(日志存储)
- 需要集群部署(至少 3 broker)
- 运维复杂度高
何时考虑换 Kafka:
- 用户量 > 100 万
- 日均上传 > 10 万张
- 需要消息回溯(重新消费历史消息)
- 需要流处理(实时分析访问日志)
结论:
不要过早优化基础设施。
RabbitMQ → Kafka 的迁移成本远高于继续使用 RabbitMQ 的成本。
等到 RabbitMQ 真的成为瓶颈再考虑。