做 MongoDB 的同学迟早会碰到一个绕不开的话题:分片集群(Sharded Cluster)。我第一次看分片集群架构图时,Config Server、Mongos、Shard 三个角色摆在眼前,知道它们各管一摊,但真到配置的时候,才发现文档里的“角色”“副本集”“片键”串起来并不容易。这篇文章会把这三个组件拆开讲清楚:它们分别解决什么问题、配置里有哪些关键项、生产环境里有哪些容易踩的坑,最后给出一套可以直接动手的搭建流程。适合刚把单机 MongoDB 玩熟、准备往集群方向推的同学,也适合被线上分片集群折腾过、想系统补一补运维细节的人。
1. 分片集群组件到底在解决什么问题
1.1 单机和副本集的边界在哪
MongoDB 单机好装,副本集也容易理解:一份数据存多个节点,主节点写,从节点同步,主挂了自动选主。但副本集解决的是高可用,并没有解决“一台机器放不下”的问题。当数据量到了 TB 级,或者单节点的写入 QPS 把 CPU 打满,你会发现自己陷入两难:升配确实简单,但越贵的机器性价比越低;不升配,单机延迟和磁盘占用迟早拖垮你。
分片集群的思路不是把机器升级成超级计算机,而是把数据切碎之后,均匀放到多台普通服务器上。每台服务器只负责整体数据的一个子集,写一条记录时由 Mongos 路由到对应 Shard,读一条记录时也只访问包含该记录的 Shard。如果之前的副本集是“一个团队合租一间大办公室”,分片集群就是“按部门拆到不同工区,再由前台引导访客找到对应座位”。
1.2 三个组件各管哪一块
一个最小可用的分片集群拓扑里,至少要有三种角色:Config Server、Mongos、Shard。它们各自的职责差别很大,别混着记。
| 组件 | 是否持久化数据 | 主要职责 | 端口惯例 |
|---|---|---|---|
| Config Server | 持久化元数据 | 保存分片路由、集合分片规则、配置信息 | 27019 |
| Mongos | 不持久化 | 路由查询、合并结果、转发写请求 | 27017 |
| Shard | 持久化用户数据 | 真正存储业务数据,通常以副本集形式存在 | 27018 |
可以这样理解:Config Server 是“地图”,Shard 是“仓库”,Mongos 是“前台问询处”。客户端只需要连 Mongos,不需要知道数据落在哪台 Shard 上;Mongos 每次请求先去 Config Server 查地图,再找到对应的仓库。地图出问题,整个集群就像快递员没了导航,所有路由动作都会瘫痪。
1.3 什么时候该上分片
我先说结论:数据量没到几百 GB、单一集合没上千万,或者写入瓶颈还没出现,别急着拆集群。分片本身会引入额外的组件运维、chunk 迁移和路由开销,会让一些简单查询变慢。如果只是开发测试,副本集完全够用。
真正考虑分片集群,一般逃不过下面几个信号:
- 单机磁盘容量已经接近上限,压缩和清理都解决不了;
- 热数据集的写 QPS 高到单机 CPU 或 IO 长期在 70% 以上;
- 单个集合数据量过大,备份和恢复时间长到无法接受;
- 你需要把不同热点数据隔离到不同物理节点,做资源池化。
项目里如果同时出现两三条,分片集群就是合理方向;如果只是偶发慢查询,先排查索引,别让分片背锅。
2. Config Server:集群里最容易被低估的元数据中心
2.1 它到底存了哪些数据
Config Server 在 MongoDB 里表现为一个有数据目录的 mongod 进程,内部会形成一个名为 config 的库。这个库里包含 shards、databases、collections、chunks、settings、mongos 等集合。简单说,集群里每个数据库叫什么、每个集合分布在哪些分片、chunk 的边界是什么、当前 balancer 是否开启,全都在这里。
权限信息也和它有关。分片集群里的 admin 库数据由 Config Server 维护,所以新建用户、修改角色,最终也要落在 Config Server 上。我见过有人在 Shard 节点上直接建账号,结果重启后 Mongos 完全认不到,就是因为认错了元数据的位置。Config Server 的数据量通常不大,但它是整个集群的“大脑”,重要性远超你的直观感受。
2.2 为什么 3.4 之后 Config Server 必须是副本集
MongoDB 3.4 之前,Config Server 可以当作普通 mongod 跑,甚至能用单节点。这样做的最大问题是“地图”挂了,Mongos 虽然还活着,却没法知道新的 chunk 映射,查询会大面积报错。
3.4 之后官方直接砍掉了单节点 Config Server 的玩法,强制要求以副本集形式部署,并给了专门的 clusterRole: configsvr。这样路由元数据本身有了多副本,写请求会通过副本集协议复制到多个节点。要注意的是,这个副本集里不要和业务数据混用一个 mongod 进程,也不要图省事把它部署成“一个数据节点加一个 arbiter”。Config Server 通常建议至少三个数据节点,并且别放在同一台物理机或者同一个故障域里,否则副本集在形式上可用,实际上还是单点。
2.3 Config Server 的配置文件与初始化
配置成 Config Server 的核心点有三个:sharding.clusterRole设为configsvr,replication.replSetName必须设置,端口按团队规范最好用 27019。一个最小配置示例:
# /etc/mongod-configsvr-1.conf systemLog: destination: file logAppend: true path: /var/log/mongodb/configsvr-1.log storage: dbPath: /data/configdb-1 net: bindIp: 0.0.0.0 port: 27019 processManagement: fork: true replication: replSetName: configReplSet sharding: clusterRole: configsvr第一个节点启动后,连到 27019 执行初始化:
rs.initiate({ _id: "configReplSet", configsvr: true, members: [ { _id: 0, host: "192.168.10.11:27019" }, { _id: 1, host: "192.168.10.12:27019" }, { _id: 2, host: "192.168.10.13:27019" } ] })这里如果漏掉configsvr: true,后面启动 Mongos 时大概率会报错,或者路由不可用。初始化完成后,用rs.status()确认三个节点都正常同步。
2.4 备份和容灾最容易踩的坑
Config Server 的数据量不大,但重要性是全局性的。备份时不要只想着 mongodump 导出 config 库,因为要保证和 chunk 状态一致,一定要先做“全局一致”的快照。用云盘快照或 LVM 快照时,建议先确保文件系统一致。
我维护过的集群里,Config Server 最大的坑不是崩溃,而是“还有一台节点活着,但和另外两台数据不一致”。这种分裂通常由网络分区触发,恢复时不能简单把旧节点重新加回副本集,否则可能把新路由信息回滚。真要恢复,先把存活节点的元数据完整备份,再决定是让少数派节点重新全量同步,还是从备份里做恢复。千万别“看着哪台顺眼就重启哪台”。
3. Mongos:无状态的路由层,客户端唯一入口
3.1 Mongos 只是一个转发器?
可以这么理解,但不全面。Mongos 不像 Nginx 那样只做流量转发,它还需要解析查询条件、判断数据落在哪些 chunk、把多个 Shard 的结果做 merge。比如对分片集合执行带排序的 find,Mongos 会把排序下推给各个 Shard,再把每个 Shard 返回的局部有序结果做归并排序。如果查询没带片键,Mongos 就得把请求广播到所有 Shard,再对结果做 merge,这种查询在分片集群里最费资源。
Mongos 进程本身不落盘业务数据,也没有自己的副本集。它的“数据”只是一份在内存里缓存的 Config Server 路由信息,启动时从 Config Server 加载,平时也会通过订阅更新。因为无状态,所以可以任意增删节点,重启也不会丢任何业务数据。对外默认端口是 27017,和普通 mongod 一样,但连接到的进程类型完全不同。
3.2 配置 Mongos 的正确方式
Mongos 的配置比 mongod 简单,但有一个参数是核心:sharding.configDB。它指向 Config Server 副本集,格式是副本集名称/节点1:端口,节点2:端口,节点3:端口。配置示例:
# /etc/mongos-1.conf sharding: configDB: configReplSet/192.168.10.11:27019,192.168.10.12:27019,192.168.10.13:27019 net: bindIp: 0.0.0.0 port: 27017 systemLog: destination: file logAppend: true path: /var/log/mongodb/mongos-1.log processManagement: fork: true启动时执行mongos -f /etc/mongos-1.conf即可。我在这里吃过一个亏:如果configDB里的副本集名字和 Config Server 初始化时的_id不一致,Mongos 会一直报 “Unable to reach config server”。这时候不要先怀疑网络,先对一下名称。
3.3 高可用方案:多 Mongos 加驱动连接串
因为 Mongos 是无状态的,高可用主要靠数量。生产环境一般起两个以上 Mongos,前端通过负载均衡或者驱动连接串里写多个地址接入。使用官方驱动的连接串可以写成:
mongodb://mongos1:27017,mongos2:27017,mongos3:27017/appdb分片集群本身不是副本集,连接串里不需要replicaSet参数,驱动会自行处理多个 Mongos 的故障转移。注意不要在 Mongos 前面再套一层随意断连的连接池代理,否则每次冷启动都会让连接建立特别慢。如果域名层面能做到多 IP 轮询,也可以直接用 SRV 记录配合mongodb+srv://,但对分片集群来说,多个 host 地址最直观。
3.4 Mongos 调优和故障定位心得
Mongos 最常见的“假故障”是连接数过高。它自己不存储数据,但每个客户端连接都会占用文件描述符和内存。线上建议调整net.maxIncomingConnections,同时监控操作系统的 open files 限制。另一个现象是 Mongos 刚重启后路由缓存是冷的,第一次访问某些集合会比较慢,这是正常的,跑一会儿会恢复。
排查问题时,不要直接在 Shard 上去执行面向业务的查询。虽然单节点 mongod 也能查,但它不知道整个集群的路由,查出来的只是本分片上的局部数据。我经常看到有人连错端口,明明要连 Mongos 27017,结果连到 Shard 的 27018,查出来的数据只有一半,还误以为是分片丢了数据。
4. Shard:真正存储数据的位置
4.1 Shard 本身可以是副本集
一个分片并不要求是一台裸 mongod,生产环境里每个 Shard 通常是一个独立副本集。副本集内部有 primary、secondary、arbiter 等角色,对外则作为一个整体加入分片集群。这样一个 Shard 挂了,它的副本集可以自动选出新的 primary,数据不丢,集群整体不受影响。
单个 Shard 的主从和普通副本集配置几乎一样,只是在sharding.clusterRole里要设置为shardsvr。这个角色让 mongod 允许自己接受来自 Mongos 的路由命令,也会参与 chunk 分裂和迁移。如果你把一个没有shardsvr角色的普通 mongod 当作 Shard 添加,Mongos 会拒绝。
4.2 主分片和 chunk:数据切碎的粒度
分片集群里有一个“主分片”的概念。开启分片前,一个数据库的所有数据都放在某个 Shard 上,这个 Shard 叫数据库的主分片。启用数据库分片之后,系统才会开始为集合拆 chunk;而没有被分片的集合,仍然会整体留在主分片。
chunk 是数据分布的最小逻辑单位,MongoDB 默认一个 chunk 约 64MB。这个 64MB 不是物理文件上的固定大小,而是根据数据量估算出来的逻辑边界。当某个 chunk 增长到超过阈值,系统会把它分裂成两个,再通过 balancer 把多余的 chunk 迁移到其他 Shard 上。片键的值决定了 chunk 的边界,所以片键设计直接决定了数据是否均匀。
4.3 片键选择:决定集群命运的选择题
片键是分片集合的灵魂。选片键之前,要想清楚业务查询是“按范围”查得多,还是“按单点”查得多。两种最常用的策略:
- 哈希片键(hashed):对片键字段做哈希后路由,能打散单调递增的值,写入分布比较均匀,适合日志、订单这类按 ID 点查或并发写入很高的场景。
- 范围片键(ranged):按字段值范围划分 chunk,适合明显的范围查询,比如按时间查报表。但如果用时间戳作为范围片键,写入会一直打到最后一段,容易出现“单点热点”。
一个好的片键要满足三个条件:基数高、出现频率均衡、不能被大量更新。比如_id用哈希就很常见,而status这种只有几个取值的字段,即使拆出 chunk,数据也很难均匀分布。片键一旦选错,早期看不出问题,数据量上来后,某个 Shard 的磁盘会快速被塞满,而其他 Shard 还在空闲。
4.4 Shard 配置与扩容方式
Shard 的配置不复杂,重点是角色参数。以单个 Shard 副本集为例:
systemLog: destination: file logAppend: true path: /var/log/mongodb/shard1-1.log storage: dbPath: /data/shard1-1 net: bindIp: 0.0.0.0 port: 27018 processManagement: fork: true replication: replSetName: shardReplSet1 sharding: clusterRole: shardsvr初始化副本集和普通副本集一样。之后通过 Mongos 执行sh.addShard()把这个副本集加入集群。要扩容时,再启动一个新 Shard 副本集,同样执行sh.addShard(),然后 balancer 会把部分 chunk 自动迁过去,整个过程不用停业务。注意扩容不是瞬间完成,chunk 迁移需要时间,迁移期间集群会多消耗一些 IO 和带宽,最好在低峰期触发。
5. 分片路由机制:一次查询如何找到数据
5.1 Mongos 的路由缓存与 Config Server 的同步
Mongos 之所以能快速路由,依赖的是内存里的路由缓存。它启动时会把 Config Server 上的一部分元数据加载进来,比如集合的 chunk 范围、每个 chunk 所在的 Shard。当 Config Server 上的 chunk 布局发生变化,Mongos 会收到一个无效化消息,下次查询时重新拉取最新信息。
这个机制有一点像 DNS 缓存:平时看起来很顺,但刚重启或者元数据频繁变化时,会短暂出现“路由还在用旧数据”的情况。生产环境如果在大量写入的同时迁移 chunk,偶尔碰到一次ChunkNotFound并不罕见,多半是路由缓存还没刷新。遇到时可以通过重试查询或者等待一下让 Mongos 重新获取元数据,而不是立刻重启所有节点。
5.2 查询场景:命中片键、广播查询、排序合并
分片集群里查询按是否带片键可以分成两类:
- 查询条件包含片键字段:Mongos 能根据片键值直接算出对应的 chunk 范围,只把请求发给必要的 Shard。这种查询最便宜,延时可预测。
- 查询条件不包含片键字段:Mongos 只能把查询广播给所有 Shard,每个 Shard 各自执行,再把结果汇总给 Mongos。这种查询就是“全表扫描”的分布式版本,越到后期数据量越大越慢。
如果查询条件不完整,又没有二级索引,分片集群会把问题放大很多倍。我建议所有核心查询尽量带上片键字段,或者在非片键字段上建好索引,让 Mongos 至少能在每个 Shard 上利用索引降低扫描量。对于sort和limit,Mongos 会把排序和截断尽量下推,减少传输量,但如果条件本身是广播查询,结果集仍然会被完整传到 Mongos 做合并。
5.3 写入流程与 chunk 分裂迁移
写入请求到 Mongos 后,Mongos 根据文档的片键值计算它属于哪个 chunk 范围,然后转发给对应 Shard 的 primary。如果这个 chunk 数据量接近阈值,Shard 会触发 split,把一个 chunk 拆成两个。随后 balancer 会看各个 Shard 的 chunk 数量差距,决定是不是要迁移 chunk。
这正是分片集群的核心魅力:数据切分不是靠人工指定“前 100 万条去 A,后 100 万条去 B”,而是由系统自动按 chunk 管理。这也解释了为什么片键选择很重要——如果大量写入都落在同一个范围,那么所有 split 都发生在同一个 Shard 上,其他 Shard 闲着,热点问题依旧存在。理解这条链路后,再去看sh.status()输出,很多异常就能一眼定位。
6. 从零搭建一套分片集群:完整实操记录
6.1 节点规划和目录准备
这套流程适合本地练习,也适合照改成生产脚本。假设我用三台机器跑最小集群:节点 A、B、C,每台都部署一个 Config Server 节点、一个 Shard 节点、一个 Mongos。虽然生产上不建议把角色混在一台机器,但做演示足够了。严格生产建议把三种角色分开,至少 Config Server 和 Shard 不要混装。
先规划端口:Config Server 用 27019,Shard 用 27018,Mongos 用 27017。目录方面,每台机器准备好:
mkdir -p /data/configdb mkdir -p /data/sharddb mkdir -p /var/log/mongodb然后把配置文件分别写好。下面我按三个角色的顺序说明。
6.2 初始化 Config Server 副本集
在三台机器上分别启动 Config Server:
mongod -f /etc/mongod-configsvr.conf然后任意选一个节点进入:
mongosh --port 27019执行初始化:
rs.initiate({ _id: "configReplSet", configsvr: true, members: [ { _id: 0, host: "nodeA:27019" }, { _id: 1, host: "nodeB:27019" }, { _id: 2, host: "nodeC:27019" } ] })执行完rs.status()应该能看到三个节点的 stateStr 变成 PRIMARY 或 SECONDARY。如果 host 名解析有问题,可以在/etc/hosts里配置对应关系,避免后续 Mongos 因为找不到 host 报错。
6.3 初始化 Shard 副本集
在同样的三台机器上启动 Shard mongod 进程。注意每个节点用自己的 dbPath 和日志路径,进程参数保持shardsvr角色。启动后,找一个节点执行:
rs.initiate({ _id: "shardReplSet1", members: [ { _id: 0, host: "nodeA:27018" }, { _id: 1, host: "nodeB:27018" }, { _id: 2, host: "nodeC:27018" } ] })到这里,Shard 还只是一套独立的副本集,Mongos 还没起来,它并不知道自己属于哪个集群。
6.4 启动 Mongos 并挂载分片
在三台机器上分别启动 Mongos:
mongos -f /etc/mongos.conf进入其中一个 Mongos:
mongosh --port 27017先添加 Shard:
sh.addShard("shardReplSet1/nodeA:27018,nodeB:27018,nodeC:27018")然后启用数据库分片并指定片键:
sh.enableSharding("appdb") sh.shardCollection("appdb.users", { userId: "hashed" })添加完成后,用sh.status()查看输出。如果看到shards里已经有 shardReplSet1,且databases里 appdb 的partitioned为 true,说明分片已经挂上。
6.5 验证分片确实生效
分片生效不是看命令执行不报错,而是看数据是否真的散到多个 chunk 上。可以插入一些模拟数据,然后反复执行sh.status()观察 chunk 数量。刚创建的分片集合只会有一个 chunk,且全部在某个 Shard 上,等插入的数据量超过 split 阈值后,会看到 chunk 被拆开并迁移。
如果想快速验证,可以连到 Mongos 执行:
db.users.getShardDistribution()这个命令会把当前集合在各 Shard 上的数据量和占比直接列出来。如果某个 Shard 显示 100%,说明集群虽然建了,但数据还没真正分布开。
7. 常见问题与排查技巧实录
7.1 Mongos 启动报 Connection refused
最常见的四个原因:Config Server 没起来、配置里的configDB写错、副本集名不一致、网络层禁了 27019 端口。排查时先在 Mongos 机器上直接测:
mongosh "mongodb://192.168.10.11:27019/config"如果手动能连上 Config Server,再检查 Mongos 启动日志。日志里如果出现Unable to reach config server,先把/etc/hosts和配置文件里的 host 名统一,再重新启动 Mongos。还要确认 Config Server 当前确实有 primary 节点,副本集如果长期没有 primary,Mongos 也无法工作。
7.2 集合没有真正分片
有时配置完sh.shardCollection()后,插入大量数据,发现所有数据还是落在一个 Shard,sh.status()里 chunks 也只有一条。这个问题多半是片键选得太差,比如选了一个低基数字段。如果业务数据里的字段只有 true/false 两种值,系统没法把它切成多个有意义的范围,数据自然都堆在一个 chunk 里。
另一种情况是还没达到 split 阈值。默认 64MB 的逻辑块,如果总数据量只有几十 MB,chunk 不分裂很正常。想确认片键是否合理,可以用 explain 看查询是否走了分片路由:
db.users.find({ userId: "u123" }).explain("executionStats")如果看到shards里只有一个 Shard,说明带上了片键;如果shards列出了所有 Shard,说明这次查询被广播了,需要警惕。
7.3 balancer 不干活或迁移过慢
Balancer 是分片集群里负责均衡 chunk 的后台进程。它默认是开的,但可能因为 Config Server 负载过高、迁移窗口设置、或某个 Shard 磁盘满而停止。我遇到过的问题是 balancer 一直显示 waiting,最后发现是有一个 Shard 的磁盘剩余空间不足,迁移任务被反复中止。
排查时可以看sh.status()里的 balancer 字段,也可以查 Config Server 上的config.locks集合。手动停止或开启 balancer 使用sh.stopBalancer()和sh.startBalancer(),但生产环境不要只靠重启 balancer 解决问题,先找到迁移阻塞的原因。
7.4 更新片键、缩容等敏感操作
分片集群一旦真正跑起来,就不能像单机集合那样随意改结构。缩容 Shard、关闭分片、更换片键,都属于“能不做就不做”的高级操作。如果确实要调整,我的经验是先完整备份 Config Server 和对应集合的数据,再在 staging 环境练一遍操作流程。
早期版本改片键基本等于重建集合,要先导出再导入。4.4 之后提供了refineShardKey,5.0 开始也有reshardCollection,但代价依然不小,不能把它当成普通 alter table。任何这类操作都要在变更窗口执行,并且准备好回滚方案。
7.5 关于运维监控和版本升级的几条个人建议
最后说几条我实际踩过后的体会。第一,分片集群必须监控 Config Server 的磁盘和延迟,它看起来数据量小,但一旦磁盘满,整个集群写入都会断。第二,Mongos 要多起几个,接入端配置多地址连接串,单纯依赖一台 Mongos 没有意义。第三,升级时先升 Config Server,再升 Mongos,最后升 Shard,整个过程别跳大版本,最好按官方升级路径走。我升级时因为先滚动了 Shard,导致 Mongos 缓存了旧的连接信息,一小时内部分查询出现了shard not found,后来才明白顺序很重要。
第四,别把所有 Shard 放在同一批机柜或者同一台物理机上,故障域隔离比形式上的副本集更重要。分片集群本身就是为了横向扩展,如果底层物理资源被一个故障点带走,架构再漂亮也白搭。第五,平时养成定期查看sh.status()的习惯,关注 chunk 数量分布,不要等问题报警了再上去查。分片集群的日常运维,很多时候就是把这些“慢变量”盯住,比优化一条 SQL 更能保障稳定性。