广告位 · 页头(后台-模块管理-新建模块后替换此区域)

MongoDB分片集群由哪些核心组件构成?它们如何协同工作?

MongoDB分片集群要解决的核心问题是单机副本集在存储容量和写入吞吐接近上限后的扩展难题。通过将集合数据按照某个字段切分成多个区间,每个区间对应一个分片,集群可以在多台服务器上并行处理读写请求。与单纯的副本集相比,分片集群多出了两类关键角色:路由进程 mongos 和保存元数据的配置服务器,而每个分片本身依然是一个完整的副本集。整体来看,客户端只和 mongos 交互,由 mongos 根据分片键和元数据把请求转发到正确的分片,再把结果合并返回。

MongoDB分片集群由哪些核心组件构成?它们如何协同工作?

接下来会依次展开各个组件的职责、分片键的选择方法以及部署运维中的关键点。

一、核心组件与请求路由流程

分片集群由三类进程组成:mongos、配置服务器(config server)和分片(shard)。mongos 是一个轻量级的无状态路由节点,它本身不保存任何数据,只负责接收客户端请求,查询配置服务器获取目标分片信息,然后向对应分片发起操作。由于 mongos 不存储状态,生产环境通常部署多个 mongos 实例并挂载在负载均衡器之后,以实现高可用和水平扩展。

配置服务器是整个集群的元数据中心,它保存集群中有哪些分片、哪些数据库和集合启用了分片、每个集合的分片键是什么、每个数据块(chunk)落在哪个分片上等信息。配置服务器必须以副本集形式部署,官方要求至少三个节点,因为一旦配置服务器不可用,mongos 将无法获取路由信息,整个集群会陷入不可用状态。

分片是实际存储数据的角色,每个分片是一个副本集。一个分片可以包含多个分片集合的不同数据块,也可以作为一个普通的非分片集合的存储位置。当数据块在分片之间迁移时,目标分片会从源分片读取数据并写入本地,迁移完成后更新配置服务器中的 chunk 分布元数据。

一次典型的查询流程如下:客户端连接 mongos 并发出查询,mongos 先检查本地缓存中是否有目标集合的元数据;如果没有或已过期,则向配置服务器查询该集合的分片键和 chunk 区间。然后 mongos 根据查询条件计算出目标数据可能落在哪些分片上,将请求并行发送给相关分片。如果是范围查询且没有覆盖所有分片,mongos 只会访问必要的分片;如果查询条件不包含分片键,则必须广播到所有分片。最后 mongos 将各分片返回的结果合并,如果需要排序还要进行全局归并。

# 启动mongos,连接配置服务器副本集
mongos --configdb cfgReplSet/cfg1:27019,cfg2:27019,cfg3:27019 --port 27017
# 登录mongos
mongosh --port 27017
# 添加分片副本集
sh.addShard("shardReplSet/shard1:27018,shard2:27018,shard3:27018")
# 启用数据库分片
sh.enableSharding("ecommerce")
# 对集合orders按userId做哈希分片
sh.shardCollection("ecommerce.orders", { "userId": "hashed" } )

二、分片键选择与数据分布机制

分片键决定了文档被分配到哪个分片的规则,是分片集群设计中最关键的决策。MongoDB 支持两种分片策略:范围分片(ranged sharding)和哈希分片(hashed sharding)。范围分片按照分片键的实际值划分 chunk,比如以 createDate 为分片键时,相邻日期的文档会落在同一个 chunk 中。这种方式对范围查询非常友好,因为 mongos 可以精确地将查询路由到一个或少数几个分片。但范围分片有一个明显弱点:如果分片键是单调递增的,新写入的数据总是集中在最后一个 chunk,导致单个分片承受全部写入压力,形成热点。

哈希分片则对分片键值计算哈希,然后根据哈希值的范围划分 chunk。这样做的好处是即使原始字段单调递增,哈希后的分布仍然均匀,写入会被分散到多个分片。代价是范围查询无法利用分片键定位,只能广播到所有分片,因为哈希值不保留原值的顺序关系。

