深蓝黑背景封面。左侧是一幅纵向分片集群示意图:顶部「应用 / 客户端」方框向下箭头指向青色高亮的「mongos 路由」;mongos 右侧以虚线连接紫色虚线框「Config Server(存元数据)」;mongos 再向下经分流线扇出到三个并排分片方框 Shard 1、Shard 2、Shard 3,各注「数据子集 1/2/3」。下方文字「片键决定每条文档落到哪个分片」,并列出片键三问三枚胶囊:基数够大?分布均匀?单调递增?右侧大标题「MongoDB 分片,到底怎么回事」,副标题「数据超单机后的水平拆分」。左上标注「面试经典 · 数据库」,右上圆角胶囊标注「Day 01 · 11-08」 图 1:本文封面,分片集群的三类组件与片键三问。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「MongoDB 分片是怎么回事」是数据库面试的常客,能背出 mongos、config server、shard 三个名词的人不少,但真正的分水岭在追问里:片键选错会发生什么,什么量级才值得上分片。

为什么需要分片:单机到顶之后的水平出路。

单机数据库的压力来自两个方向:数据量(磁盘与内存装不下,热数据无法常驻内存)和写吞吐(单机的写入上限被持续压满)。垂直扩容(换更贵的机器)有硬上限且成本陡增,分片(sharding)是 MongoDB 给出的水平答案:把数据集切成若干子集,分散到多台机器上,容量与吞吐随机器数近似线性扩展。先记住结论再展开:分片解决的是「装得下、写得动」,不管「挂不了」——高可用是副本集的职责(本批另一篇真题专门讲),两者正交。

分片集群的三类组件。

MongoDB 官方文档把分片集群的组件列为三类。第一类是 shard:每个分片持有数据集的一个子集,且每个分片本身必须部署为一个副本集——分片管扩展,副本集管分片内部的高可用。第二类是 mongos:查询路由器,应用不直接连分片,只跟 mongos 对话;mongos 拿着元数据判断目标数据在哪个分片,把请求路由过去,涉及多分片的查询再合并结果。第三类是 config server:存储集群的元数据与配置——核心就是「哪个数据块在哪个分片」这张映射表,它也必须部署为副本集。生产集群的典型形态是:3 节点的 config server 副本集、每个分片各 3 节点、一个或多个 mongos。

数据怎么被切开:片键、范围与数据块。

分片的魔法全在一个字段上:片键(shard key)。片键是集合中一个带索引的字段(或复合字段),MongoDB 把片键的值域划分成互不重叠的范围,每个范围对应一个数据块(chunk),后台均衡器在分片之间迁移数据块,让各分片负载尽量均匀。

一次写入的旅程是:应用把文档发给 mongos;mongos 按片键值查到它属于哪个范围、该范围当前在哪个分片;请求只打到那个分片。查询同理——带片键条件的查询能精确定位单个分片,不带片键的查询只能广播到所有分片做散射收集,这也是片键影响查询性能的原因。

片键三问:选错全盘皆输。

片键在集合分片那一刻就基本绑定了(新版本支持补充字段或整体重分片,但都是要停机评估的大动作),所以面试里「怎么选片键」比「分片是什么」值钱得多。三个检查问题:

基数够大吗?片键取值太少,值域就切不出足够多的范围,数据块无法继续分裂,出现无法迁移的超大块(jumbo chunk),分片数再多也用不上。

分布均匀吗?取值集中在少数几个键上,对应的数据块会持续膨胀成热点,均衡器疲于迁移也追不上。

会单调递增吗?这是最经典的坑:用时间戳或自增 id 做范围分片的片键,新写入的键值永远落在最大范围,而最大范围永远在某个「最后」的分片上——集群再大,写入也全压一台机器。解法之一是换哈希片键:MongoDB 对片键值做哈希再切范围,单调的键被打散,写入近似均匀铺开,代价是范围查询失去局部性。

用代码把写热点模拟出来。

