“麻辣鲜”火锅最初只有一家店,所有食材堆在一个后厨仓库里,老板一人管账,门店自己决定从仓库取什么菜。后来生意火爆,连锁店开到全国,一个仓库再也塞不下每天从各地涌来的毛肚、牛肉和冻虾——进货堵在门口,取货绕晕店员。 老板想出了一个办法:把食材按类别分到不同城市的中央仓库——成都仓管毛肚、内蒙仓管牛肉、沿海仓管海鲜。每家门店的前台负责接单(路由),总部设一个“食材目录处”记录每种食材在哪个仓库(元数据),各城市仓库各自存储、各自出货(数据分片)。这样一来,仓库再多也不乱,门店再忙也不挤。这就是 MongoDB 分片集群的雏形:能水平扩展的分布式数据库架构。 本文用这套“连锁火锅店”的故事,带你从零搞懂 MongoDB 分片集群的三层架构、分片键选型、chunk 与 balancer 机制、运维扩容与问题排查,让你真正理解“数据该怎么分、分了怎么管、出了问题怎么救”。

岗位关联

掌握 MongoDB 分片集群架构与运维,是构建互联网级高并发、海量存储数据库系统的核心能力,也是资深 DBA 和架构师岗位的必备技能。

目录
  1. 可视化演示
  2. 第一章:分片架构总览——前台、目录与仓库
  3. 第二章:分片键选型——决定命运的分仓维度
  4. 第三章:Chunk机制——仓库里的货架切分
  5. 第四章:均衡器原理——总部的调度车队
  6. 第五章:部署与运维——开分店的硬规矩
  7. 第六章:扩容与容灾——再开一个仓库
  8. 第七章:问题排查——仓库异常三查法
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示MongoDB分片集群架构与的核心机制,通过概览/流程/对比/趋势四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

MongoDB分片集群架构与:分片集群 分片集群三层架构与组件职责 分片键选择策略与数据分布 核心机制可视化 MongoDB分片集群架构与整体架构与关键组件

第一章:分片架构总览——前台、目录与仓库

“麻辣鲜”在全国开了几十家分店之后,老板发现单靠“一个超大总仓库”已经行不通:进货通道堵死,取货路线绕晕,最要命的是稍微一扩容就要停业整顿。于是他把仓库按食材类别拆到了不同城市——这就是分片(sharding)的初衷:把数据水平切分到多个存储单元,让容量和并发可以“加仓库”而不是“换仓库”。

对应到 MongoDB,一个分片集群由三个角色组成。mongos 是门店前台:应用只连它,它不存任何数据,只负责接单并转发;config server 是总部食材目录处:保存集群的元数据,比如每个 chunk(货架)归哪个 shard(仓库),mongos 每次路由都要问它;shard 是城市仓库:真正存放用户数据的单元,每个 shard 本身又是一个副本集,保证数据多副本不丢。

三者的关系可以用一条链路描述:应用 → mongos →(查 config 拿到映射)→ 目标 shard。集群的存储天花板就是所有 shard 容量之和:

一句话总结:mongos 不存货、config 不存货、只有 shard 存货。三个角色各司其职,才能让“前台、目录、仓库”这套体系在不停业的情况下越开越大。

集群容量=∑i=1nCi\displaystyle \text{集群容量} = \sum_{i=1}^{n} C_i
这条公式说的是:整体容量等于把每个仓库的库容加起来,想扩容就增加仓库数量 n。
  1. 累加符号 Σ把从 i=1 到 n 的所有项逐一相加,相当于“把全国每个仓库的容量全部盘点加总”。
    ∑i=1n\displaystyle \sum_{i=1}^{n}
  2. C_i(第 i 个 shard 的容量)第 i 个仓库能装多少食材,单位是 GB 或 TB。
    Ci\displaystyle C_i
  3. n(shard 总数)仓库的个数,水平扩展的本质就是让 n 变大。
    n\displaystyle n

第二章:分片键选型——决定命运的分仓维度

分仓库的维度决定了生意格局。如果按“口味”分(辣/不辣),全国人民都吃辣,不辣仓就空置;按“食材类别”分,几百种食材刚好均匀散开。在 MongoDB 里,“按什么分”就是分片键(shard key),它是集合中一个或几个字段,MongoDB 按它的值把数据切到不同 chunk。

