文件命名与目录设计

OSS 上出现了一个巨型目录

迁移到 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 的内部索引)对单目录下的文件数量有隐性限制。

扁平目录 vs 分层目录

两种目录结构,差别不在”看起来整不整齐”,而在对象存储的索引模型能不能扛住。

维度扁平目录分层目录
单目录对象数无上限(实际上万后会明显变慢)每个末级目录的对象数可控
list_objects 性能对象数上升后线性劣化按前缀列举,稳定
列举单个对象的开销低多一次前缀解析,可忽略
路径长度短长,需要留意路径拼接规则
生命周期规则粒度只能按整个前缀配可以精确到月份 / 哈希前缀
冷热分层迁移只能整目录搬可以只搬一部分前缀
批量删除一次前缀删除整个目录需要按更细的前缀删除
权限控制通常整个前缀一套权限可以按前缀分层授权

结论很直接:对象存储不是文件系统,“目录”只是 key 里的一个字符串片段。它没有真正的目录 inode,但也确实有单前缀对象数过多时的性能拐点。所以扁平目录省事,但规模一上来就要付账。

分层目录的三个真实收益:

  1. 列举可用。运营后台要导出”某个月上传的所有图片”,扁平结构下这个查询会随总量劣化;分层结构下它是精确的前缀列举。
  2. 生命周期规则可落地。“30 天无访问转低频”要落到对象存储的原生规则上,就得能按前缀圈定对象范围。
  3. 迁移可以分片。做冷热分层时,分层目录能把一次巨大的搬迁拆成很多个互不影响的批次。

代价也要认:路径规则一旦定下来就极难改。所有派生图、缓存键、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 位十六进制字符能分出 162=25616^2 = 256 个子目录。取 1 位只有 16 个子目录,单目录下的文件数还是偏多;取 3 位能分出 4096 个子目录,但每个目录里的对象太少,反而让列举次数和路径长度无谓地增长,而且按前缀列举时层级太深也会变慢。2 位是权衡后的结果——目标是让每个末级目录的对象数落在一个好管的区间,而不是让目录看起来整齐。

还有一点:分片必须用哈希,不能用时间或自增 ID。如果按时间分片,当月所有写入都落在同一个前缀上,形成写热点;自增 ID 顺序递增,分片同样不均匀。内容哈希天然均匀分布,不需要任何中心化协调。

完整的命名规范:

规则说明反例
用内容哈希,不用随机数同一张图重复上传会落到同一个 key,天然去重img_20240612_38471.jpg
扩展名取实际格式以解码结果为准,不信任上传时的文件名原图是 JPEG,却存成 .png
不包含用户输入原始文件名只进元数据表,绝不进 keymy_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 是业务语义的变体名,而不是宽度数字。用语义名的好处是:尺寸档调整时不用改命名规则,而宽度数字一旦要调整,等于要重命名历史对象,缓存键全变。

三条配套要求:

  1. 原始文件名只存在元数据表里。需要展示文件名时从数据库读,不从 key 反推。
  2. 内容哈希要真哈希。上传时对文件内容算 SHA-256,取前若干位;如果只对文件名算哈希,改个名字就会产生一份新对象,去重能力形同虚设。
  3. 去重要考虑引用计数。两张内容相同的图片会共享同一个对象,删除其中一张时不能把对象一起删掉,否则另一张就变成死链。

发现内容有问题?

告诉我们哪里需要改进。