文件命名与目录设计
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 分层目录
# ❌ 扁平目录:所有文件平铺
FLAT_STRUCTURE = 'originals/{uuid}.jpg'
# 问题:单目录文件数无上限,list_objects 慢
# ✅ 分层目录:按规则分散到子目录
HIERARCHICAL_STRUCTURE = 'originals/{date}/{hash_prefix}/{uuid}.jpg'
# 优势:每个子目录文件数可控我的设计:四层目录结构
经过研究,我设计了这样的目录结构:
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.webpimport hashlib
import time
def generate_object_key(user_id, filename):
"""生成存储路径:年/月/哈希前缀/唯一ID.扩展名"""
# 按日期分第一层
date_parts = time.strftime('%Y/%m').split('/')
year, month = date_parts[0], date_parts[1]
# 按文件哈希前缀分第二层(分散热点)
hash_input = f"{user_id}_{time.time()}_{filename}"
file_hash = hashlib.md5(hash_input.encode()).hexdigest()
hash_prefix = file_hash[:2] # 取前 2 位 = 256 个子目录
# 唯一文件名
unique_id = uuid.uuid4().hex[:12]
ext = filename.rsplit('.', 1)[-1].lower()
object_key = f'originals/{year}/{month}/{hash_prefix}/{unique_id}.{ext}'
return object_key
# 示例输出:
# originals/2024/06/a3/f8c2e1b45678.jpg
# originals/2024/06/b7/d9e1f2345678.png
# originals/2024/07/c1/e2f3a4567890.jpg为什么取哈希前 2 位?
# 256 个子目录的容量分析
hash_prefix_count = 256 # 16^2 = 256 个子目录
# 假设 100 万张图片
total_files = 1_000_000
files_per_dir = total_files / hash_prefix_count
# = 3,906 个/目录 ← 非常健康
# 即使 1 亿张图片
total_files = 100_000_000
files_per_dir = total_files / hash_prefix_count
# = 390,625 个/目录 ← 仍然可以接受文件命名规范
NAMING_RULES = {
'原图': '{unique_id}.{ext}',
# 例:f8c2e1b45678.jpg
'缩略图': '{unique_id}_{size_name}.{format}',
# 例:f8c2e1b45678_small.webp
'裁剪图': '{unique_id}_{width}x{height}.{format}',
# 例:f8c2e1b45678_300x200.webp
'临时文件': 'temp/{user_id}/{unique_id}.{ext}',
# 例:temp/12345/abc123def456.jpg
}
# 缩略图的完整路径生成
def get_thumb_path(original_key, size_name):
"""根据原图路径推导缩略图路径"""
# originals/2024/06/a3/f8c2e1b45678.jpg
# → thumbs/small/2024/06/a3/f8c2e1b45678_small.webp
parts = original_key.split('/')
filename = parts[-1]
name_without_ext = filename.rsplit('.', 1)[0]
return f'thumbs/{size_name}/{" ".join(parts[1:-1])}/{name_without_ext}_{size_name}.webp'本节小结
✅ 关键原则:
- 绝不平铺所有文件——用日期 + 哈希前缀做多级目录
- 文件名不含业务含义(用户 ID、标题等),只用唯一 ID
- 路径可推导——知道原图路径就能算出缩略图路径,不需要查数据库
我的思考
思考 1
为什么用哈希前缀而不是用户 ID 做目录分片?按用户 ID 分不是更方便管理吗?
参考答案
按用户 ID 分片的问题:
users/12345/photos/abc.jpg ← 热门摄影师,上传了 10 万张
users/67890/photos/def.jpg ← 普通用户,只上传了 3 张- 数据倾斜严重:活跃用户的目录可能积累几十万文件,不活跃用户的目录几乎为空
- 隐私风险:路径中包含用户 ID,第三方 CDN 日志可能泄露用户行为
- 迁移困难:如果某个用户的数据需要迁移,会影响整个目录结构
按哈希前缀分片的优势:
originals/2024/06/a3/abc.jpg ← 用户 12345 的
originals/2024/06/a3/def.jpg ← 用户 67890 的
originals/2024/06/b7/ghi.jpg ← 用户 12345 的- 均匀分布:哈希函数天然分散,每个子目录的文件数基本相同
- 无法反推用户:路径中不含用户信息
- 访问热点分散:同一用户的图片分散在不同子目录,避免集中读取
管理需求怎么解决? 在数据库中维护 用户 → 图片列表 的映射,而不是在文件路径中编码。
-- 数据库负责关系,文件系统只负责存储
CREATE TABLE photos (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
object_key VARCHAR(255) NOT NULL, -- originals/2024/06/a3/abc.jpg
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_user (user_id),
INDEX idx_object_key (object_key),
);