写MongoDB分片集群的文章,大部分人都把目光放在分片、chunk、balancer这些“重机制”上,反而忽略了那个每天都在替所有请求跑腿的核心组件——mongos。mongos很简单,它是一个无状态的路由进程,客户端连它就像连一个普通的mongod,但背后它得把每个读写请求拆解、转发到正确的分片,再把结果合并回来;同时它还要和config server保持通信,缓存整个集群的路由元数据。很多人部署完分片集群就把它扔在一边,直到出现“所有业务都变慢了”或者“连不上数据库”才发现,其实绝大多数分片集群故障,都和mongos的配置、连接数、路由缓存有关系。这篇文章我就把mongos组件彻底聊透,从架构定位、部署方式、进程模型到调优排障,尽量用实际运维中会遇到的口吻讲清楚,适合刚上手分片集群的DBA,也适合被mongos问题折磨过的后端开发。
1. mongos在分片集群里到底扮演什么角色
1.1 一次查询请求是怎么走通的
先讲一个最基础的场景。假设你的业务库已经做了分片,集合按某个分片键拆到了两个shard上。客户端发起一个db.users.find({user_id: 123}),这个请求首先打到的不是任何一个shard,而是mongos。mongos拿到请求后,会根据自身的路由缓存判断user_id: 123这个数据落在哪个chunk、哪个shard上,然后把查询转发给对应的shard,拿到结果后返回给客户端。
整个过程客户端无感知,它甚至不知道集群后面有几台机器。这就是mongos存在的意义:把“集群”伪装成一个“单机”给业务用。如果没这个“翻译官”,客户端每次都得自己判断数据在哪台机器,那分片就完全没有可用性了。你可以把mongos想象成公司前台的接待员——访客(客户端)不需要知道每个部门在几楼几号工位,只要告诉接待员要找谁,接待员就能带到正确的地方。
从这个例子能看出,mongos本身不存数据,它只负责“带路”。所以正常设计下,mongos可以是一个很轻量的进程,CPU和内存要求都不高,集群真正吃资源的是各shard节点。
1.2 mongos、config server、shard三者的分工
很多初学者把分片集群的三个角色搞混,我直接给一张对照表:
| 组件 | 主要职责 | 存不存业务数据 | 可不可以多实例 | 挂了会怎样 |
|---|---|---|---|---|
| mongos | 路由转发、合并查询结果 | 不存 | 可以,且推荐至少2个 | 业务无法访问集群,但数据不受影响 |
| config server | 保存集群元数据(分片信息、chunk分布等) | 只存元数据 | 必须以副本集方式运行 | 集群路由信息失联,新路由变化无法同步 |
| shard | 真正存储业务数据 | 存 | 必须是副本集 | 所在分片数据不可用,可用性由副本集保证 |
这里有一个常见的误解:很多人以为balancer这个“搬数据”的活是mongos干的。实际不是,balancer跑在config server的主节点上,mongos只是被动接收元数据变化。所以如果chunk分布不均,你去找mongos调参数是没用的,得从config server侧观察balancer状态。我见过有团队把均衡任务挂在mongos上定时重启,属于完全搞错了对象。
1.3 为什么mongos可以随便多开
mongos最大的亮点是无状态、可以随意水平扩展。因为在它内部,最重要的东西只有一份从config server同步过来的元数据缓存,这份缓存丢了也无所谓,重启后重新拉一遍就行。也正因无状态,你可以在每个业务接入层旁边都放一个mongos,让业务就近连接;也可以在前端用LVS、Nginx做一层负载均衡,让一堆mongos对外只暴露一个虚拟IP。
实际项目里我建议mongos的数量不要少于2个,最好按“接入层机器数+N”的方式规划。比如你有3台应用服务器,就放3个mongos,每台应用连本机的mongos,既省了网络跳数,又天然做了容灾。mongos不存数据,哪怕整台机器宕机,换台新机器重新拉起来,几十秒就能恢复服务。
2. 手把手部署一个mongos进程
2.1 部署前需要想清楚的两件事
第一个是网络连通性。mongos要和config server、所有shard建立连接,所以部署mongos的机器最少要能访问到config server的27019端口和各shard的27017端口。很多人启动mongos报错,查了半天发现是防火墙只放通了客户端到mongos的端口,mongos到后端网络不通,自然起不来。
第二个是配置来源。mongos自身的配置文件里有几项必须在启动前想好:监听端口、绑定的IP、config server地址、日志路径。尤其注意bindIp,默认情况下MongoDB只绑定127.0.0.1,如果部署在独立机器上,不显式改成内网IP或0.0.0.0,业务服务器根本连不上。
2.2 mongos启动命令和配置文件写法
mongos虽然也是通过mongos这个二进制启动的,但它的配置方式和mongod不太一样。最核心的一条配置是sharding.configDB,必须写成config server副本集名称/节点1地址,节点2地址,节点3地址的格式。下面是一份我常用的mongos配置文件:
# /etc/mongos.conf systemLog: destination: file path: /var/log/mongodb/mongos.log logAppend: true processManagement: fork: true net: bindIp: 0.0.0.0 port: 27017 sharding: configDB: cfg/10.0.1.11:27019,10.0.1.12:27019,10.0.1.13:27019配置写好之后,用同一套二进制启动:
mongos -f /etc/mongos.conf启动后立刻看一眼日志,正常情况下会出现一行类似Waiting for connections的日志。然后随手验证一下路由状态:
mongosh --host 127.0.0.1 --port 27017 sh.status()sh.status()能正常输出分片集群的拓扑信息,说明mongos已经成功从config server拉到了元数据,这一套就算跑通了。
这里我提一个实操细节:先有config server,再启动mongos。如果config server还没初始化成副本集就启动mongos,大概率会在日志里看到Failed to connect to config server,此时mongos进程会直接退出,甚至不会留在后台。所以部署顺序一定是:shard副本集 → config server副本集 → mongos。
2.3 应用层的连接串该怎么配
mongos部署好只是第一步,更关键的是业务侧怎么连。很多人把连接串写成一个mongos节点,然后问“mongos挂了怎么办”。答案很简单:连接串里至少写两个mongos地址。
mongodb://user:pass@10.0.1.101:27017,10.0.1.102:27017/?connectTimeoutMS=3000驱动会按地址列表轮询或者选最近节点连接,单个mongos宕机后,应用侧会自动转移到另一个可用mongos。注意这个连接串里不需要配replicaSet参数,mongos对外表现出来的“像一个单机”,和副本集那套选举机制不是一回事。配进replicaSet反而可能让驱动产生误解。
还有个小建议:如果客户端服务是多实例部署的,连接串里尽量带上一两个“备用mongos”,哪怕当前只有一台在服务,也把另一台写进地址列表。这样后续加mongos的时候,应用不需要改配置就能自动感知。
2.4 用systemd管理mongos进程
生产环境不建议裸启动mongos进程,最好交给systemd托管。这样mongos异常退出会自动拉起,开机也能自启动。写一个service文件:
[Unit] Description=MongoDB Mongos Router After=network.target [Service] Type=forking ExecStart=/usr/local/mongodb/bin/mongos -f /etc/mongos.conf ExecReload=/bin/kill -SIGUSR1 $MAINPID Restart=always RestartSec=3 LimitNOFILE=64000 [Install] WantedBy=multi-user.target保存到/etc/systemd/system/mongos.service,然后:
systemctl daemon-reload systemctl enable mongos systemctl start mongos这里顺手说一句“进程”层面的经验:MongoDB的mongos默认开了processManagement.fork: true后会自己daemon化,由init进程接管,这种方式在容器里其实容易出问题,因为容器退出时mongos会变成孤儿进程,经常出现“明明容器停了,进程还在”的诡异情况。所以容器化部署mongos时,务必把fork关掉,让mongos以前台进程方式跑,由容器编排系统管理生命周期。
3. mongos的关键机制:路由、缓存与进程模型
3.1 数据定位:mongos怎么知道该找哪个分片
mongos的核心机制是元数据缓存。它启动后第一件事就是从config server拉取完整的集群元数据,包括:有哪些分片、每个集合的分片键是什么、chunk按什么范围分布、每个chunk现在归哪个shard管。这些信息会被缓存到内存里,之后的请求直接查缓存定位,不需要每次访问config server。
缓存不是永久不变。当发生chunk分裂、迁移或者新的分片加入时,config server会把变更推给mongos,mongos更新自己的缓存。偶尔也会出现网络抖动导致缓存放旧的情况,这时候mongos执行查询会报StaleConfig错误。别慌,这是MongoDB设计的正常信号,驱动会自动重试一次并重新拉取元数据,大多数场景对业务无感。
我在实际运维中提醒过自己很多次:mongos的缓存不是越新鲜越好,频繁刷新反而会带来抖动。如果config server频繁做chunk迁移,mongos会伴随大量元数据刷新,此时如果业务侧看到偶发超时,先检查config server在做什么,而不是盲目重启mongos。
3.2 定向查询和分散聚合查询
mongos的转发策略就两种:定向查询(targeted query)和分散聚合查询(scatter-gather)。
- 定向查询:查询条件中带了完整的分片键,mongos可以直接算出对应chunk,只转发给一个shard,效率最高。
- 分散聚合查询:查询条件没有分片键,mongos不知道数据在哪,只能把查询广播给所有分片,等所有分片返回后再合并结果。数据量一大,这种查询性能就是灾难级的。
举个例子,集合users以user_id作为分片键。
// 这个查询带了完整分片键,mongos只会访问一个分片 db.users.find({ user_id: 12345 }) // 这个查询没有分片键,mongos会广播到全部分片 db.users.find({ email: "test@example.com" })很多性能问题根源就是第二个查询太多了。排查的时候,在mongos上执行:
db.currentOp()如果发现大量正在执行的查询里有shards字段列出了全部分片,那就说明业务侧的查询模式不符合分片键设计。更直观的办法是在mongos里执行explain("queryPlanner"),结果中的winningPlan如果带有SHARD_MERGE或者SHARD_MULTI字样,基本就是scatter-gather没跑了。
3.3 mongos的进程与线程模型
从操作系统角度看,mongos就是一个普通的用户态进程,和mongod类似,内部用epoll事件驱动配合线程池处理网络连接。但它比mongod简单得多:不负责存储引擎,不需要管理WiredTiger缓存,也不需要跑副本集心跳选举之类的逻辑。所以mongos进程的“身形”很轻,通常看它占的内存也就一两百MB,CPU主要用于解析BSON、执行路由计算和请求转发。
这里要引入一个很多人混淆的概念:线程和进程的区别。mongos作为一个进程,内部会创建若干线程来处理并发请求;线程之间通过内部分配器协调任务,这就是类似“进程池”的做法。mongos不会为每个客户端请求创建一个线程,而是复用一组工作线程,减少线程创建销毁的开销。客户端和服务端之间、mongos和mongod之间的通信,本质上都是进程间的网络通信,不是共享内存那种IPC。
运维上有一点要特别注意:mongos的文件描述符和连接数限制。因为mongos要同时维持两类连接——客户端到mongos的连接和mongos到后端mongod的连接。一个业务请求打进来,mongos内部可能要和后端建立多条连接来把操作分发到不同分片。如果系统ulimit -n设得很低,mongos连接数一涨就报too many open files。生产环境一般建议至少设到65535,可以用ulimit -n 64000或者systemd里的LimitNOFILE来调整。
3.4 mongos挂了,数据会丢吗
这个问题我几乎每次培训都会被问到。答案是不会丢数据,但服务会中断。mongos不存数据,它只是路由入口,挂掉之后业务连不上集群,但这相当于前台的接待员临时不在,后面的货物仓库一点没动。重启mongos后,它会重新从config server同步元数据,几十秒到几分钟内恢复正常。
但这里有个容易被忽略的点:当多个业务系统共用同一个mongos时,一个业务写崩了mongos,所有人一起遭殃。最常见的“写崩”其实是慢查询或者超大结果集把mongos连接池吃满,导致其他请求全部排队。所以稍微大一点的团队,我会倾向于让不同核心业务组用不同mongos,甚至不同端口,做故障隔离。
4. 性能调优和监控实践
4.1 先分清瓶颈在mongos,还是不在mongos
调优的第一步是定位。mongos这个角色的特性决定了它很少是性能瓶颈,大多数“mongos慢”其实是后端慢。做一次判断可以从三个方向入手:
第一,看mongos的CPU和内存。如果mongos本身CPU不高、内存稳定,但业务还是慢,那瓶颈大概率在shard侧的磁盘、索引或锁上。第二,看mongos的连接数。客户端大量建连但请求量不大,往往是驱动配置问题,比如连接池开得太大。第三,看mongos日志里的慢查询。mongos默认慢查询日志阈值是100ms,日志里如果大量记录慢查询,但每个慢查询都卡在等待后端返回,就要去shard上排查了。
一个实用判断方法:在mongos上执行db.currentOp(),看操作列表里大多数操作的状态是awaiting replication、waiting for lock还是reading from cursor。如果是前两种,问题基本都在后端shard;如果是sending data且长时间不结束,可能是大结果集正在传输,这时候要检查业务是否一次性拉取过多数据。
4.2 mongos常用调优参数
mongos的调优不像mongod那么多,核心参数也就那么几个。我按优先级排个序:
客户端的maxPoolSize:Java、Node等驱动默认连接池上限通常是100。如果业务并发高,100个连接可能不够用,可以适当调大,但不要盲目调成几千。每个客户端连接都对应mongos内部的一部分资源,连接数太多反而降低整体吞吐。建议压测时从100开始,逐步加,直到吞吐不再上升为止。
socketTimeoutMS / connectTimeoutMS:连接超时设为2到3秒比较合适,能快速暴露网络问题;socket超时取决于业务最长可接受等待时间,一般30秒起步。
读偏好(readPreference):mongos支持把读操作转发到分片的从节点上,以减轻主节点压力。如果业务能接受稍旧的数据,配置
readPreference=primaryPreferred或者secondaryPreferred会有明显收益。但注意事务中的读写都必须走主节点,如果业务开启了事务,读偏好不会对事务内的操作生效。批量大小和游标:业务侧做全量导出时,不要一次
find()出全部数据后循环处理,应该用batchSize控制每次返回的文档数量,或者用游标分批拉取。否则mongos要缓存大量结果集,内存占用会肉眼可见地涨。
另外,mongos本身也支持一部分setParameter,比如maxTimeMS兜底、限制单个查询的执行上限,避免个别慢SQL拖垮整个入口。但这类参数要小步灰度试,别一上来就改狠了,容易误伤正当业务。
4.3 监控mongos看什么
监控mongos不复杂,但要看对指标。我列一张速查表:
| 指标项 | 获取方式 | 关注点 |
|---|---|---|
| 连接数 | db.serverStatus().connections | 持续接近上限要扩容或排查客户端连接池 |
| 活跃操作数 | db.currentOp() | 大量写等待或查询堆积说明后端或索引有问题 |
| 慢查询 | mongos日志 | 记录超过slowms的操作,定位需要优化的查询 |
| 元数据刷新 | 日志中refreshing metadata | 过于频繁说明config server在做大量chunk迁移 |
| 命令耗时 | mongostat | 观察query、insert、update等操作的速率和耗时 |
mongostat是一个很好用的命令行工具,直接指向mongos端口就行:
mongostat --host 10.0.1.101 --port 27017 --discover--discover参数会从mongos自动发现集群里的所有分片,这样你可以在一个终端看到全集群的QPS和延迟分布,非常直观。日常巡检我基本就是开一个mongostat挂在旁边,再配合mongos日志里的慢查询告警,基本能覆盖80%的问题场景。
5. 常见故障排查与避坑记录
5.1 故障速查表
我在多个环境里跑过MongoDB分片集群,mongos相关的故障大同小异,先给一张速查表,后面再展开讲几个印象深刻的现场。
| 现象 | 可能原因 | 排查命令/日志关键字 | 解决办法 |
|---|---|---|---|
| mongos启动后立刻退出 | config server连不上 | Failed to connect to config server | 检查config DB地址、网络、config server是否已初始化 |
业务报Could not find host matching read preference | mongos路由缓存或分片状态异常 | no host found | 检查分片是否处于正常状态,重启mongos刷新元数据 |
大量StaleConfig错误 | chunk迁移导致元数据更新 | StaleConfig | 驱动自动重试,若持续出现则检查balancer是否频繁触发 |
操作报cannot open connection to shard | mongos到shard网络或shard宕机 | cannot open connection | 检查shard存活和网络连通性 |
| mongos内存持续上涨 | 客户端大量游标未关闭/结果集过大 | 查看db.serverStatus().mem | 优化业务查询,控制批量大小,杀掉僵尸游标 |
| 所有请求都慢 | 后端shard慢,非mongos瓶颈 | mongostat观察shard侧指标 | 到shard侧排查慢查询和锁 |
5.2 几个印象深刻的翻车现场
第一个案例是configDB连接串写错导致的连环故障。有一次新集群上线,部署文档里把config server的副本集名写成了cfg1,但实际初始化时副本集名用的是configRS。mongos启动日志一直在报unable to connect,表面看像是网络问题,排查了好久才发现是名字不匹配。这种问题只要在启动前用rs.status()确认config server的副本集名,然后再写进configDB,就能完全避免。
第二个案例是mongos进程突然消失,systemd却没有把它拉起来。当时mongos是被运维用裸命令mongos -f手动启动的,进程崩了之后没人发现,客户端连接全部失败,业务告警炸了。这个问题直接推动了我在项目里全面改用systemd托管,并给mongos加了存活探针。因为mongos本身无状态,崩溃不可怕,可怕的是没有守护手段,让它“死得悄无声息”。
第三个案例更耐人寻味,是一个**“mongos慢但其实不慢”**的假象。当时业务反馈所有通过mongos的查询延迟都飙升到几百毫秒,但查看mongos的CPU、连接数、shard侧指标都正常。后来抓包发现,延迟主要花在客户端的DNS解析上——业务连接的mongos使用了域名,某台DNS临时抖动,导致每次新建连接都要卡一下。启用长连接池之后,问题就消失了。这提醒我一件事:mongos调优不能只盯着MongoDB组件的指标,客户端的网络解析、驱动配置同样能造成“伪mongos瓶颈”。
5.3 排查工具与日志使用技巧
mongos日志默认路径是启动配置里指定的,排查问题时优先看启动周期前后的报错。善用日志里的关键字,比如Fatal、Assertion、cannot find、refreshing metadata。
另外,mongos支持logLevel动态调整,通过mongosh执行:
db.adminCommand({ setParameter: 1, logLevel: 2 })可以把日志级别调高,临时拿到更详细的转发信息,排查完再调回0。注意不要长期保持高级别日志,生产环境下日志量会很吓人。
还有一个容易被忽略的点:mongos版本必须和config server、shard保持一致。MongoDB不承诺跨大版本混用,有一次我把mongos升级到6.0,但shard还在5.0,结果mongos日志里频繁出现Incompatible server version,客户端各种报错。所以升级集群时务必先升级shard和config server,最后再升级mongos,或者统一在同一个维护窗口全量升完。
6. 关于mongos的一些个人体会
说句实在话,mongos是分片集群里最容易被低估的组件。因为它不存数据、不跑选举、不做均衡,看起来像个“代理”,但恰恰是这个代理决定了整个集群对外表现的稳定度。我自己经历过几次大型故障之后,最深的体会是:一定要把mongos当作一等公民来运维,给它做systemd守护、做监控告警、限制文件描述符、规划好连接数,而不是把它当成“启动完就不用管”的透明层。
另外一个很重要的经验是:mongos的数量和位置要提前规划,不要等到业务增长后再硬塞。mongos无状态,扩容听起来很容易,但如果一开始只在机器上部署了一个mongos,后来所有应用都连它,那它就是整个集群前台唯一的接待员。一旦某个粗心同事写了个全表scan,mongos连接一满,全公司业务跟着遭殃。多部署几个mongos,并在应用连接串里都配上,这种成本极低的事情,收益却非常可观。
最后分享一个小技巧:每次mongos重启后,可以手动执行一遍sh.status()确认元数据拉取成功,再放业务流量进来,避免“进程起来了但路由没就绪”的短暂空窗。这个步骤写进发布脚本里,能少接很多半夜的告警电话。