选分片键有四个硬指标:基数高、写入分散、查询常带、不可变。基数(cardinality)指分片键不同取值的个数,基数太低(比如只有两个值的“性别”)会导致数据最多只能切成两块,后面的 shard 白加。写入分散最容易被忽略:如果分片键是自增 _id,新插入永远落在最大键值所在的 chunk,所有写流量都打在同一 shard,这就是“写热点”。

解决办法是 hashed 分片(把键值哈希后再分布)或选业务上天然分散的字段。查询方面,带分片键的查询能精确路由到单个 shard,不带则要广播所有 shard(scatter-gather)。此外,分片键一经选定很难修改(MongoDB 4.4 起可 refine,但成本仍高),所以上线前必须想清楚。复合分片键的基数可以用乘法估算:

分片键就像火锅店的“分仓规则”,规则定错了,后面再好的车队、再多的仓库也救不回来。选键是分片集群里唯一“事后极难反悔”的设计决策。

N=C1×C2×⋯×Cm\displaystyle N = C_1 \times C_2 \times \cdots \times C_m
复合分片键的总基数等于各字段基数相乘——两个字段的取值两两组合,数量翻倍增长。
  1. N(复合键总基数)组合键一共有多少种不同取值,决定理论上最多能切多少个 chunk。
    N\displaystyle N
  2. C_1(第一个字段的基数)第一个字段有多少种不同取值,比如城市字段有 300 个城市。
    C1\displaystyle C_1
  3. ×(取值组合)两个字段的取值两两搭配,就像“菜系 × 食材”能组合出几十种新菜品。
    ×\displaystyle \times
  4. C_m(最后一个字段的基数)所有字段基数连乘,得出组合键的最终基数。
    Cm\displaystyle C_m

第三章:Chunk机制——仓库里的货架切分

食材进仓库后不能胡乱堆,得按分片键的值域分成一段段“货架区”,每段就是一个 chunk。比如把用户 ID 哈希后的 0-99 键值域切成 [0,20)、[20,40)……每个区间就是一块货架,写进来的数据按分片键值落在对应的货架上。

chunk 是数据的逻辑组织单位,范围用 [minKey, maxKey) 表示,默认目标大小是 64MB(maxChunkSize)。数据落在哪个 chunk 由分片键值决定,而 chunk 归哪个 shard 由 config server 记录。写入量上来后,chunk 会“长大”:当一个 chunk 的文档数或大小超过阈值,MongoDB 就会尝试 split——在它的键值范围中间找一个分界点,切成两个更小的 chunk。

注意 split 只是“把账本上的一个区间拆成两行”,数据物理位置没动;真正搬数据的是后面的迁移(migration)。需要规划的碎片数量,可以用总数据量除以目标 chunk 大小来估算:

货架满了就拆成两半——split 让数据能继续“长个”,也让均衡器有更多可搬的小块。理解了 chunk,再看 balancer 就顺理成章了。

Nchunk=⌈DtotalSchunk⌉\displaystyle N_{\text{chunk}} = \left\lceil \frac{D_{\text{total}}}{S_{\text{chunk}}} \right\rceil
估算 chunk 总数:总数据量除以每个 chunk 的目标大小,向上取整。
  1. D_total(总数据量)整个集合要存放的数据总量,比如 10TB。
    Dtotal\displaystyle D_{\text{total}}
  2. S_chunk(目标 chunk 大小)默认 64MB,相当于每个货架的“标称容量”。
    Schunk\displaystyle S_{\text{chunk}}
  3. ⌈⌉(向上取整)算出 10.2 也算 11 个货架,最后那个装不满也得存在。
    ⌈⋅⌉\displaystyle \left\lceil \cdot \right\rceil

第四章:均衡器原理——总部的调度车队

前台的目录再准,仓库之间也可能“旱的旱死、涝的涝死”。总部为此养了一支“调度车队”——balancer,它常驻后台,发现某个仓库的货架明显多于其他仓库时,就把多余的货架搬到空一点的仓库。

