做 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持久化元数据保存分片路由、集合分片规则、配置信息27019Mongos不持久化路由查询、合并结果、转发写请求27017Shard持久化用户数据真正存储业务数据通常以副本集形式存在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设为configsvrreplication.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。比如对分片集合执行带排序的 findMongos 会把排序下推给各个 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 记录配合mongodbsrv://但对分片集群来说多个 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和limitMongos 会把排序和截断尽量下推减少传输量但如果条件本身是广播查询结果集仍然会被完整传到 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 用 27019Shard 用 27018Mongos 用 27017。目录方面每台机器准备好mkdir -p /data/configdb mkdir -p /data/sharddb mkdir -p /var/log/mongodb然后把配置文件分别写好。下面我按三个角色的顺序说明。6.2 初始化 Config Server 副本集在三台机器上分别启动 Config Servermongod -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 并挂载分片在三台机器上分别启动 Mongosmongos -f /etc/mongos.conf进入其中一个 Mongosmongosh --port 27017先添加 Shardsh.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 节点副本集如果长期没有 primaryMongos 也无法工作。7.2 集合没有真正分片有时配置完sh.shardCollection()后插入大量数据发现所有数据还是落在一个 Shardsh.status()里 chunks 也只有一条。这个问题多半是片键选得太差比如选了一个低基数字段。如果业务数据里的字段只有 true/false 两种值系统没法把它切成多个有意义的范围数据自然都堆在一个 chunk 里。另一种情况是还没达到 split 阈值。默认 64MB 的逻辑块如果总数据量只有几十 MBchunk 不分裂很正常。想确认片键是否合理可以用 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 之后提供了refineShardKey5.0 开始也有reshardCollection但代价依然不小不能把它当成普通 alter table。任何这类操作都要在变更窗口执行并且准备好回滚方案。7.5 关于运维监控和版本升级的几条个人建议最后说几条我实际踩过后的体会。第一分片集群必须监控 Config Server 的磁盘和延迟它看起来数据量小但一旦磁盘满整个集群写入都会断。第二Mongos 要多起几个接入端配置多地址连接串单纯依赖一台 Mongos 没有意义。第三升级时先升 Config Server再升 Mongos最后升 Shard整个过程别跳大版本最好按官方升级路径走。我升级时因为先滚动了 Shard导致 Mongos 缓存了旧的连接信息一小时内部分查询出现了shard not found后来才明白顺序很重要。第四别把所有 Shard 放在同一批机柜或者同一台物理机上故障域隔离比形式上的副本集更重要。分片集群本身就是为了横向扩展如果底层物理资源被一个故障点带走架构再漂亮也白搭。第五平时养成定期查看sh.status()的习惯关注 chunk 数量分布不要等问题报警了再上去查。分片集群的日常运维很多时候就是把这些“慢变量”盯住比优化一条 SQL 更能保障稳定性。