文件命名与目录设计
迁移到 OSS 后的第一个周末,我在 OSS 控制台看了一眼文件列表:
guangying-images/
originals/
a3f8c2e1b4567890abcdef1234567890.jpg
b7d9e1f234567890abcdef1234567890.jpg
c1e2f3a4567890123456789012345678.jpg
d4e5f678901234567890123456789abcd.jpg
... (3,500 个文件,全平铺在一个目录下)
3,500 个文件不算多,但 OSS 的 list_objects API 在单目录下文件数超过 1,000 时性能开始下降。等到 100 万张图时,这个接口会慢到不可用。
更重要的是,很多文件系统(包括 OSS 的内部索引)对单目录下的文件数量有隐性限制。
两种目录结构,差别不在”看起来整不整齐”,而在对象存储的索引模型能不能扛住。
| 维度 | 扁平目录 | 分层目录 |
|---|---|---|
| 单目录对象数 | 无上限(实际上万后会明显变慢) | 每个末级目录的对象数可控 |
list_objects 性能 | 对象数上升后线性劣化 | 按前缀列举,稳定 |
| 列举单个对象的开销 | 低 | 多一次前缀解析,可忽略 |
| 路径长度 | 短 | 长,需要留意路径拼接规则 |
| 生命周期规则粒度 | 只能按整个前缀配 | 可以精确到月份 / 哈希前缀 |
| 冷热分层迁移 | 只能整目录搬 | 可以只搬一部分前缀 |
| 批量删除 | 一次前缀删除整个目录 | 需要按更细的前缀删除 |
| 权限控制 | 通常整个前缀一套权限 | 可以按前缀分层授权 |
结论很直接:对象存储不是文件系统,“目录”只是 key 里的一个字符串片段。它没有真正的目录 inode,但也确实有单前缀对象数过多时的性能拐点。所以扁平目录省事,但规模一上来就要付账。
分层目录的三个真实收益:
- 列举可用。运营后台要导出”某个月上传的所有图片”,扁平结构下这个查询会随总量劣化;分层结构下它是精确的前缀列举。
- 生命周期规则可落地。“30 天无访问转低频”要落到对象存储的原生规则上,就得能按前缀圈定对象范围。
- 迁移可以分片。做冷热分层时,分层目录能把一次巨大的搬迁拆成很多个互不影响的批次。
代价也要认:路径规则一旦定下来就极难改。所有派生图、缓存键、CDN 回源规则、数据库里的 object_key 都依赖它。所以目录设计要在项目第一天定死,之后只能往前兼容,不能回头改。
经过研究,我设计了这样的目录结构:
originals/
2024/
06/
a3/
a3f8c2e1.jpg
a3b7d9e1.png
b7/
b7d9e1f2.jpg
...
07/
...
thumbs/
small/
2024/06/a3/a3f8c2e1_small.webp
medium/
2024/06/a3/a3f8c2e1_medium.webp
large/
2024/06/a3/a3f8c2e1_large.webp
为什么取哈希前 2 位?
先回答上面那个问题:为什么取哈希前 2 位?
前 2 位十六进制字符能分出 个子目录。取 1 位只有 16 个子目录,单目录下的文件数还是偏多;取 3 位能分出 4096 个子目录,但每个目录里的对象太少,反而让列举次数和路径长度无谓地增长,而且按前缀列举时层级太深也会变慢。2 位是权衡后的结果——目标是让每个末级目录的对象数落在一个好管的区间,而不是让目录看起来整齐。
还有一点:分片必须用哈希,不能用时间或自增 ID。如果按时间分片,当月所有写入都落在同一个前缀上,形成写热点;自增 ID 顺序递增,分片同样不均匀。内容哈希天然均匀分布,不需要任何中心化协调。
完整的命名规范:
| 规则 | 说明 | 反例 |
|---|---|---|
| 用内容哈希,不用随机数 | 同一张图重复上传会落到同一个 key,天然去重 | img_20240612_38471.jpg |
| 扩展名取实际格式 | 以解码结果为准,不信任上传时的文件名 | 原图是 JPEG,却存成 .png |
| 不包含用户输入 | 原始文件名只进元数据表,绝不进 key | my_photo(1).jpg |
| 字符集限定 | 只用小写字母、数字、点和下划线 | 空格、中文、括号、大写字母混用 |
| 长度可控 | 对象 key 总长要留出余量,方便派生图追加变体后缀 | 超长原始文件名直接拼进 key |
| 派生图带变体标识 | 同一哈希下用后缀区分尺寸与格式 | 所有变体共用一个名字 |
| 版本号显式可见 | 内容版本变化时 key 要能变,否则旧缓存永远不失效 | 只改元数据不改 key |
派生图的命名在原图 key 上追加变体后缀:
originals/2024/06/a3/a3f8c2e1b4567890.jpg
thumbs/small/2024/06/a3/a3f8c2e1b4567890_small.webp
thumbs/medium/2024/06/a3/a3f8c2e1b4567890_medium.webp
thumbs/large/2024/06/a3/a3f8c2e1b4567890_large.webp
其中 _small / _medium / _large 是业务语义的变体名,而不是宽度数字。用语义名的好处是:尺寸档调整时不用改命名规则,而宽度数字一旦要调整,等于要重命名历史对象,缓存键全变。
三条配套要求:
- 原始文件名只存在元数据表里。需要展示文件名时从数据库读,不从 key 反推。
- 内容哈希要真哈希。上传时对文件内容算 SHA-256,取前若干位;如果只对文件名算哈希,改个名字就会产生一份新对象,去重能力形同虚设。
- 去重要考虑引用计数。两张内容相同的图片会共享同一个对象,删除其中一张时不能把对象一起删掉,否则另一张就变成死链。
发现内容有问题?
告诉我们哪里需要改进。