下面这段脚本模拟 3 个分片、3 万条单调递增键的写入:范围分片对照哈希打散后的分布差异。用 Node.js 22.15.0 运行(node shardkey.js):

const SHARDS = ['Shard-1', 'Shard-2', 'Shard-3'];
function routeRange(key) {           // 范围分片:键值落在哪段就去哪个分片
  if (key < 33333) return SHARDS[0];
  if (key < 66666) return SHARDS[1];
  return SHARDS[2];
}
function hash32(x) {                 // FNV-1a,模拟哈希片键的打散效果
  let h = 0x811c9dc5;
  for (const ch of String(x)) {
    h ^= ch.charCodeAt(0);
    h = Math.imul(h, 0x01000193) >>> 0;
  }
  return h % 100000;
}
const N = 30000;
const mono = Object.fromEntries(SHARDS.map(s => [s, 0]));
const hashed = Object.fromEntries(SHARDS.map(s => [s, 0]));
for (let i = 0; i < N; i++) {
  const ts = 70000 + i;              // 新写入的键持续增大(时间戳语义)
  mono[routeRange(ts)]++;
  hashed[routeRange(hash32(ts))]++;
}
const pct = o => SHARDS.map(s => `${s}=${(o[s] / N * 100).toFixed(1)}%`).join('  ');
console.log('单调递增片键写入占比:', pct(mono));
console.log('哈希片键写入占比:    ', pct(hashed));

实测输出:

单调递增片键写入占比: Shard-1=0.0%  Shard-2=0.0%  Shard-3=100.0%
哈希片键写入占比:     Shard-1=33.3%  Shard-2=33.6%  Shard-3=33.1%

单调片键下 3 万条写入 100% 落在 Shard-3——扩容扩了个寂寞;哈希打散后三个分片各占约三分之一,这才是分片该有的样子。

深蓝黑背景概念图,分三个区域。上部架构图:左侧「应用」方框箭头指向青色高亮的「mongos 查询路由」;mongos 上方以虚线连接紫色虚线框「Config Server」,旁注「存『块在哪个分片』的元数据」;mongos 向右扇出三条箭头分别指向 Shard A(块 1–33)、Shard B(块 34–66)、Shard C(块 67–99),每个分片注「本身是副本集」。架构图右侧是编号查询流程:① 应用只跟 mongos 对话;② mongos 查元数据定位分片;③ 请求只打到相关分片;④ 均衡器在分片间迁移数据块。左下卡片「片键三问(选错全盘皆输)」:基数够大吗——太小则块无法再分裂;分布均匀吗——集中则单分片热点;会单调递增吗——新写全压最后一个分片;注「单调键(时间戳、自增 id)可考虑哈希片键打散」。右下卡片「什么时候才需要分片」:数据量或内存占用超单机可承受范围、写吞吐持续压满单机上限、否则优先副本集因为分片运维成本高得多;注「分片是最后的扩容手段,不是默认配置」 图 2:集群架构、查询路由流程与片键三问的全景。

什么时候才需要分片。

分片不是默认配置,而是最后的扩容手段。值得上分片的信号:数据量或热数据内存占用持续超出单机可承受范围,或者写吞吐长期压满单机上限,且读扩展、索引优化等手段已经用尽。不满足这些信号时,优先用副本集扛住读流量和高可用——分片集群多出了 mongos 层、config server、均衡器与跨分片事务的复杂度,运维成本高得多,提前上马是给自己加戏。向量库与特征存储面对「数据超单机后按什么键切分」时,片键的基数、单调性与写热点分析可以整套迁移。

回到面试现场,建议的答题结构:先一句话定位「分片管水平扩展,副本集管高可用」,再讲三类组件与一次查询的路由流程,然后重点展开片键三问与单调键写热点(能画出分布对比更加分),最后给出「什么量级才值得分片」的判断标准。组件名单是及格线,片键分析与上马时机才是区分度。你经手的系统里,有没有遇到过片键或分库分表键选错、后期不得不迁移的经历?欢迎讲讲当时的代价。