balancer 是运行在 mongos 上的协调者(新版本里协调组件在 config server 主节点上运行,原理一致)。它周期性检查各 shard 的 chunk 数量:当最大值和最小值的差值超过阈值(chunk 总数较少时阈值是 8 个),就进入均衡流程。迁移一个 chunk 分为四步:把源 chunk 标记为 draining;把文档逐个拷贝到目标 shard;拷贝完成后在源 shard 删除;最后更新 config 里的 chunk 映射。

迁移不是原子的,但业务读写不会被阻塞——读写可以打到正在迁移的 chunk 上。为了避免迁移流量挤占业务 IO,可以设置 balance window(比如凌晨 2-6 点),也可以临时停掉 balancer 做运维操作。触发条件可以用差值公式表达:

调度车队不会一次搬空,而是搬一块、核实一块、再搬下一块,还要避开城市高峰——这就是 balancer 的“慢工出细活”。看清它的节奏,运维时就不会误判“为什么数据还不均衡”。

max⁡iCi−min⁡iCi>T\displaystyle \max_{i} C_i - \min_{i} C_i > T
当 chunk 最多的 shard 与最少的 shard 差值超过阈值 T 时,才值得派车队跑一趟。
  1. C_i(各 shard 的 chunk 数)每个仓库当前拥有的货架数量。
    Ci\displaystyle C_i
  2. max_i / min_i(找最大与最小)在所有 shard 里挑出 chunk 最多的和最少的那两个仓库。
    max⁡iCi,min⁡iCi\displaystyle \max_i C_i, \quad \min_i C_i
  3. T(迁移阈值)小集群默认 8 个,差值不够大不值得折腾车队。
    T=8\displaystyle T = 8

第五章:部署与运维——开分店的硬规矩

开分店有硬规矩:仓库不能只有一个值班员,总目录更不能交给“临时工”。部署 MongoDB 分片集群,第一原则是每个角色都高可用:config server 必须用副本集,生产环境 3 节点起步——它保存着全集群的 chunk 路由映射,一旦丢失,mongos 就变成“没有目录的前台”,数据明明在却找不到。

每个 shard 也建议 3 节点副本集(可加仲裁节点),mongos 无状态,可以部署多台并用负载均衡器对外。日常运维离不开三件事:备份、监控、巡检。备份要“先停 balancer 再做快照”,对每个 shard 和 config 分别备份并保证一致性;监控聚焦 chunk 分布、迁移速率、各 shard 的 QPS 与容量;巡检脚本要定期检查 config.chunks 里有没有异常块。

副本集的高可用可以用独立节点的可用性推导出来:

规矩越硬,营业越稳。把 config 副本集、shard 副本集、备份调度当成开店执照一样对待,分片集群才能扛住生产流量。

A=1−(1−a)3\displaystyle A = 1 - (1 - a)^3
三副本时,整个副本集可用的概率等于 1 减去三台机器同时故障的概率。
  1. a(单节点可用性)单个值班员不撂挑子的概率,比如 99%。
    a\displaystyle a
  2. (1-a)³(三节点同时故障)三个节点都挂掉整体才不可用,所以三个“挂的概率”相乘。
    (1−a)3\displaystyle (1 - a)^3
  3. 1 - ……(整体可用性)用 1 减去“全挂”的概率,就是至少有一台活着的概率。
    1−(1−a)3\displaystyle 1 - (1 - a)^3

第六章:扩容与容灾——再开一个仓库

生意继续涨,仓库又不够了。这时候老板只需说一句“再开一个仓”,剩下的交给车队:MongoDB 新增 shard 后用一条 addShard 命令完成挂载,balancer 会自动把部分 chunk 迁入新 shard,业务全程不停。

扩容后每个 shard 的平均数据量可以用总数据量除以 shard 个数来估算。但要注意:迁移需要时间,扩容瞬间新 shard 是“空仓”,热点还留在老 shard 上;而且迁移过程会产生额外 IO,所以扩容动作最好放在低峰期,并盯住迁移进度。除了横向加 shard,还有两种手段:垂直扩容(给单个 shard 加 CPU/内存/磁盘)——相当于把仓库盖高,简单但有硬件上限;zone 分片——通过 zone 和 tag 把特定键值范围的数据“钉”在指定的 shard 上,适合数据属地化或读写隔离需求。

扩容后的容量规划用下面的公式:

扩容不是“加个仓库马上统统均匀”,而是“挂好仓库,给车队一点时间”。把期望容量、迁移成本、zone 策略放在一起规划,集群才能优雅地长大。