选择分片键时需要综合考虑几个因素:一是字段基数要足够大,避免过少的唯一值导致 chunk 难以分裂;二是查询模式要尽量包含分片键,否则会发生散射-收集(scatter-gather)查询;三是写入模式要避免单调递增或聚集热点。常见做法是把高基数字段如 userId 或 orderId 作为哈希分片键,既能均匀分布又不会破坏单文档查询。

数据分布的最小管理单元是 chunk,默认大小为 64MB。当一个 chunk 内数据量超过阈值时,MongoDB 会自动将其分裂成两个较小的 chunk。后台均衡器(balancer)会监控各分片之间的 chunk 数量差异,当差异超过迁移阈值时,自动将 chunk 从负载较高的分片迁移到负载较低的分片。注意 chunk 迁移会消耗网络和磁盘 IO,因此运维时可以设置均衡窗口避开业务高峰期。

# 查看分片状态
sh.status()
# 查看集合的chunk分布
use config
db.chunks.find({ ns: "ecommerce.orders" }).sort({ min: 1 })
# 手动启动均衡器
sh.startBalancer()

三、部署架构与高可用实践

生产环境的分片集群至少需要三个配置服务器节点、若干分片副本集以及多个 mongos 实例。配置服务器必须使用三节点副本集,避免单点故障。每个分片应该是一个至少包含一个主节点和一个从节点的副本集,最好增加一个仲裁节点或隐藏节点以应对宕机。所有组件之间的通信建议使用内网,并开启认证和 TLS 加密。

mongos 的数量取决于客户端并发数,因为它是无状态的,可以横向扩展。通常会在应用服务器旁边部署一个 mongos,或者在每一台应用服务器上运行一个 mongos 进程,这样客户端连接本地 mongos 可以减少网络延迟。但要注意 mongos 需要能够访问配置服务器和所有分片。

扩容分片集群的核心操作是添加新的分片,命令为 sh.addShard()。新分片加入后并不会立即分担压力,需要等待均衡器将部分 chunk 迁移到新分片上。迁移过程对业务是透明的,但会占用源分片和目标分片的资源,因此建议在低峰期进行,并且一次不要迁移过多 chunk。监控方面需要重点关注 balancer 是否持续活跃、chunk 数量是否长期不均衡、mongos 的请求延迟以及分片副本集的主从延迟。

# 添加新分片
sh.addShard("newShardReplSet/newShard1:27018,newShard2:27018,newShard3:27018")
# 设置均衡器只在凌晨2点到4点运行
sh.setBalancerState(true)
use config
db.settings.updateOne(
  { _id: "balancer" },
  { $set: { activeWindow: { start: "02:00", stop: "04:00" } } },
  { upsert: true }
)

四、常见误区与排查思路

一个典型误区是认为分片键一旦设定就不能修改,实际上目前 MongoDB 仍然不支持直接修改已有分片集合的分片键,但可以通过创建新集合并迁移数据来变相调整。所以在首次分片前一定要充分测试分片键的分布和查询表现,避免上线后重新分片带来的停机窗口和数据迁移成本。

另一个常见问题是查询性能反而比单机副本集更差。这通常是因为查询没有包含分片键,导致 mongos 需要广播到所有分片,每个分片都执行查询,最后由 mongos 合并排序。此时即使每个分片返回少量数据,网络往返和合并开销也会放大延迟。解决办法是调整查询模式,尽量将分片键作为过滤条件,或者通过二级索引和部分索引减少扫描范围。

热点问题不仅与分片键有关,也可能来自大文档或大数组字段。例如某个分片键值对应的文档特别大,或者 chunk 分裂不及时,都会造成单个分片负载过高。通过 sh.status() 查看 chunk 分布,如果发现某些分片的 chunk 数量明显多于其他分片,可以手动触发均衡或检查是否存在 oversized chunk。必要时可以调整 chunk 大小,但需要谨慎,过小的 chunk 会导致频繁迁移,过大的 chunk 会使均衡粒度变粗。

最后,配置服务器的元数据一致性至关重要。不要手动修改配置服务器中的 config 数据库内容,所有分片相关操作都应通过 mongos 执行。如果配置服务器发生故障,需要尽快从副本集中恢复,并确保至少一个节点能提供读操作。定期备份配置服务器数据与备份业务数据同等重要。

广告位 · 文章底部(后台-模块管理替换此区域)