资讯详情

MongoDB复制集与分片集群实战:原理、搭建与排错

📅 2026/10/26 3:36:22 | 华诺云谱 👁 阅读
MongoDB复制集与分片集群实战:原理、搭建与排错
1. 复制集与分片到底解决什么问题别一上来就敲命令在沙箱环境里把 MongoDB 的复制集和分片跑通大概是每个后端方向的学习者都要迈的一道坎。我第一次接触这套东西的时候也是照着文档一路rs.initiate()、sh.addShard()敲下来rs.status()显示 PRIMARY 就觉得完事了结果换个场景——把主节点进程 kill 掉——整个应用直接连不上数据库报了一堆not master。说白了MongoDB 复制集Replica Set解决的是数据不丢、服务不断的问题分片Sharding解决的是一张表大到单机扛不住的问题这两个东西一个管可用性一个管扩展性本质上是两套独立机制但在生产里几乎总是叠在一起用。这篇文章我想讲清楚的是这两套机制背后的运行原理、实验环境里怎么一步步搭起来、参数到底该怎么算、以及我在沙箱里踩过的那些坑。适合刚摸 MongoDB 不久、能跑简单增删改查、但一碰到副本集选举和 mongos 路由就发懵的朋友也适合做完实验想搞明白为什么这么配的同学。1.1 复制集不是备份是自动接管很多人第一反应会把它当备份用这是个挺危险的误解。备份是错了能回去复制集是挂了能顶上两者解决的方向完全不一样。复制集的本质是一份数据存在多个 mongod 实例上通过 oplog操作日志持续同步主节点挂掉之后由剩下的成员投票选出新主。一次写入的完整链路大致是这样客户端连到主节点主节点把写操作应用到自己的数据文件同时写进local.oplog.rs这个特殊的固定大小集合从节点各自有一个 tailable cursor 一直挂在主节点的 oplog 上看到新条目就拉下来、重放、写进自己的数据。这条链路是异步的所以主节点写完就返回和多数节点都写完才返回之间的差别就是数据安全性的差别。成员数量上有个硬约束投票成员建议用奇数。因为选举需要多数派majority同意3 个成员能容忍 1 个挂掉5 个成员能容忍 2 个挂掉公式是floor((N-1)/2)。为什么强调奇数4 个成员的多数派是 33 个成员的多数派是 2前者比后者多花一台机器却换来完全一样的容错能力纯粹浪费。成员总数多数派门槛可容忍故障数是否推荐220不推荐等于没容错321推荐最常见431不推荐浪费一台532推荐跨机房场景常用743上限附近运维成本高还有一个容易被忽略的角色是仲裁节点arbiter它只参与投票、不存数据。当年资源紧张的时候很流行两数据节点 一仲裁看起来省了一台机器的磁盘。但我的实际体会是除非你真的很缺存储否则别用仲裁节点不存数据意味着数据冗余只有 2 份一旦其中一个数据节点磁盘损坏需要全量重同步期间没有任何冗余压力全压在一台上而且仲裁节点重启之后对 oplog 位置毫无概念投票时可能做出不太聪明的判断。1.2 分片是把一张大表拆成很多小表但不是随便拆分片的核心思路其实很朴素单机的磁盘、内存、CPU 都有上限那就按某个规则把数据切成若干份分散到不同机器上查询的时候由一层路由把请求转发到对的那台机器。MongoDB 的分片集群里三个角色分工非常明确config server配置服务器存的是元数据——哪个 chunk 在哪个分片上、分片集群有哪些成员、集合有没有开启分片。数据量极小但绝对不能丢所以从 3.4 版本开始强制要求它也部署成副本集业内叫 CSRSConfig Server as Replica Set。shard分片真正存数据的地方。生产环境每个 shard 本身也必须是一个副本集否则单个 shard 挂掉就等于整个集群缺了一块数据。mongos路由无状态的查询路由客户端只连 mongosmongos 去 config server 拿路由表然后直接把请求转发到对应 shard。mongos 可以随便开几个挂了也不影响数据。这里有个概念要先建立起来MongoDB 的拆分单位不是文档而是chunk块默认大小 64MB。一个集合会先被切成若干个 chunk然后由**均衡器balancer**在后台把 chunk 从多的分片搬到少的分片。所以你选分片键的时候真正在选的是数据按什么规则聚成块。维度复制集分片集群解决的核心问题高可用、故障自动切换水平扩展、突破单机容量数据副本每个成员一份全量数据每个 shard 只存一部分关键配置replSetName、priority、votes分片键、chunkSize、balancer最小可用规模3 个 mongod3 个 config 2 个 shard 1 个 mongos典型故障表现选举、回滚、同步延迟chunk 迁移卡住、热点写、路由广播1.3 两者叠加后的真实形态在生产里最常见的形态是分片集群的每个 shard 本身是副本集也就是 3 个 config server 组成一个副本集shard1 是 3 节点副本集shard2 是 3 节点副本集前面挂两三个 mongos总共十来个进程。这样任何一台机器挂掉无论是 config、shard 还是 mongos集群都能继续服务。沙箱环境当然不会给你这么多机器所以通常用单机多实例的方式模拟同一台机器开不同端口用不同 dbPath 隔离。这时候最要紧的是理解每个进程扮演的角色不然rs.status()里出现127.0.0.1:27019你不知道它是仲裁还是 shard就容易乱。2. 动手之前目录、端口、配置文件的准备工作跳过准备工作直接敲命令是实验环境里翻车率最高的环节。我见过太多人因为两个实例用了同一个 dbPath初始化之后发现数据莫名其妙被覆盖也见过端口冲突导致rs.initiate()一直卡在STARTUP2状态。这一步看着枯燥但它决定了后面几个小时是顺畅还是抓狂。2.1 目录与端口的规划思路我的习惯是把复制集实验和分片实验彻底分开做因为两组实验都会占用 27017 这个默认端口混在一起容易互相干扰。分别用两个端口段一眼就能看出当前在哪个环境里。复制集实验mkdir -p /data/mongo/rs0_1 /data/mongo/rs0_2 /data/mongo/rs0_3 mkdir -p /data/mongo/log # 端口27017 / 27018 / 27019分片实验mkdir -p /data/mongo/cfg1 /data/mongo/cfg2 /data/mongo/cfg3 mkdir -p /data/mongo/sh1 /data/mongo/sh2 mkdir -p /data/mongo/log # 端口config 30001-30003shard 40001-40002mongos 27017分片实验里 config server 我只开 3 个必须奇数理由和复制集一样shard 只开 2 个。为什么 shard 只开 2 个因为分片的核心价值在于数据被切开、均衡器能搬块2 个分片已经足够观察到 chunk 分布不均、迁移发生这些现象了3 个分片在单机上纯粹是多吃内存。等你真正理解了迁移逻辑再扩展到 4 个分片验证多对多迁移也不迟。提示单机跑多个实例时务必给每个实例单独配置cacheSizeGB。WiredTiger 默认会抢占约一半可用内存三个实例同机就会出现互相驱逐缓存、查询突然变慢的诡异现象很多人误以为是索引没生效其实是被内存挤的。2.2 配置文件里真正要关心的字段实验环境我倾向用 YAML 配置文件而不是一长串命令行参数好处是可复现、能进版本管理、改一个字段就好。以复制集第一个节点为例storage: dbPath: /data/mongo/rs0_1 wiredTiger: engineConfig: cacheSizeGB: 0.5 systemLog: destination: file path: /data/mongo/log/rs0_1.log logAppend: true net: port: 27017 bindIp: 127.0.0.1 replication: replSetName: rs0 processManagement: fork: true逐字段说一下取舍理由cacheSizeGB: 0.5是硬性要求。默认值是(总内存 - 1GB) × 50%一台 8GB 的实验机单个实例就想吃 3.5GB三个实例直接爆掉。0.5GB 足够跑几十万条文档的实验数据而且外围系统还得留内存。bindIp: 127.0.0.1在纯本机实验里最安全但如果是在容器里跑、或者想让宿主机访问就得改成0.0.0.0。改完之后要记得成员配置里的 host 地址必须和实际可连通地址一致这是后面一大类报错的根源。replSetName: rs0必须三个节点完全一致一个字都不能差。如果不小心把某个节点写成rs1它在rs.status()里会显示成(not reachable/healthy)或者干脆不出现排查起来很费时间。processManagement.fork: true让进程后台运行。在部分容器环境里 fork 会失败报parent process has exited这时候去掉这一项改用nohup或者--fork --logpath方式启动。分片实验的 config server 配置要多一个角色声明sharding: clusterRole: configsvr replication: replSetName: cfgRS storage: dbPath: /data/mongo/cfg1shard 节点则是sharding: clusterRole: shardsvr replication: replSetName: shardRS1clusterRole这个字段是从 3.6 开始强制的如果漏写启动时可能不报错但后面sh.addShard一定失败报is not running with --shardsvr。mongos 不进 dbPath它的配置全在命令行或配置文件里sharding: configDB: cfgRS/127.0.0.1:30001,127.0.0.1:30002,127.0.0.1:30003 net: port: 27017注意configDB里的副本集名字cfgRS必须和 config server 实际的replSetName完全一致这是另一个高频翻车点。2.3 启动顺序与第一次握手验证分片集群的启动顺序是有讲究的先 config server再 shard最后 mongos。原因很直白mongos 启动时要连上 config server 拿元数据config 没起来它直接退出shard 加入集群时需要 config server 记录元数据。顺序错了就是一连串连接超时。复制集的启动顺序则随意三个 mongod 先起来再挑其中一个执行rs.initiate()。启动完先别急着初始化做个基础连通性检查mongosh --port 27017 --quiet --eval db.runCommand({ping:1})如果这一步就失败八成是进程没起来、端口被占、或者 bindIp 限制。先看日志tail -50 /data/mongo/log/rs0_1.log日志里出现Waiting for connections才算真正就绪。我吃过一次亏--fork返回了成功但进程立刻因为权限问题退出了脚本继续往下跑后面所有命令全在报连接拒绝浪费了二十分钟。3. 复制集实操从 rs.initiate 到断电演练准备工作就绪之后复制集的搭建命令其实只有一行但这一行里的每个字段都值得掰开看因为它们决定了后面出故障时谁顶上、谁不能顶上。3.1 初始化命令逐字段拆解rs.initiate({ _id: rs0, members: [ { _id: 0, host: 127.0.0.1:27017, priority: 2 }, { _id: 1, host: 127.0.0.1:27018, priority: 1 }, { _id: 2, host: 127.0.0.1:27019, priority: 1 } ] })_id就是副本集的名字和配置文件里的replSetName必须一致。members里每个成员的_id是内部标识从 0 开始不重复一旦设定就不能直接改要改只能rs.reconfig()整体重配。host字段是最容易踩坑的。它不是这个节点监听的地址而是其他节点和客户端应该用什么地址找到它。所以如果三个节点跑在同一台机器上用127.0.0.1:端口没问题一旦跨机器或者跨容器写127.0.0.1就完蛋——其他成员永远连不上它因为它会把自己介绍成127.0.0.1别人照着这个地址去连连的是自己。priority决定选举优先级范围 0-1000默认 1。数字越大越优先成为主节点。设成 2 的那个节点在正常情况下会稳定当选这样做的意义是让主节点固定下来减少不必要的切换。比如你有一个机房是主用、另一个是灾备那就把主用机房节点的 priority 设高避免网络抖动导致主节点来回跳。priority: 0的节点永远不会成为主适合放只读分析节点或者延迟节点。初始化之后不要立刻开始写数据等状态收敛while (!db.hello().isWritablePrimary) { sleep(1000) } rs.status().members.forEach(m print(m.name, m.stateStr, m.health))正常情况下应该看到 1 个PRIMARY、2 个SECONDARY。如果某个成员一直停在STARTUP2说明它正在做初始同步initial sync——从主节点拉全量数据再追 oplog。数据量小的时候几秒钟数据量大可能几十分钟这时候别急着 kill让它跑完。STARTUP2还有一种可能是永远卡住判断依据是看日志里有没有initial sync的进度输出。如果没有进度、optimeDate也不动通常是网络不通或者磁盘空间不足。3.2 Secondary 怎么读、怎么确认同步延迟默认情况下从节点是不可读的直接查会报not primary and secondaryOkfalse。要读从节点得显式声明读偏好// 方式一当前连接全局设置 db.getMongo().setReadPref(secondaryPreferred) // 方式二单次查询指定 db.orders.find().readPref(secondary)secondaryPreferred的意思是优先读从节点从节点都不可用才读主节点这是报表、统计这类场景最常用的secondary则是只读从节点读不到就报错适合对主节点压力极度敏感的场景primaryPreferred相反正常读主、主挂了读从但读的是可能过期的数据。读从节点之前一定要想清楚一件事从节点的数据可能比主节点旧。旧多少看同步延迟rs.printSecondaryReplicationInfo()输出里会显示每个从节点落后主节点多少秒。这个值在正常负载下应该是个位数秒如果持续几十秒甚至几分钟说明从节点跟不上常见原因有三种主节点写入量太大、从节点磁盘 IO 太慢、或者网络带宽被打满。这时候可以考虑调大oplogSizeMB、给从节点加secondaryIndexPrefetch相关配置老版本或者干脆升级从节点的磁盘。oplog 的大小也值得单独说一句。默认值在 WiredTiger 引擎下是可用磁盘的 5%且不小于 990MB。这个默认值对实验环境磁盘小是够的但对写入量大的生产环境5% 可能只够撑十几分钟——一旦从节点断线时间超过 oplog 覆盖范围它就再也追不上了只能做全量重同步。经验公式是oplog 至少要能覆盖最长一次计划外停机 一次全量备份的时间。比如你预估最坏情况从节点掉线 4 小时峰值写入 1000 条/秒每条 oplog 条目平均 500 字节那需要4 × 3600 × 1000 × 500 ≈ 7.2GB多留点余量配 10GB 比较稳。3.3 主节点挂掉之后到底发生了什么这一步是复制集实验里最值得做的因为它把前面所有抽象的选举变成可观察的现象。先手动降级rs.stepDown(60)60是冷却时间表示这个节点在 60 秒内不会再参与竞选。执行后立刻rs.status()你会看到原主节点变成SECONDARY某个从节点在 10 秒内变成PRIMARY。这 10 秒从哪来的是electionTimeoutMillis的默认值。选举的完整过程大致是主节点每 2 秒heartbeatIntervalMillis给各成员发心跳从节点如果连续一个心跳周期没收到就把主节点标记为不可达等超过electionTimeoutMillis之后有资格当选的成员发起投票请求拿到多数派同意就成为新主。所以故障切换的实际耗时大约是 10-12 秒这期间客户端会看到连接错误或写超时。这就引出一个非常实际的工程问题你的应用怎么应对这 10 秒如果驱动开了retryWrites: true4.2 之后默认开启驱动会自动重试一次可重试的写操作大多数情况下用户无感知如果没开就得在业务层做重试和幂等设计。我在实验里故意在写入循环中 kill 主节点观察驱动的表现效果比看文档直观得多。还有一种是进程直接挂掉用kill -9模拟。这里有个细节如果被 kill 的是主节点且它正在写入重启后可能会出现 rollback回滚——它本地多写了一些还没同步到多数节点的数据重新加入集群时这些数据会被丢弃写入dbPath/rollback/目录下的 BSON 文件。这个目录值得看一眼能直观理解为什么 w:1 不安全。3.4 写关注与读关注把丢数据的窗口关掉这是复制集里最容易看起来没问题、实际有隐患的地方。db.orders.insertOne( { orderId: A001, amount: 99 }, { writeConcern: { w: majority, j: true, wtimeout: 5000 } } )w的含义是确认多少个节点写入后才返回w 值含义数据安全写入延迟适用场景1主节点写入即返回主节点挂可能丢最低日志、埋点等可丢数据majority多数节点写入才返回不会丢已确认数据中等绝大多数业务数字 N指定节点数取决于 N高特殊合规要求majority j:true多数节点落盘最高最高资金、订单等核心数据j: true表示写进 journal 日志才返回防止写在内存里但机器断电的情况。开了j:true之后单次写入延迟会上升因为涉及强制刷盘实测在普通 SSD 上每次多 1-3ms。如果你的业务是高频小写入别不加区分地全开。wtimeout是超时时间超过就报错而不是无限等。不建议设成 0无限等待那样当副本集挂到只剩一个节点时所有写请求会永久挂起应用线程池被占满比直接报错惨得多。读关注readConcern是另一半local默认读本节点最新数据主节点切换时可能读到后来被回滚的数据。majority只返回已被多数节点确认的数据代价是略微增加延迟。linearizable强一致读只能读主节点而且会等待所有已确认的写完成最慢。snapshot事务内使用保证事务内读到一致快照。我的建议是普通查询用 local 就够了涉及读完立刻据此做决策的场景比如余额判断、库存扣减用 majority。这两个参数搭配得当复制集的可靠性才算真正落到实处否则只是搭了个样子。4. 分片集群实操config server、shard、mongos 三件套分片这部分命令本身比复制集多不了几行但需要理解的东西多一个数量级尤其是分片键的选择几乎决定了整个集群的性能上限。4.1 三层的职责与启动顺序config server 三个实例先起然后在其中一个上初始化副本集rs.initiate({ _id: cfgRS, configsvr: true, members: [ { _id: 0, host: 127.0.0.1:30001 }, { _id: 1, host: 127.0.0.1:30002 }, { _id: 2, host: 127.0.0.1:30003 } ] })注意configsvr: true这个字段它让这个副本集以配置服务器模式运行会创建config数据库。如果漏了它后面 mongos 连上来会直接拒绝。shard 同理每个 shard 是一个副本集。实验环境我用单节点副本集只有一个成员生产必须三个。单节点的写法rs.initiate({ _id: shardRS1, members: [{ _id: 0, host: 127.0.0.1:40001 }] })然后启动 mongos连上 config 集群mongos -f /data/mongo/mongos.confmongos 起来之后连上去把 shard 加进来sh.addShard(shardRS1/127.0.0.1:40001) sh.addShard(shardRS2/127.0.0.1:40002) sh.status()sh.status()的输出值得反复看它会列出所有 shard、config server 版本、数据库是否开启分片、每个集合的 chunk 分布。刚加完 shard 的时候databases里应该是空的因为还没有任何库开启分片。开启分片是两步sh.enableSharding(shop) // 库级别开关 sh.shardCollection(shop.orders, { userId: hashed }) // 集合级别指定分片键这两步缺一不可。只做第一步集合仍然只存在于主分片上只做第二步会报sharding not enabled for db。4.2 分片键怎么选三个硬指标加一个算例选分片键是分片设计里唯一真正需要动脑子的地方。行业里通用的三条判断标准第一基数要高。基数就是字段有多少个不同取值。status字段只有 5 个值gender只有 2 个这类字段做分片键最多只能切出 5 个、2 个 chunk而 chunk 是不能跨分片拆的结果就是所有数据集中在少数几个块上分片形同虚设。第二频率要低。指某个取值对应的文档不能太多。假设你按tenantId分片但 90% 的数据都属于同一个大客户那这个客户的 chunk 会撑爆其他分片空着这就是典型的数据倾斜。第三不能单调递增。时间戳、自增 ID、ObjectId 这类字段新数据永远比旧数据大按范围分片的话所有新写入都会落到最后一个 chunk也就是同一台机器上其他分片只承接历史查询。这个现象叫写热点是新手最常犯的错。举个具体的算例。假设订单表预估 2 亿条文档平均每条 1KB总数据量约 200GB。chunk 默认 64MB那理想情况下会有200GB / 64MB ≈ 3200个 chunk。如果有 4 个分片平均每个分片承担 800 个 chunk每片约 50GB这个分布就是健康的。现在换成按status分片只有 5 个取值最多切出 5 个 chunk每个 chunk 分到 40GB——注意chunk 超过 64MB 之后会被标记为可拆分但因为分片键取值只有 5 种它根本无法再拆最终变成 5 个巨型 chunkjumbo chunk全部堆在少数分片上。再换成按{ userId: hashed }分片用户基数是几十万哈希之后分布均匀3200 个 chunk 能均匀铺开每个分片都吃得住。但哈希分片有个代价范围查询会退化成广播。因为哈希之后userId的相邻值不再相邻存储查userId 1000 且 2000这种条件mongos 没法定位到具体分片只能发给所有分片再合并scatter-gather。如果你的查询主要是按用户查订单这种等值查询哈希没问题如果大量是按时间段查订单就得上范围分片。最实用的折中是复合分片键sh.shardCollection(shop.orders, { tenantId: 1, createdAt: 1 })这样租户内按时间有序满足范围查询跨租户又分散避免单租户热点。这也是很多多租户 SaaS 系统的标准做法。注意分片键一旦确定在 5.0 之前是完全不可更改的只能删库重建。5.0 之后提供了reshardCollection可以在线换键但代价很大——它会全量复制数据。所以选键的时候多花半小时想清楚比后面花两天迁移划算得多。还有一个索引约束容易忘分片键字段必须在集合的每个文档里都存在而且唯一索引的前缀必须包含分片键。如果你在分片集合上建{ orderId: 1 }, { unique: true }会直接报错正确的做法是建{ orderId: 1, userId: 1 }, { unique: true }或者干脆用_id作为唯一约束。4.3 哈希分片与范围分片的性能差异对比项范围分片ranged哈希分片hashed写入分布单调递增键会导致热点分布均匀等值查询路由到单分片路由到单分片范围查询路由到相关分片效率高广播到所有分片开销大排序可利用分片内有序性需要全分片归并适合场景时间序列、日志、按区间查询用户维度、设备维度、写多读散一个容易忽略的点哈希分片下{字段: hashed}生成的哈希值是 64 位整数所以按哈希键排序是没意义的。如果业务里有按 userId 排序分页的需求哈希分片会很难受。4.4 观察 chunk 分布与均衡器分片跑起来之后最该养成的习惯是定期看分布sh.status() db.orders.getShardDistribution()getShardDistribution()的输出会告诉你每个分片上有多少 chunk、多少文档、多少数据量、占比多少。理想情况是各分片占比接近误差 10% 以内。如果出现 90% / 10% 这种先别急着怀疑均衡器坏了多半是分片键选得有问题。关于均衡器有几个常用命令sh.getBalancerState() // 均衡器是否启用 sh.isBalancerRunning() // 当前是否正在迁移 sh.setBalancerState(false) // 临时关闭大批量导入前建议关均衡器迁移 chunk 是有条件的两个分片的 chunk 数量差超过阈值默认 2才会触发迁移而且迁移本身有开销拷贝数据 更新元数据 删除源数据。所以在大批量导入数据之前很多人会临时关闭均衡器导入完再打开避免迁移拖慢导入速度。还要认识一个概念jumbo chunk。当某个 chunk 里的文档因为分片键取值过于集中而无法拆分时它会被标记为 jumbo均衡器不会迁移它。sh.status()里看到 jumbo 标记基本就是分片键选错了的信号。解决方式只有两个拆数据重建或者 5.0 之后用reshardCollection换键。5. 排错实录那些让人抓头的报错实验环境里我踩过的坑大概能分成复制集和分片两大类。这里按报错现象整理方便对照排查。5.1 复制集相关的典型坑报错not primary and secondaryOkfalse看着像故障其实不是。你连的是从节点而当前连接没有声明读偏好。解决db.getMongo().setReadPref(secondaryPreferred)或者显式.readPref(secondary)。这个报错在实验里出现的频率高到应该写进肌肉记忆。报错replSetInitiate quorum check failed because not enough voting nodes responded初始化时成员列表里有节点连不上。排查顺序先nc -zv 127.0.0.1 27018确认端口通再看该节点日志有没有启动成功最后检查bindIp和防火墙。还有一种隐蔽情况是 host 写成了机器名但本机 hosts 没配也不通。现象某个成员一直停在STARTUP2两种可能。一是正在做初始同步看日志有initial sync进度就是在干活等着二是卡死了日志没有进度、optimeDate不动超过几分钟通常是磁盘满、oplog 太小导致从节点追不上、或者网络问题。现象重启之后数据少了一部分大概率是发生了 rollback。去dbPath/rollback/目录看看有没有 BSON 文件如果有说明那个节点之前以w:1写入了一些没同步到多数节点的数据重新加入集群时被丢弃了。这不是 bug是复制集的正确行为只不过说明了w:1的风险。现象副本集频繁切换主节点先看时钟。副本集对时间同步极其敏感如果节点间时钟偏差过大选举会反复触发。实验环境里如果用了虚拟机快照恢复时钟可能跳变务必检查。其次是网络抖动heartbeatIntervalMillis默认 2 秒如果网络延迟本身就超过 1 秒心跳会频繁超时。5.2 分片集群相关的典型坑报错could not contact config server或 mongos 启动即退出mongos 的configDB配置里副本集名字写错或者 config server 还没起。先确认mongosh --port 30001能连上并且rs.status()显示 PRIMARY。报错Cannot add shard because configdb is not running with clusterRole configsvrconfig server 启动时漏了sharding.clusterRole: configsvr或者初始化时漏了configsvr: true。只能重来——把 config 的 dbPath 清掉重新初始化因为已经写进去的角色信息改不了。报错sharding not enabled for db忘了sh.enableSharding(库名)。这个命令是幂等的重复执行没影响。报错cant shard collection because keys dont match集合上已经有索引了但和你要建的分片键不匹配。MongoDB 要求分片键必须有对应索引如果已有索引冲突会拒绝。解决是先删掉旧索引或者临时用一个中间集合做迁移。现象chunk 迁移一直卡住不动sh.isBalancerRunning()返回 true 但getShardDistribution()几十分钟没变化。常见原因目标分片磁盘空间不足迁移需要额外空间、网络带宽被打满、或者有 jumbo chunk 挡住。先看sh.status()里有没有 jumbo 标记再看目标分片磁盘。现象写入全部集中到一个分片分片键是单调递增字段时间戳、ObjectId导致写热点。这个没法通过配置解决只能改分片键。如果版本允许用reshardCollection换成哈希键或复合键。5.3 常见问题速查表现象最可能原因快速验证处理方式not primary and secondaryOk连到从节点且未设读偏好db.hello().secondary设置setReadPref成员停在 STARTUP2初始同步中 / oplog 太小看日志有无进度、optimeDate是否更新等待 / 重建数据莫名减少发生 rollback查dbPath/rollback/检查 writeConcern频繁切换主节点时钟偏差 / 网络抖动rs.status()看 optime 与心跳校时、检查网络mongos 启动失败configDB 名字错 / config 没起直连 config 端口核对 replSetNamesharding not enabled漏了 enableShardingsh.status()看库状态补执行各分片数据严重不均分片键基数低或频率高getShardDistribution()换分片键写入集中单分片单调递增分片键sh.status()看 chunk 分布改哈希或复合键迁移卡住磁盘满 / jumbo chunk查磁盘、查 jumbo 标记扩容 / 调整键范围查询变慢哈希分片导致广播explain()看是否 scatter-gather改范围或复合键6. 做完这两组实验之后我留下的几点体会实验跑通和真正理解之间隔着一段距离我是在踩了足够多的坑之后才慢慢感觉到这套东西的设计逻辑。关于复制集最值得反复做的是故障演练而不是搭建。搭建是一次性的故障演练能让你看到stepDown之后那 10 秒里客户端到底报什么错、驱动重试有没有生效、w:majority到底把多少写入挡在了门外。我建议至少做三种手动stepDown、kill -9主节点、以及停掉两个节点让集群失去多数派。第三种情况特别有意思——此时集群会降级为只读没有主节点就不接受写入很多人第一次遇到会以为数据库坏了。关于分片我的体会是先想清楚查询模式再选分片键。顺序反过来一定会返工。一个pratical的做法是把业务最常见的 5 条查询列出来看它们是不是都带同一个字段的等值条件如果是那个字段就是分片键的强候选如果一半是等值、一半是范围就考虑复合键。另外别迷信哈希一定均匀哈希均匀的是分布不是查询效率范围查询在哈希分片下会明显变慢这个代价在小数据量时看不出来数据一多就很明显。还有一个细节分片集群里的每个 shard 都应该是副本集哪怕实验环境里只用单节点副本集也千万别用裸 mongod 直接当 shard。一旦那个 shard 挂掉整个集群的数据就少了一块查询会直接报错而副本集至少还能切主继续服务。这个习惯从实验环境就该养成。最后分享一个观察技巧。想直观看到 chunk 是怎么切的可以先把chunkSize调小use config db.settings.updateOne( { _id: chunksize }, { $set: { value: 1 } }, { upsert: true } )把 chunk 从 64MB 改成 1MB然后插入几万条数据sh.status()里立刻就能看到密密麻麻的 chunk 分布和均衡器搬块的痕迹。等观察完再调回 64MB——注意这个参数改了之后影响的整个集群实验环境随便玩别在生产上这么干。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