Cavg=⌈Dtotalnshard⌉\displaystyle C_{\text{avg}} = \left\lceil \frac{D_{\text{total}}}{n_{\text{shard}}} \right\rceil
扩容后每个 shard 平均承载的数据量等于总数据量除以 shard 数。
  1. D_total(总数据量)整个集群要承载的全部数据。
    Dtotal\displaystyle D_{\text{total}}
  2. n_shard(分片个数)扩容后一共有多少个仓库。
    nshard\displaystyle n_{\text{shard}}
  3. ⌈⌉(向上取整)每片至少要能装下平均值,实际生产还要预留 30% 以上余量。
    ⌈⋅⌉\displaystyle \left\lceil \cdot \right\rceil

第七章:问题排查——仓库异常三查法

仓库出问题,老司机的排查顺序是“先看键,再看块,最后查车”:键是分片键选型,块是 chunk 状态,车是 balancer 迁移。三个环节对应三类典型故障。

热点问题:某个 chunk 的访问量远高于平均,常见原因是分片键基数低或取值倾斜(比如“城市”字段里上海占 80%)。热点表现为单个 shard 的 QPS/CPU 飙高,排查时看 config.chunks 里的键值分布,解法是换复合分片键或对热点键做二次哈希。jumbo chunk:chunk 超过 maxChunkSize 且无法再 split(键值域已经没有中间值可切),会被打上 jumbo 标记,balancer 拒绝迁移它,导致均衡卡死;排查 config.chunks 里的 jumbo: true,处理手段是把块内数据拆细,或临时调大 maxChunkSize 清理标记。

迁移慢则要检查 balance window 是否太短、目标 shard 是否缺索引、网络是否被限速。热点程度可以量化:

排查如看病:“键”是体质、“块”是病灶、“车”是药效。按这个顺序定位,大部分分片集群问题都能在十分钟内找到方向。

H=QPSchunkQPS‾\displaystyle H = \frac{\text{QPS}_{\text{chunk}}}{\overline{\text{QPS}}}
热点度等于某个 chunk 的 QPS 除以集群平均 QPS,比值越大说明这个货架越烫手。
  1. QPS_chunk(单 chunk 请求量)这个货架每秒被取货的次数。
    QPSchunk\displaystyle \text{QPS}_{\text{chunk}}
  2. QPS(集群平均请求量)所有 chunk 每秒请求数的平均值。
    QPS‾\displaystyle \overline{\text{QPS}}
  3. H(热点度)大于 2 说明明显偏热,大于 5 就要紧急干预。
    H\displaystyle H

AI 应用场景

场景一:大规模推荐系统的用户行为特征存储

阶段:特征存储与数据服务层

解决的问题:推荐模型需要近线存储海量用户行为日志供特征工程读取。传统单机 MongoDB 在几十亿条记录、每秒数万写入面前很快触顶;用分片集群按 user_id 做哈希分片,写入均匀分散到所有 shard,特征查询带上 user_id 后由 mongos 精确路由,不再广播全表。

具体案例:某互联网公司日活 5000 万,行为特征表每天新增约 20 亿条记录,把集合按 user_id hashed 分片到 24 个 shard,成功支撑 10 万级 QPS 写入,同时把特征读取 P99 延迟稳定在 20ms 内。

场景二:RAG 知识库的文档切片元数据管理

阶段:RAG 数据工程管道

解决的问题:大模型 RAG 应用要把海量文档切块、向量化后入库,向量存在向量数据库,但文档原文、切片元数据、切片之间的父子关系需要一个可靠且可扩展的数据库来管。MongoDB 分片集群按 doc_id 分片,支持知识库元数据的海量存储与按文档维度聚合查询。

具体案例:某企业级知识库平台存放 1 亿篇文档的切片元数据,8 个 shard 承载知识库的每日刷新与在线检索,管理端按 kb_id 过滤时能精确路由,避免跨 shard 广播。

场景三:模型训练指标与日志的可观测性平台

阶段:MLOps 训练运维

解决的问题:大模型训练任务每秒产生大量指标日志,需要持久化供调参与复盘分析。日志按 run_id 分片,天然把同一个训练任务的日志聚在同一批 shard 上,查询某次实验只需路由到少量分片。

