技术方案全景
一张图片的生命周期
做完缩略图方案后,我在白板上画了一张图——一张图片从上传到用户看到,到底要经历哪些环节?
最简实现:Flask + 本地磁盘,无 CDN,无压缩
用户层
用户浏览器 直接访问服务器
应用层
Flask 服务器 1 核 2G,5Mbps 带宽
存储层
本地磁盘 100GB,无冗余
客户端直传 OSS + 缩略图生成 + WebP 转换
用户层
用户浏览器 直传 OSS
应用层
上传服务 签发 STS 凭证
处理服务 缩略图 + WebP
存储层
阿里云 OSS 原图 + 缩略图
PostgreSQL 元数据
CDN 全球加速 + 内容审核 + 异步处理队列
用户层
用户浏览器 就近访问 CDN
CDN 层
阿里云 CDN 2800+ 节点,边缘裁剪
应用层
上传服务 × 3 签发凭证 + 回调
审核服务 × 2 鉴黄 + OCR
处理服务 × 4 多尺寸 + WebP/AVIF
数据层
阿里云 OSS 标准 + 低频 + 归档
RabbitMQ 异步处理队列
PostgreSQL 元数据 + 用户
生产级图片系统:12,000 用户,85,000 张图片,月成本 ¥1,720
用户层
用户浏览器 <picture> + 懒加载
CDN 层
阿里云 CDN 缓存命中率 96%,边缘裁剪
服务层
上传服务 × 3 STS 凭证 + 频率限制
审核服务 × 2 鉴黄 + OCR + 人审
处理服务 × 4 多尺寸 + EXIF
数据层
阿里云 OSS 标准 + 低频 + 归档
RabbitMQ upload→audit→process
辅助系统
Prometheus CDN 命中率 / 延迟
Grafana 监控面板 + 告警
六个环节,每一个都有技术挑战。我需要为每个环节选择合适的技术方案。
我整理了一份技术选型清单
上传方案
方案 A:直传服务器(当前方案)
客户端 → 我的服务器 → 对象存储
方案 B:客户端直传对象存储
客户端 → 对象存储(服务器只签发上传凭证)
方案 C:分片上传 + 断点续传
客户端 → 分片 → 对象存储(适合大文件)审核方案
方案 A:自研审核模型
自己训练 NSFW 检测模型,部署推理服务
方案 B:云服务 API
调用阿里云/腾讯云的内容安全 API
方案 C:开源模型
使用 NSFW.js / open_nsfw 等开源方案处理方案
方案 A:上传时处理(同步)
用户上传后立即生成所有尺寸
方案 B:上传后异步处理
上传后丢到消息队列,异步处理
方案 C:CDN 边缘处理
不预生成,用户请求时在 CDN 边缘节点按需裁剪存储方案
方案 A:服务器本地磁盘(当前方案)
简单但不可靠,无法扩展
方案 B:对象存储(S3/OSS/COS)
可靠、可扩展、按量付费
方案 C:对象存储 + 分层
热数据标准存储,冷数据低频/归档存储分发方案
方案 A:源站直出(当前方案)
所有用户直接访问源站
方案 B:全站 CDN
所有图片通过 CDN 分发
方案 C:CDN + 源站回源
CDN 缓存未命中时回源站拉取我的选择
基于当前的业务规模(用户数千人,日新增图片几百张),我做了第一版技术选型:
| 环节 | 选择 | 理由 |
|---|---|---|
| 上传 | 客户端直传 OSS | 减轻服务器压力,支持大文件 |
| 审核 | 云服务 API | 快速上线,准确率高 |
| 处理 | 异步处理 + CDN 边缘处理 | 平衡实时性和成本 |
| 存储 | 阿里云 OSS + 分层 | 可靠,可扩展 |
| 分发 | CDN + 回源 | 就近访问,降低延迟 |
整体架构:
最简实现:Flask + 本地磁盘,无 CDN,无压缩
用户层
用户浏览器 直接访问服务器
应用层
Flask 服务器 1 核 2G,5Mbps 带宽
存储层
本地磁盘 100GB,无冗余
客户端直传 OSS + 缩略图生成 + WebP 转换
用户层
用户浏览器 直传 OSS
应用层
上传服务 签发 STS 凭证
处理服务 缩略图 + WebP
存储层
阿里云 OSS 原图 + 缩略图
PostgreSQL 元数据
CDN 全球加速 + 内容审核 + 异步处理队列
用户层
用户浏览器 就近访问 CDN
CDN 层
阿里云 CDN 2800+ 节点,边缘裁剪
应用层
上传服务 × 3 签发凭证 + 回调
审核服务 × 2 鉴黄 + OCR
处理服务 × 4 多尺寸 + WebP/AVIF
数据层
阿里云 OSS 标准 + 低频 + 归档
RabbitMQ 异步处理队列
PostgreSQL 元数据 + 用户
生产级图片系统:12,000 用户,85,000 张图片,月成本 ¥1,720
用户层
用户浏览器 <picture> + 懒加载
CDN 层
阿里云 CDN 缓存命中率 96%,边缘裁剪
服务层
上传服务 × 3 STS 凭证 + 频率限制
审核服务 × 2 鉴黄 + OCR + 人审
处理服务 × 4 多尺寸 + EXIF
数据层
阿里云 OSS 标准 + 低频 + 归档
RabbitMQ upload→audit→process
辅助系统
Prometheus CDN 命中率 / 延迟
Grafana 监控面板 + 告警
这个架构看起来很美好。但我知道,真正的挑战在于每个环节的细节实现。
接下来,我要从最基础的开始——理解图片本身。
我的思考
思考 1
为什么我选择”客户端直传 OSS”而不是”服务器中转”?两种方案各有什么优缺点?
参考答案
服务器中转方案:
客户端 → 我的服务器 → OSS
优点:
- 服务器可以对文件做预处理(校验、压缩)
- 可以控制上传权限,更安全
- 上传逻辑集中管理
缺点:
- 服务器带宽是瓶颈(8.7MB 的图片 × 并发上传 = 带宽爆炸)
- 服务器磁盘 I/O 压力大
- 单点故障——服务器挂了就全挂了
- 服务器成本高(需要更大的带宽和磁盘)客户端直传方案:
客户端 → OSS(服务器只签发上传凭证)
优点:
- 上传流量不经过服务器,节省带宽
- OSS 的上传能力远强于单台服务器(支持分片、断点续传)
- 服务器只做轻量操作(签发凭证),不容易成为瓶颈
缺点:
- 需要设计安全的凭证机制,防止滥用
- 客户端需要集成 OSS SDK
- 服务器无法在上传时预处理
典型实现:
```python
import oss2
import time
def generate_upload_credentials(user_id, file_type):
"""生成临时上传凭证"""
auth = oss2.StsAuth(
access_key_id='your_key',
access_key_secret='your_secret',
security_token=sts_token
)
# 生成带签名的上传 URL,15 分钟有效
bucket = oss2.Bucket(auth, 'oss-cn-beijing.aliyuncs.com', 'my-bucket')
object_key = f'uploads/{user_id}/{uuid.uuid4().hex}.{file_type}'
upload_url = bucket.sign_url('PUT', object_key, 15 * 60)
return {
'upload_url': upload_url,
'object_key': object_key,
'expires_in': 900,
}对于图片场景,客户端直传几乎总是更好的选择。 图片文件大,走服务器中转纯属浪费带宽。
思考 2
架构图中,审核和处理是并行还是串行?如果审核发现图片违规,已经处理好的缩略图怎么办?
参考答案
我的方案:先审后处理
上传 → 审核 → (通过) → 异步处理 → 存储 → CDN
→ (拒绝) → 删除原图 → 通知用户原因:
避免资源浪费:如果先处理再审核,违规图片的处理算力白花了。处理 5 种缩略图需要 1~2 秒,如果最终是违规图片,这些算力就浪费了。
避免”违规内容短暂可访问”的问题:如果先处理再审核,在审核完成前,违规图片的缩略图可能已经被 CDN 缓存了。
审核本身很快:云 API 的审核通常在 200ms~1s 内返回结果,不会成为上传流程的瓶颈。
审核通过后的处理流程:
def on_upload_complete(object_key, user_id):
"""图片上传完成后的处理流程"""
# 第一步:审核(同步,快速判断)
audit_result = content_audit(object_key)
if not audit_result.passed:
# 审核不通过:删除原图,通知用户
oss_client.delete_object(object_key)
notify_user(user_id, "图片内容不符合规范,请修改后重新上传")
return
# 第二步:审核通过,异步处理
message_queue.publish({
'task': 'process_image',
'object_key': object_key,
'user_id': user_id,
})
# 同时更新数据库状态
db.execute(
"UPDATE images SET status = 'processing' WHERE object_key = %s",
(object_key,)
)
def process_image_async(object_key):
"""异步处理图片"""
# 下载原图
image_data = oss_client.get_object(object_key)
# 生成多种尺寸
thumbnails = generate_thumbnails(image_data)
# 上传缩略图到 OSS
for size_name, thumb_data in thumbnails.items():
thumb_key = f'thumbs/{object_key}_{size_name}.webp'
oss_client.put_object(thumb_key, thumb_data)
# 更新数据库状态
db.execute(
"UPDATE images SET status = 'ready', thumbs = %s WHERE object_key = %s",
(json.dumps(thumbnails), object_key)
)极端情况:如果审核漏放了一张违规图片怎么办?
- CDN 层面:支持紧急刷新(purge),可以快速从 CDN 节点删除
- OSS 层面:直接删除源文件,CDN 缓存过期后自动不可访问
- 数据库层面:标记为违规,前端不再展示
