GridFS分片后数据分布不均的根本原因在于未对fs.files和fs.chunks显式启用分片或分片键设计不合理:fs.chunks必须用{files_id: "hashed"}分片以打散同一文件的chunk,否则默认索引{files_id: 1, n: 1}会导致热块;仅分片fs.files而漏掉fs.chunks,或误用{files_id: 1}、{_id: "hashed"}均会引发倾斜;验证需用db.fs.chunks.getShardDistribution()或直连各shard查stats,而非仅看sh.status();已倾斜数据不宜强行moveChunk,应停写重建分片键。

GridFS分片后数据分布不均,根本不是GridFS的问题
GridFS本身不参与分片逻辑——fs.files和fs.chunks只是普通集合,能否均匀分布,完全取决于你对这两个集合是否正确启用了分片,以及分片键选得是否合理。很多人误以为“开了GridFS就自动分片”,结果发现fs.chunks全挤在一个shard上,其实是压根没对它分片,或者只分了fs.files却漏掉fs.chunks。
为什么fs.chunks默认不分片,且容易倾斜
fs.chunks集合的默认索引是{ files_id: 1, n: 1 },这个结构天然不适合范围分片:
- 所有属于同一文件的chunk,
files_id相同,n递增——写入时全部落在同一个chunk range里,极易形成“热块” - 如果用
{ files_id: 1 }做分片键,所有同文件的chunk仍会路由到同一分片,毫无分散效果 - 如果用
{ _id: "hashed" }哈希分片,能强制打散,但会破坏files_id + n的局部性,导致读取单个文件时需广播查询多个分片(分散-聚集) - 真正可行的是
{ files_id: "hashed" }——它把同一文件的chunk打散到不同分片,但代价是单文件读需跨分片合并;若业务以小文件为主、且读多于写,这反而是更优解
查证是否真分片了,别信sh.status()一眼结论
sh.status()只显示chunk数量,不反映真实数据量。一个分片可能有100个chunk但只存10MB日志,另一个只有5个chunk却塞了2GB视频块。确认是否真分片成功,必须:
- 连到mongos执行
sh.shardCollection("fs.chunks", { files_id: "hashed" })——注意:必须显式调用,GridFS不自动触发 - 检查
db.fs.chunks.getShardDistribution()(4.4+支持),看各分片的data和docs是否接近 - 若无此方法,直连每个shard节点,运行
db.fs.chunks.stats({scale: 1024*1024}),比对size字段(单位MB) - 特别警惕
config.chunks里fs.chunks的记录数为0——说明根本没分片,所有chunk都还在default shard上
已倾斜后强行迁移chunk,风险远大于收益
不要试图用sh.moveChunk()手动搬fs.chunks的chunk——WiredTiger底层对小文档(255KB chunk)的迁移效率极低,且容易触发“UUID冲突”错误(如日志里报Cannot receive chunk because we already have an identically named collection with UUID xxx):
- 该错误本质是目标shard上残留了旧版本
fs.chunks集合(比如之前分片失败或手动drop过),必须先连到该shard执行db.fs.chunks.drop() - 但drop前务必确认该shard上没有正在使用的文件——否则会变相删掉线上文件
- 更稳妥的做法是停写、导出
fs.files和fs.chunks、重建分片键、再导入,而非在线硬搬 - 如果只是少量倾斜,优先开大
balancer窗口(sh.setBalancerState(true)+sh.startBalancer()),让自动均衡器慢慢消化
真正难处理的不是“怎么搬”,而是“为什么一开始没设对分片键”——files_id哈希分片后,后续上传的新文件才会均匀,历史已倾斜的数据只能靠时间或重建来修正。


















