当单台机器的存储或写入吞吐到达天花板,副本集就不够了,需要水平扩展——分片(Sharding)。但分片是 MongoDB 里最容易踩坑的特性:片键选错,整个集群退化成单点;chunk 不均衡,迁移把集群拖垮。本文讲清分片集群的三大组件、数据如何分布、以及片键设计这个生死攸关的决定。
场景:分了片反而更慢
团队上了分片集群,本以为写入能线性扩展,结果发现绝大多数写都压在一个 shard 上,其他 shard 闲着。这是片键选了单调递增字段(如时间戳、自增 ID)的经典悲剧。要讲清为什么,先看分片集群长什么样。
机制一:三大组件
1 | ┌─────────────────────────────────────┐ |
- mongos:无状态路由进程。应用只连 mongos,它根据片键把请求路由到正确的 shard。可以部署多个做负载均衡。
- config servers:本身是一个副本集,存集群元数据——哪个 chunk 在哪个 shard、片键范围如何划分。元数据丢了集群就瘫,所以它必须高可用。
- shard:每个 shard 是一个独立副本集,存实际数据的一个子集。
mongos 把 config server 的元数据缓存在本地,据此判断一条查询该发给谁。
机制二:chunk 与数据分布
MongoDB 把分片集合按片键范围切成 chunk(默认逻辑上限 128MB,旧版本 64MB)。每个 chunk 覆盖一段连续的片键区间,归属某个 shard。
1 | chunk A: shardKey ∈ [minKey, "h") → Shard1 |
当一个 chunk 超过大小上限,会分裂(split)成两个。当 shard 之间 chunk 数量不均,balancer 后台把 chunk 从多的 shard 迁移到少的 shard(migration)。迁移是有代价的:复制数据 + 更新元数据 + 短暂的路由变更,高峰期可能影响性能,所以可以配置 balancer 只在低峰窗口运行。
机制三:分片策略——范围 vs 哈希
范围分片(ranged):按片键原始值分区。好处是范围查询高效(连续区间落在少数 chunk),坏处是单调递增片键会导致所有新写入都落到"最大值"那个 chunk → 热点 shard。这就是开头悲剧的成因。
哈希分片(hashed):对片键算哈希再分区,把数据均匀打散到所有 shard,写入负载均衡。代价是范围查询失效——查一个区间得 scatter 到所有 shard(broadcast query)。
1 | // 哈希分片:写入均衡,适合高写入、点查为主的场景 |
机制四:片键设计——分片集群的生死线
片键一旦选定极难更改(虽然新版支持 reshardCollection,但仍是重操作)。好片键要同时满足三个维度:
1 | 1. 高基数(Cardinality):取值足够多,否则 chunk 无法细分 |
举几个反例:
- 用
status(只有几种取值)→ 基数太低,整个集合只能分成几个 chunk,根本分不开。 - 用
timestamp或_id(ObjectId 前缀含时间,单调递增)→ 新写入永远在最右 chunk,热点。 - 用
userId(若某些大客户数据量极大)→ 单值频率过高,产生无法分裂的 jumbo chunk。
实战常用复合片键兼顾均衡与查询,比如 {region: 1, _id: 1}:region 提供分散,_id 提供高基数,同时支持按 region 的路由查询。或者直接 {userId: "hashed"} 求写入均衡。
机制五:targeted vs scatter-gather 查询
这是分片性能的核心区别:
1 | 查询带片键 → mongos 直接路由到 1 个 shard(targeted,快) |
1 | # 看一条查询是 targeted 还是 broadcast |
如果 shards 数组包含所有 shard,说明这是 scatter-gather,随集群规模增大而变慢。理想情况是大部分高频查询都带片键前缀,能被精确路由。带 sort/limit 的 scatter 查询尤其贵,mongos 要从每个 shard 拉够数据再做归并排序。
工程权衡与边界
- 何时该分片:不要过早分片。副本集 + 索引优化 + 垂直扩容能扛很久。分片引入运维复杂度(更多节点、balancer、元数据一致性),数据量(TB 级)或写吞吐确实超单机才上。
- jumbo chunk:超过大小上限又因为片键频率太高无法分裂的 chunk,会卡住 balancer,需要人工干预或重选片键。
- 孤儿文档(orphaned documents):chunk 迁移中断可能在源 shard 留下已迁走的数据副本,新版有 range deleter 清理,老版本需注意。
- 跨片事务/聚合:分布式事务和跨 shard 聚合代价高,设计 schema 时尽量让一个逻辑单元(如一个用户的所有数据)落在同一 shard(用片键前缀控制)。
常见误区与踩坑
- 误区:分片就能自动线性扩展。片键选错(单调递增/低基数)直接退化成单点甚至更慢。
- 踩坑:用
_id当片键做范围分片。ObjectId 单调,写入全打到一个 shard,要么 hashed 要么换片键。 - 踩坑:大量 scatter-gather 查询。查询不带片键时被广播,集群越大越慢,设计时让热点查询都能带片键。
- 误区:config server 不重要。它存全部元数据,必须是高可用副本集,挂了整个集群无法路由。
- 踩坑:balancer 在业务高峰迁移 chunk 拖慢集群。配置 balancer window 让它只在低峰运行。
小结
分片集群 = mongos 路由 + config server 元数据 + 多个 shard 副本集,数据按片键切成 chunk 分布并由 balancer 动态均衡。所有成败几乎都系于片键设计:高基数、低频率、非单调,且让高频查询能带片键被精确路由。分片是重武器,先把副本集和索引用到极致,真到单机瓶颈再上,并且把片键当成架构级决策慎重对待。