用户上传的原图直接存原始文件,一年后存储账单翻了四倍。明细里原因不是"用户变多了"——上传量只涨三成,涨的是单张图的平均大小(摄像头从 12MP 到 48MP 用了两年),而系统同时保留原图、一份"压缩版"和所有历史中间产物,没人记得哪些能删。媒体成本容易失控,因为它由四个独立账目构成,而大多数人只盯第一项。
1. 四项成本与"存原图"的取舍
| 成本项 | 由什么驱动 | 失控时的表现 |
|---|---|---|
| 存储 | 对象数 × 平均大小 × 副本数 | 账单线性上涨,找不到突增点 |
| 带宽 | 回源次数 × 单次大小 + 出网流量 | CDN 缓存命中率下降即放大 |
| 转码算力 | 任务数 × 单任务耗时 × 实例单价 | 队列积压,转码延迟进入小时级 |
前三项账单上有数,第四项没有,但它真正拉开差距:一个"缩略图静默失败"的问题能让几个人反复查一周。
"存原图"与"存多档规格"的取舍不是二元选择。要点是原图必须存(它是唯一不可再生的输入),而多档规格是派生产物。派生产物每多一档,存储和转码成本各增加一份,而适配收益是递减的:
| 策略 | 存储成本 | 转码成本 | 适配性 | 适用场景 |
|---|---|---|---|---|
| 只存原图 | 最低 | 零 | 最差,客户端自己缩放,流量反而高 | 内网、超大图、归档 |
| 原图 + 3 档 | 约 1.2–1.6 倍 | 中 | 适配列表、详情、封面 | 图文为主的站点 |
具体倍率按自己的图片分布实测,不要照搬。关键判断是:档位由实际使用分布决定,不由设备种类数决定。设备上百种,但真实访问里往往 90% 的展示宽度只落在两三个值附近——按设备数设计档位,等于为长尾付全部成本只服务少数请求。档位应当是一份可核对的清单,而不是散落在模板里的魔法数字:
─── 派生规格声明:每档都能被核对与重建
renditions:
- name: thumb width: 240 fit: cover format: [webp, jpg]
- name: list width: 640 fit: cover format: [webp, jpg]
- name: detail width: 1280 fit: inside format: [webp, jpg]
policies:
source: keep-forever ── 原图永久保留,唯一可重建源头
derived-ttl-days: 0 ── 派生不设过期,靠缺失核对重建
tmp-ttl-hours: 24 ── 中间产物过期清理
2. 转码时机与图片格式
转码有三个时机,每个都有明确的缺点,选择它们等于选择接受哪种缺点:
| 时机 | 优点 | 缺点 | 什么时候选 |
|---|---|---|---|
| 上传时同步 | 访问时零等待,链路简单 | 上传接口变慢,大文件容易超时 | 尺寸可控、单文件小时 |
| 上传后异步 | 上传快,可重试可限流 | 有窗口期内无缩略图 | 绝大部分场景 |
| 首次访问时 | 零存储浪费,只生成被用到的 | 首屏慢,第一次访问必然慢一档 | 访问分散、图量极大 |
首次访问转码的陷阱是它把转码成本变成了用户可感知延迟,而且只出现在最冷门的内容上——恰恰是最需要打开看看的内容。定时批量的陷阱是队列深度与吞吐不匹配时积压会一直涨,而积压本身不告警。
格式选择的判据是兼容性下限与体积收益:WebP 在主流浏览器可用,AVIF/JPEG XL 体积更小但解码与工具链要求更高。稳妥做法是渐进增强——按 Accept 头协商,同一张图存两档格式,老客户端永远拿得到 JPEG:
─── 按 Accept 协商格式,回退永远存在
map $http_accept $img_suffix {
default ".jpg";
"~*image/webp" ".webp";
"~*image/avif" ".avif";
}
location ~ ^/media/(?<key>.+)\.jpg$ {
try_files /media/$key$img_suffix /media/$key.jpg =404;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary Accept;
}
Vary: Accept 必须保留。缺了它,CDN 会把 WebP 版本发给不支持它的客户端,或者反之——这类错误只在部分终端上出现,排查成本极高。
3. 视频的分片与码率阶梯
视频比图片贵一个量级,贵在多档并存的存储与转码算力:HLS 每个清晰度档位都是一份完整数据,档位翻倍则存储与转码时间同时翻倍。码率阶梯的经验做法是分辨率翻倍的档位、码率约 1.5 倍,档位控制在三到四档:
─── 三档阶梯:720p / 480p / 360p,HLS 分片 6 秒
for p in 720:2800k 480:1400k 360:800k; do
h=${p%%:*}; b=${p##*:}
ffmpeg -y -i src.mp4 -vf "scale=-2:${h}" -c:v libx264 -b:v "$b" \
-preset medium -profile:v main -g 150 -keyint_min 150 \
-c:a aac -b:a 96k -f hls -hls_time 6 -hls_playlist_type vod \
"out/${h}p.m3u8"
done
-g 150 与 -hls_time 6 必须对齐(25fps 下 6 秒即 150 帧)。不对齐会导致分片边界切不出关键帧,转码器插关键帧,码率上升而画质不变,成本白涨。分片长度也是成本参数:短则首播快、请求数多、元数据占比高,长则相反;6 秒左右是常见折中,按目标观众网络条件核对。10 分钟视频三档合计可能等于原片 1.5 到 2.5 倍体积,而原片是可重新转码的源头,保留它才有重建能力。
4. 缩略图、生命周期与 CDN 回源
缩略图几乎一定是系统里最常见也最少被监控的失败,因为它在链路上处于"不重要"的位置:生成失败时页面照样打开,只是显示占位图,没有异常没有告警,只有用户觉得"这网站有点糙"。失败集中在几类:
| 现象 | 原因 | 处理 |
|---|---|---|
| 部分图有缩略图、部分没有 | 生成任务丢了消息,或队列重试耗尽 | 加补偿任务,按缺失扫描重建 |
| 缩略图为纯黑或纯白 | EXIF 方向 / 色彩空间异常 | 转码前统一 -auto-orient 与 sRGB |
| 透明 PNG 变黑底 | 输出格式不支持 alpha | 保留 alpha 或合成白底 |
最有效的措施不是重试,而是可核对:把"该有的派生规格"写成规则,定时比对实际对象,缺失即重建。核对比重试便宜得多,因为它不依赖链路任何一环的可靠性。
─── 缺失核对:按规则生成期望清单,与对象存储实际清单取差集
for key in $(mc ls --recursive --json s3/media/src | jq -r '.key' | sed 's/^src\///'); do
for w in 240 640 1280; do
echo "derived/${w}/$key.webp"
done
done | sort > expected.txt
mc ls --recursive --json s3/media/derived | jq -r '.key' | sort > actual.txt
comm -23 expected.txt actual.txt > missing.txt ── 只补缺,不重跑全部
生命周期要一开始就想清楚,因为后补的删除策略永远推不动:原图保留(唯一可重建的源头,除合规要求外不删);派生规格可重建,因此可以随时删、随时再生成;中间产物(临时转码文件、切片、多部分上传残留)必须有 TTL,且清理跑在定时任务里而不是靠人;废弃规格下线时必须单独扫一遍删除,否则它会永远躺在账单里。
─── 存储桶生命周期:原图与派生分开治理
rules:
- id: tmp-cleanup
prefix: inbound/tmp/
expire-days: 1
- id: derived-rebuildable
prefix: derived/
expire-days: 0 ── 由缺失核对保证完整,不靠过期
- id: source-permanent
prefix: src/
expire-days: 0 ── 永不自动删除
最容易被忽略的杠杆是回源率。命中率从 95% 掉到 85% 看似只差十个百分点,回源请求量却翻了三倍——5% 变成 15% 是三倍关系,不是 1.1 倍;回源次数直接等于源站带宽与存储读取成本。命中率下降的常见原因按频率排:
| 原因 | 表现 | 处理 |
|---|---|---|
Cache-Control 缺失或因签名的 URL 变化 |
每个 URL 都 miss | 稳定 URL + 长期 max-age + 内容哈希 |
未设 Vary: Accept 或设置错误 |
不同格式互相污染或反复回源 | 规范响应头,避免 Vary: * |
| 回源无缓存层 | 源站直接扛全部未命中 | 加一层源站缓存 |
真正省钱的不是"压缩更多",而是"让同一份字节只被请求一次"。判据很直接:单日回源流量占比超过两成时,先修缓存头,再谈转码参数——前者通常免费。
"中间产物可重建"的价值就在这里:派生物可随时重算,才敢对它用激进缓存、宽松删除甚至整批重建;原图必须走相反策略(永久保留、异地备份)。两类对象不该混在同一套桶规则里。
5. 排错、度量与边界
| 现象 | 最可能的原因 | 处理 |
|---|---|---|
| 转码队列持续积压 | 吞吐低于上传速率,或单任务耗时上升 | 看单任务耗时分布,而非队列长度 |
| 首屏偶尔很慢且集中在冷门页 | 首次访问转码 | 改为上传后异步预生成 + 热点预热 |
| CDN 命中率缓慢下滑 | 版本号进查询串 / UA 参与缓存键 | 规范缓存键,版本放路径 |
| 删除后账单没降 | 对象有版本控制或多副本残留 | 检查存储桶版本与生命周期规则 |
度量只需要五个:人均媒体存储(判断增长来自用户还是单文件变大)、转码成功率与失败原因分布、CDN 命中率与回源流量占比、单次访问带宽成本(总出网 ÷ 访问数)、派生规格缺失率。最后一个是唯一能提前发现静默失败的指标。
以下情况不必建立正式媒体流水线:媒体量极小(存储与带宽成本在账单上排不进前五,人工处理足够)、内容一次生成不再变更(上传即终态,无需生命周期与重建)、存储成本占比可忽略。反过来,出现任一条就该正式化:多种展示位需要不同尺寸、视频进入主流程、出现"某张图显示不对"的偶发反馈、账单按月两位数增长。
6. 自测
- CDN 命中率从 95% 降到 85%,回源请求量大约变成几倍?为什么不是 1.1 倍?
- 为什么"原图"和"派生规格"应当使用不同的保留策略?各自策略的核心是什么?
- 首次访问转码省下了什么成本、又付出了什么代价?哪类内容受害最深?
Vary: Accept缺失会导致哪两类相反的故障?- 缩略图失败为什么难被发现?哪一种指标可以提前暴露它?
- 视频转码里
-g与-hls_time不对齐会带来什么成本后果?