具体案例:某 MLOps 平台累计管理 10 万次训练实验,每次实验产生约 500GB 指标日志,分片集群按 run_id 哈希分片到 12 个 shard,支持按实验维度快速切片分析,日志留存 180 天。

Python 演示

python116 行
# 模拟 MongoDB 分片集群: mongos 路由 + config 元数据 + chunk 迁移均衡import hashlib  def hash_key(user_id):    # 把 user_id 哈希到 0-99, 模拟 hashed 分片键    return int(hashlib.md5(str(user_id).encode()).hexdigest(), 16) % 100  class Shard:    # 一个分片, 相当于 replica set 的主节点    def __init__(self, name):        self.name = name        self.docs = {}     def insert(self, key, value):        self.docs[key] = value     def query(self, key):        return self.docs.get(key)     def count(self):        return len(self.docs)     def take_range(self, lo, hi):        # 把落在 [lo, hi) 的文档取走, 用于 chunk 迁移        moved = {}        for k in [k for k in self.docs if lo <= hash_key(k) < hi]:            moved[k] = self.docs.pop(k)        return moved  class ConfigServer:    # 记录每个 chunk 区间属于哪个 shard    def __init__(self, boundaries, shards):        self.boundaries = boundaries        self.shards = shards        self.chunk_owner = [0] * (len(boundaries) - 1)     def route(self, key):        for i in range(len(self.boundaries) - 1):            if self.boundaries[i] <= key < self.boundaries[i + 1]:                return self.shards[self.chunk_owner[i]]        return self.shards[-1]     def migrate(self, chunk_idx, dst):        # 把一个 chunk 的数据和归属一起迁到 dst        lo, hi = self.boundaries[chunk_idx], self.boundaries[chunk_idx + 1]        src = self.shards[self.chunk_owner[chunk_idx]]        for k, v in src.take_range(lo, hi).items():            dst.insert(k, v)        self.chunk_owner[chunk_idx] = self.shards.index(dst)  class MongosRouter:    # 应用入口: 先问 config 找 shard, 再读写数据    def __init__(self, config):        self.config = config     def write(self, uid, doc):        self.config.route(hash_key(uid)).insert(uid, doc)     def read(self, uid):        return self.config.route(hash_key(uid)).query(uid)     def scatter_gather(self, city):        # 不带分片键的查询: 广播到所有 shard        hits = []        for s in self.config.shards:            for doc in s.docs.values():                if doc["city"] == city:                    hits.append(doc)        return hits  def balance(config):    # 极简 balancer: 从 chunk 最多的 shard 迁移一个 chunk 到最少的    for _ in range(10):        counts = [sum(1 for o in config.chunk_owner if o == i)                  for i in range(len(config.shards))]        if max(counts) - min(counts) <= 1:            break        src = max(range(len(counts)), key=lambda i: counts[i])        dst = min(range(len(counts)), key=lambda i: counts[i])        for ci in range(len(config.chunk_owner)):            if config.chunk_owner[ci] == src:                config.migrate(ci, config.shards[dst])                break    return [sum(1 for o in config.chunk_owner if o == i)            for i in range(len(config.shards))]  def main():    shards = [Shard("shardA"), Shard("shardB"), Shard("shardC")]    boundaries = list(range(0, 100, 20)) + [100]    config = ConfigServer(boundaries, shards)    router = MongosRouter(config)     # 模拟 300 个用户写入, 初始所有 chunk 都在 shardA    for uid in range(300):        router.write(uid, {"user_id": uid, "city": "city_" + str(uid % 10)})    print("docs per shard before:", [s.count() for s in shards])     # 带分片键的查询: 精确路由到单个 shard    print("read user 42:", router.read(42))     # 不带分片键的查询: mongos 广播所有 shard    print("scatter_gather city_7:", len(router.scatter_gather("city_7")))     print("chunk owners before:", config.chunk_owner)    print("chunk owners after:", balance(config))    print("docs per shard after:", [s.count() for s in shards])  if __name__ == "__main__":    main()
目的:用 120 行代码把 MongoDB 分片集群的核心机制跑一遍——写入路由、精确查询、广播查询、chunk 迁移与均衡。
  1. self.chunk_owner = [0] * (len(self.boundaries) - 1)初始把所有 chunk 都登记到第一个 shard,模拟“部署初期数据全挤在一个仓库”。
  2. def route(self, key):config server 的核心方法:给定分片键哈希值,用边界表找到它属于哪个 chunk,进而返回所属 shard。
  3. def write(self, uid, doc):mongos 写路径:先算 hash_key(uid) 走 route 找到 shard,再 insert,一次写入只打一个 shard。
  4. def scatter_gather(self, city):不带分片键的查询:mongos 必须广播所有 shard 再合并结果,代价很高。
  5. def migrate(self, chunk_idx, dst):balancer 迁移:把 chunk 对应的文档从源 shard 搬到目标 shard,并更新 chunk_owner。
  6. if max(counts) - min(counts) <= 1:均衡终止条件:各 shard 的 chunk 数差异不超过 1,类似 MongoDB 的阈值判定。
  7. print("docs per shard before:", [s.count() for s in shards])输出均衡前的文档分布,可以清楚看到热点集中在 shardA。

