
写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:pass10.0.1.101:27017,10.0.1.102:27017/?connectTimeoutMS3000驱动会按地址列表轮询或者选最近节点连接单个mongos宕机后应用侧会自动转移到另一个可用mongos。注意这个连接串里不需要配replicaSet参数mongos对外表现出来的“像一个单机”和副本集那套选举机制不是一回事。配进replicaSet反而可能让驱动产生误解。还有个小建议如果客户端服务是多实例部署的连接串里尽量带上一两个“备用mongos”哪怕当前只有一台在服务也把另一台写进地址列表。这样后续加mongos的时候应用不需要改配置就能自动感知。2.4 用systemd管理mongos进程生产环境不建议裸启动mongos进程最好交给systemd托管。这样mongos异常退出会自动拉起开机也能自启动。写一个service文件[Unit] DescriptionMongoDB Mongos Router Afternetwork.target [Service] Typeforking ExecStart/usr/local/mongodb/bin/mongos -f /etc/mongos.conf ExecReload/bin/kill -SIGUSR1 $MAINPID Restartalways RestartSec3 LimitNOFILE64000 [Install] WantedBymulti-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会把变更推给mongosmongos更新自己的缓存。偶尔也会出现网络抖动导致缓存放旧的情况这时候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: testexample.com })很多性能问题根源就是第二个查询太多了。排查的时候在mongos上执行db.currentOp()如果发现大量正在执行的查询里有shards字段列出了全部分片那就说明业务侧的查询模式不符合分片键设计。更直观的办法是在mongos里执行explain(queryPlanner)结果中的winningPlan如果带有SHARD_MERGE或者SHARD_MULTI字样基本就是scatter-gather没跑了。3.3 mongos的进程与线程模型从操作系统角度看mongos就是一个普通的用户态进程和mongod类似内部用epoll事件驱动配合线程池处理网络连接。但它比mongod简单得多不负责存储引擎不需要管理WiredTiger缓存也不需要跑副本集心跳选举之类的逻辑。所以mongos进程的“身形”很轻通常看它占的内存也就一两百MBCPU主要用于解析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那么多核心参数也就那么几个。我按优先级排个序客户端的maxPoolSizeJava、Node等驱动默认连接池上限通常是100。如果业务并发高100个连接可能不够用可以适当调大但不要盲目调成几千。每个客户端连接都对应mongos内部的一部分资源连接数太多反而降低整体吞吐。建议压测时从100开始逐步加直到吞吐不再上升为止。socketTimeoutMS / connectTimeoutMS连接超时设为2到3秒比较合适能快速暴露网络问题socket超时取决于业务最长可接受等待时间一般30秒起步。读偏好readPreferencemongos支持把读操作转发到分片的从节点上以减轻主节点压力。如果业务能接受稍旧的数据配置readPreferenceprimaryPreferred或者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 preferencemongos路由缓存或分片状态异常no host found检查分片是否处于正常状态重启mongos刷新元数据大量StaleConfig错误chunk迁移导致元数据更新StaleConfig驱动自动重试若持续出现则检查balancer是否频繁触发操作报cannot open connection to shardmongos到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后来所有应用都连它那它就是整个集群前台唯一的接待员。一旦某个粗心同事写了个全表scanmongos连接一满全公司业务跟着遭殃。多部署几个mongos并在应用连接串里都配上这种成本极低的事情收益却非常可观。最后分享一个小技巧每次mongos重启后可以手动执行一遍sh.status()确认元数据拉取成功再放业务流量进来避免“进程起来了但路由没就绪”的短暂空窗。这个步骤写进发布脚本里能少接很多半夜的告警电话。