常见误区

误区“反正 MongoDB 会自动分片,我随便挑个字段当分片键,数据多了自然就均衡了。”

正解分片键决定数据怎么切,选错会直接造出“热点仓库”。它必须基数高、写入分散、查询常带。好比连锁火锅店按“辣度”分仓,全国人民都吃辣,不辣仓就一直空转。分片键是架构第一决策,且后期极难修改。

为何常见:新手容易把“分片”误解成数据库自动魔法,忽略了切分维度本身的物理意义。

误区“chunk 超过 64MB 就会自动 split,所以我只管写入就行。”

正解split 需要有中间值可切。如果分片键单调递增,新数据总落在最边缘的 chunk,而这个 chunk 的区间里已经没有可分界的键值,就会变成 jumbo,既不 split 也不迁移。自动拆分的前提是“区间内还有中间值”。

为何常见:只看到文档里写的“自动拆分”,没理解拆分的数学前提是键值域可继续二分。

误区“balancer 一直在跑,我大量写入后,应该几分钟就均匀了吧?”

正解balancer 有触发阈值(chunk 数差异超过 8 才动手)、有运行窗口、每次迁移要逐文档拷贝数据并耗 IO。一个大 size 的迁移 cycle 跑几小时很正常。“自动”不等于“立即”。

为何常见:把“后台异步任务”等同于“实时同步”,忽略了异步任务的调度周期与迁移成本。

误区“扩容嘛,跑一条 addShard 命令,睡一觉数据就均匀了。”

正解addShard 只是“挂上了仓库”,真正搬货的是 balancer,它逐 chunk 迁移,迁移期间源和目标 shard 都有额外 IO 与网络开销。所以扩容要选低峰期、提前评估迁移量,必要时手动调大 balance window。

为何常见:只看到“一条命令扩容”的表象,忽略了背后真实的数据搬迁工程。

误区“config server 才几十 MB 元数据,丢了大不了重新分片。”

正解config 保存 chunk 到 shard 的映射,丢了等于总部目录被烧毁,mongos 无法路由,数据虽然还在但“找不到”。所以 config 必须 3 节点副本集,并且纳入备份体系。

为何常见:元数据体积小,但它是整个路由系统的“寻址表”,重要性被严重低估。

面试题

Q1. MongoDB 分片集群中负责路由客户端请求的是哪个组件?

单选题 · easy

  • A.shard 存储数据的分片
  • B.mongos 集群路由入口← 正确答案
  • C.config server 配置中心
  • D.mongodump 备份工具

答案:B。mongos 就像火锅店的前台,应用只跟它打交道,它查总目录(config)后把请求转到正确的仓库(shard),本身不存数据,所以能水平部署多台。shard 只管存,config 只存元数据,mongodump 是备份工具。

Q2. 关于分片键“基数”的说明,正确的有?

多选题 · easy

  • A.基数越大越利于分布均匀← 正确答案
  • B.基数越小越可能造成热点← 正确答案
  • C.基数与数据分布没有关系
  • D.基数只影响查询速度

答案:AB。基数就是分片键不同取值的个数。基数大,才有足够多的 chunk 摊到各个 shard;基数小(比如只有“辣/不辣”两档),数据最多切成两块,必然形成热点。C 和 D 把基数的作用完全说反了。

Q3. MongoDB 默认 chunk 的目标大小是多少?

单选题 · easy

  • A.16MB 上限文档大小
  • B.32MB 这个常见数值
  • C.64MB 默认分片大小← 正确答案
  • D.128MB 大文档友好

答案:C。默认 maxChunkSize 是 64MB。注意 16MB 是单个文档大小上限,两者是不同概念;chunk 超过 64MB 会触发 split 尝试,拆不动就会变成 jumbo。

Q4. 关于 chunk 迁移流程,下列说法正确的是?

单选题 · mid

  • A.迁移原子且瞬间完成
  • B.先拷贝再删除更新元数据← 正确答案
  • C.迁移期间暂停业务写入
  • D.迁移只更新元数据记录

答案:B。balancer 迁移像搬家车队:先把货架上的货一件件搬到新仓库(拷贝),搬完确认无误后旧仓库才清空,最后更新总部目录(config)。整个过程业务读写不中断,也绝不是只改账本。

Q5. 以下哪种分片键最可能导致“写热点”?

单选题 · mid

  • A.按 user_id 哈希分片
  • B.按城市名均匀分布分片
  • C.按组合字段城市加用户分片
  • D.按自增字段单调递增分片← 正确答案

答案:D。自增字段是“永远在增长”的编号,新写入永远落在值域最右端的 chunk,等于所有新货都堆在同一个仓库门口;哈希分片能把连续编号打散到全部 chunk。

Q6. 关于分片集群组件,以下说法正确的有?

多选题 · mid

  • A.每个 shard 本身建议使用副本集保证高可用← 正确答案
  • B.config server 推荐 3 节点副本集← 正确答案
  • C.mongos 是无状态的,可以部署多台← 正确答案
  • D.shard 必须单节点才能完成分片

答案:ABC。mongos/前台无状态可以多台;shard/仓库必须副本集避免单点;config/总目录必须副本集,因为它挂了没人知道食材在哪。D 错在 shard 单节点恰恰是运维大忌。

Q7. jumbo chunk 会带来什么后果?

单选题 · mid

  • A.无法被 balancer 迁移卡住← 正确答案
  • B.数据会自动丢失找不回
  • C.整个集群会直接宕机
  • D.分片键被清零重新分配

答案:A。jumbo 是“大到拆不动的货架”,因为键值域只剩单值无法再 split,balancer 拒绝搬它,均衡被卡住。它不是致命故障,但会拖累数据分布与后续扩容。

Q8. 在分片集群中对不带分片键的字段做查询时,会发生什么?

多选题 · hard

  • A.mongos 会把查询广播到所有 shard← 正确答案
  • B.返回结果由 mongos 合并输出← 正确答案
  • C.只路由到第一个 shard 节点
  • D.性能较慢属于 scatter-gather← 正确答案

答案:ABD。查询不带分片键时,mongos 不知道去哪找,只能像全城寻人一样每个仓库都问一遍,再合并结果。这正是广播查询(scatter-gather),是分片集群查询的大忌,不会只查第一个 shard。

Q9. 集群新增一个 shard 后,数据分布会怎样变化?

单选题 · hard

  • A.数据立刻全部重新均匀分布
  • B.需要手工导出再导入才能分散
  • C.balancer 后台逐步迁移服务不停← 正确答案
  • D.新 shard 只会接收新写入数据

答案:C。新增仓库后,调度车队会在后台把部分货架逐步搬去新仓库,这是渐进的自动过程,期间业务照常服务;迁移受窗口与 IO 影响,所以扩容建议在低峰期做,而不是立刻均匀或手工搬家。

Q10. 分片键为“城市”字段且上海占 80% 数据时,最合理的优化是?

单选题 · extreme

  • A.使用复合分片键并对热点哈希← 正确答案
  • B.直接把城市字段删掉不用
  • C.加大 chunk 大小到 1GB 容量
  • D.只保留一个 shard 节点

答案:A。单值占比过高的键叫“倾斜键”,会让上海对应的 chunk 变成超级热点。用“城市+用户ID”复合键或对热点城市做二次哈希,能把上海的货摊到多个货架甚至多个仓库。删字段、加 chunk 大小、减 shard 都是反向操作。