
1. 先搞懂mongos在分片集群里到底干什么1.1 分片集群的三个组件mongos是唯一入口MongoDB分片集群里最容易被忽略、却长期承担请求入口的组件就是mongos。很多刚上手分片的同学把精力全放在shard和config server上等真正部署完成、应用开始读写才意识到mongos进程才是集群的“门面”客户端连的是它路由查的是它结果合并也是它。如果你把一个分片集群想象成一家大型商场的多个仓库那么mongos就是商场唯一的前台接待处。顾客业务客户端不会直接跑去各个仓库找货而是把购物需求报给接待员接待员根据库存台账告诉顾客去哪个仓库甚至帮顾客把多仓库的商品集中到一起。一边是存储数据的shard“仓库”一边是维护集群元数据的config server“库存台账”而mongos就是那个负责“报门牌号跑腿汇总”的接待员。MongoDB分片集群里mongos不是可选组件而是应用访问分片集群的必经入口。没有mongos客户端要么得知道每一个chunk在哪一个分片、自己去连对应的mongod要么只能把请求广播到所有分片再做合并这等于把路由逻辑塞进应用层。mongos存在的意义就是把“数据到底在哪”这件事从业务代码里抽出来让应用使用分片集群时感觉就像在连接一台普通的mongod单机。这也是为什么很多驱动层、业务层都不需要关心分片细节——C#、Java、Node.js的MongoDB驱动配置连接串时只要填mongos的地址和端口就行。1.2 mongos进程为什么是“无状态”的很多第一次操作mongos的人会下意识地去找它的数据目录其实不用找mongos进程本身不持久化任何业务数据也不保存chunk的原始数据。它只做三件事接收客户端请求、查路由表、把请求转发给对应的shard。这意味着mongos是典型无状态进程。无状态带来了两个非常实用的好处第一可以随随便便水平加节点一个mongos不够就加两个加三个加到十个都没问题第二可以放心地滚动重启和升级mongos进程挂掉不会直接导致数据丢失只要配置服务器还活着、shard还活着重新拉起一个mongos就能继续对外服务。生产环境里一般至少部署两个mongos目的不是“做数据高可用”而是“让入口不成为单点”。但注意无状态不等于可以乱配。mongos启动时的配置参数、运行时的元数据缓存、连接后端分片的连接池都会影响它的表现。我在生产环境里见过有人在mongos所在机器上直接改系统文件描述符上限结果进程起来了流量一大就报“Too many open files”也见过mongos进程确实能启动但因为--configdb里副本集名字写错导致路由表一直拉不到整个集群看起来“假活”。所以说mongos这个进程扛着整个集群的入口压力配置上的一个空格、一个端口都不能马虎。2. 路由与请求转发的核心机制2.1 mongos如何知道数据在哪配置服务器与元数据缓存mongos本身不存路由表它从config server读。config server在分片集群中通常以一个副本集的形式存在专门保存集群的元信息有哪些分片、每个分片上有哪些chunk、分片键的取值范围、集合和数据库的分布情况等等。mongos第一次启动时会从config server完整拉取一份元数据缓存到自己的内存里之后每次有请求过来先查自己的内存缓存而不是每次都去问config server——那样太慢了。关键在于shard之间的数据会因为chunk分裂、均衡迁移而发生变化。config server会更新元数据mongos也会通过订阅机制、定期刷新的方式来同步这些变化。所以mongos进程里那已经不是简简单单的静态路由表而是一份需要不断和config server打配合的动态“地图”。如果你把config server的整个副本集停了已经运行着的mongos会怎么样不会立刻死掉它还能拿着内存里已有的缓存继续处理一部分请求尤其是那些路由信息没变化的请求。但如果此时又发生了chunk迁移、新建了集合、或者客户端发来一个从未查询过的集合mongos就无法完成路由判定请求会失败。所以config server的可用性直接决定集群元数据层面的健康度不能因为mongos有缓存就忽视它。2.2 一次读写请求在mongos里怎么走以一条最简单的读请求为例整个流程可以拆成几步客户端向mongos发起find查询。mongos从连接池里取一个到后端分片的连接并解析请求判断目标集合是否分片。如果集合没有分片mongos会把请求转发到主分片primary shard上执行这个主分片通常是该集合初始存储的位置。如果集合已分片mongos会从缓存的路由表里定位到目标chunk确定chunk所在的shard然后只把子查询发给那一个或多个shard。多个shard返回结果后mongos负责合并排序、处理limit、skip等逻辑再把最终结果回给客户端。写请求的流程也类似。mongos会根据写操作里带的分片键算出该文档应该落在哪个chunk并转发给对应shard的mongod。所以写入时必须提供分片键或者分片键是immutable不可更新的更新一条记录时也不能改分片键字段否则mongos就不知道该把修改后的文档推到哪个chunk里。这个流程里隐含了一个很多新人容易忽略的事实mongos是“请求分发结果汇总”的角色它需要在内存里缓存结果集。如果查询条件不带分片键mongos只能把请求广播给所有分片等所有分片都返回候选数据后再做全局排序、去重。数据量一大mongos的CPU和内存就会被打满这时候问题不一定出在后端shard而很可能出在mongos的合并环节。2.3 分片键对路由的影响广播查询与定位查询很多人问为什么同一个集合带分片键查询就快不带分片键就慢这就是“定位查询”和“广播查询”的差别。假设一个订单表按order_id做了哈希分片查询条件是{order_id: A1001}mongos只要计算一下这个order_id的哈希值落到哪个chunk范围就能精准地把查询发到某一个shard。整个过程只涉及一个分片自然快。如果查询条件变成{status: pending}mongos没有status相关的分片键可利用它只能把查询广播到所有shard。每个shard在本地过滤出statuspending的文档把结果返回给mongosmongos再在内存里统一合并、排序、分页。要命的是这种广播查询会把每个shard上所有符合条件的候选数据都拉到mongos而不是一次只拉一页。如果符合条件的有几百万条mongos的内存压力会瞬间爆表。所以选分片键的时候一定要优先选“业务经常作为精确等值条件查询”的字段。如果实在无法避免非分片键查询至少要保证结果集经过$match、limit等条件后传回mongos的数据量可控。MongoDB本身没有跨分片的全局索引所谓“全局二级索引”并不存在每个shard上建本地索引只能减少单shard内的扫描不能减少广播带来的网络开销。3. 上手部署mongos配置、启动与接入3.1 启动前要把这些条件准备好栽过跟头的人都知道mongos启动失败的第一大原因不是mongos自身的参数而是前置条件没满足。启动mongos前至少检查三件事。第一config server必须已经作为副本集正常初始化而且副本集的名称、成员地址要和后面的--configdb完全一致。第二网络要通了从mongos所在机器到所有config server节点、所有shard节点都得能建立TCP连接别只测本机。第三如果开启认证所有mongos实例必须使用同一个keyFile且keyFile权限必须是600不能是644或更高权限否则mongos会直接拒绝启动。这里特别想说一下副本集名字这件事。早期MongoDB的--configdb参数只写一组地址不带副本集名称现在的新版本要求必须写成副本集名称/节点1,节点2,节点3这样的格式。如果你只写地址或者写错名字mongos会提示“our set name does not match”看起来像极了网络问题实际上就是命名没对上。启动前花十秒钟检查config server的host信息、副本集名称比在日志里翻半天强得多。3.2 mongos进程启动配置与关键参数一个最基础的mongos启动命令大概是这样的mongos --configdb rs_config/mongodb-cfg1:27019,mongodb-cfg2:27019,mongodb-cfg3:27019 \ --bind_ip 192.168.10.10 --port 27017 \ --keyFile /data/mongodb/keyfile \ --logpath /var/log/mongodb/mongos.log --logappend --fork拆开来看--configdb指定config server副本集格式是setName/host1:port,host2:port,...。这是整个命令行里最关键的参数没有之一。--bind_ip生产环境建议绑定实际使用的内网IP不要用0.0.0.0裸奔否则会把mongos暴露到不该暴露的网段。--keyFile开启内部认证时使用所有mongod和mongos进程必须共用同一个文件权限要设置成600。--logpath、--logappend、--fork日志路径、追加写入、后台运行。配合systemd管理时可以不使用--fork由systemd来拉起进程。如果你用systemd托管mongos还要在service文件里加上LimitNOFILE65535或更大的值否则进程很容易被文件描述符限制卡住。文件描述符不够时应用侧看到的现象是“连接建立后马上断开”或者“连接超时”mongos日志里则会出现Failed to accept new connection之类的报错。3.3 部署多个mongos接入层和连接池该怎么做生产环境不要只部署一个mongos。不是因为mongos本身会挂而是单点入口一旦重启或繁忙所有客户端都会受影响。常见的做法是部署两个或更多mongos然后让应用侧通过两种方式接入。第一种方式是把多个mongos地址直接写进连接串例如mongodb://mongos1:27017,mongos2:27017,mongos3:27017/?maxPoolSize200MongoDB官方驱动本身支持多地址连接串会自动做故障转移和负载分发。这样不需要额外引入负载均衡器部署更简单适合大多数中大型应用。第二种方式是前面加一层TCP负载均衡如HAProxy、Nginx stream应用只连接LB的地址由LB把流量分发到多个mongos。这种方式适合要统一管理入口、希望动态扩缩mongos实例的场景。但要注意LB本身会成为新的单点需要再做LB的高可用而且LB会增加一层TCP转发延迟延迟敏感型业务需要压测确认。连接池的设计也容易踩坑。每一个mongos进程都会单独维护到所有shard mongod后端连接的连接池。如果应用部署了20个节点、每个节点连接串里写了3个mongos地址、每个驱动实例默认连接池为100那么这些mongos进程可能会向后端shard建立远高于预期的连接。实际撑不住时shard的mongod进程先报警连接数爆掉然后mongos的转发也开始排队。解决思路是控制客户端连接串里的mongos数量调小驱动maxPoolSize同时监控后端shard的连接数。3.4 用Compass和驱动连接mongos时的常见误区用MongoDB Compass连接分片集群时直接填mongos的IP和端口就能看到全集群的数据视图体验和连接一个普通mongod几乎一样。这也是mongos“透明路由”的好处。但不少人在这一步被认证卡住开启了用户认证的集群连接串里必须带authSourceadmin或者对应的认证数据库。有些人直接在Compass里填了用户名密码却忘了指定认证数据库一直报认证失败其实和mongos本身没任何关系。C#、Java等语言驱动连接mongos也一样写法上几乎与连接mongod相同。唯独要注意的是如果业务里用到了db.collection.bulkWrite()或事务必须确认所有操作都路由到同一个分片或使用集群版本支持的事务mongos会负责协调分布式事务但某些历史版本对跨分片事务支持不完整。另外连接串里如果写多个mongos建议让驱动开启合理的retryReads和retryWrites这样单个mongos滚动重启时应用能自动重试到另一个mongos避免报错。4. mongos性能调优与进程守护4.1 mongos的资源占用模型CPU/内存/文件描述符说到mongos很多人的第一反应是“它不存储数据应该很轻量吧”。这句话对了一半。mongos确实不存数据但它承担着大量请求的解析、路由、合并且逻辑它消耗的是CPU、内存和文件描述符而不是磁盘空间。你可以把mongos想象成前台接待员他虽然不搬货物但每一个顾客都要跟他说话他要翻台账、打电话给仓库、汇总结果。当访客数量上来时前台第一个累趴下。mongos的进程里通常会看到很高的CPU使用率尤其是广播查询和带大量排序的聚合请求打到集群时mongos的CPU会直线上升。内存方面路由表本身可能只占几十到几百MB但请求合并会临时占用更多内存。比如一个查询没有分片键广播到16个分片每个分片返回100万条候选记录mongos要在内存里对这些记录做全局排序这时的内存消耗就不是“轻量”二字能形容的了。所以我见过的生产环境里mongos所在的机器至少会配置8GB以上内存根据连接数、聚合量再往上加。文件描述符更是重灾区。每建立一条客户端连接到mongosmongos又会向后端shard建立对应的转发连接这些连接都会占用文件描述符。默认的1024或4096上限根本不够用生产上建议至少设置到65535。启动前用ulimit -n确认用systemd管理的还要在service配置文件里写LimitNOFILE。4.2 连接风暴与连接池调优思路“连接风暴”是mongos上最常见的问题之一。应用刚发布新版本所有实例同时重启大量连接几乎在同一秒涌向mongos。如果后端分片的连接池没有及时建立mongos会在短时间内收到大量建连请求系统负载瞬间升高紧接着就是连接超时、应用报错。调优有几个方向。第一驱动侧合理设置连接池上限。Java、C#等驱动的默认maxPoolSize可能在100左右如果每个应用实例都保持这个值乘以实例数量很容易爆掉mongos。建议按峰值QPS估算单个实例需要的连接数 峰值QPS / (单连接每秒可处理请求数 * 连接空闲复用率)。一般控制在50到200之间不要盲目调到几千。第二mongos侧限制最大连接数。在配置里设置net.maxIncomingConnections让mongos超过上限时直接拒绝新连接并返回错误而不是硬扛到OOM。这样至少能保护mongos进程本身不挂客户端错误也会更明确。第三应用侧开启连接池复用避免每个请求新建连接。MongoDB驱动默认会复用连接但某些老代码里手动调用MongoClient构造很频繁也会造成连接浪费。遇到mongos连接数暴涨时先查一遍驱动初始化逻辑往往能发现是“每次操作都new了一个client”。4.3 日志级别与监控指标怎么看mongos的日志默认只会记录启动信息、错误和慢查询光看默认日志很难提前发现性能隐患。我在排障时习惯把sharding和query组件日志级别临时调高定位问题后再调回来db.adminCommand({ setParameter: 1, logComponentVerbosity: { query: { verbosity: 1 }, sharding: { verbosity: 1 } } });但注意调试完一定要恢复默认级别否则mongos的日志量会巨大磁盘很快被打满。生产环境里很多“日志盘爆掉”的事故就是有人把verbosity调到2、3后忘了收回来。监控指标方面我常用的工具是mongostat、mongosh和db.currentOp()。mongostat连接到mongos端口后重点看这几列conn当前客户端连接数持续上涨且接近maxIncomingConnections时要注意。qr/qw读队列/写队列如果排队数长期大于0说明mongos或后端shard处理不过来。netIn/netOut网络吞吐。广播查询多时mongos和shard之间的netOut会显著增大。command每秒命令次数抓热点命令用。另外db.currentOp()可以查看mongos当前正在执行的请求哪些操作在等待、哪些操作占了大量CPU一目了然。遇到“mongos CPU高”的问题先跑一遍currentOp看是什么请求在打通常会发现一两个缺少分片键的聚合扫全集群。5. 排查实录mongos启动失败与运行异常的常见坑5.1 启动失败配置服务器连不上、协议版本不匹配先手把手过一遍常见的启动失败现场。在mongos机器的日志里看到XXX ERROR: Cannot reach any nodes for set rs_config先别急着怀疑mongos检查mongos机器到config server三台机器的端口通不通。可以用nc -vz config-ip 27019快速验证。如果网络正常再检查config server副本集是否还活着登录任意一台config server执行rs.status()确认primary节点存在且成员状态正常。很多情况下是config server被误停或重启后成员信息异常导致mongos拉不到元数据。再看另一个常见报错XXX ERROR: our set name does not match这百分百是--configdb参数里写的副本集名称和实际config server副本集名称不一致。有人搭建时给副本集叫rs_config启动mongos时却写成了cfg_rs那肯定对不上。改名不是分分钟的事最稳妥的是统一所有节点上的副本集名称再重启config server和mongos。还有一类报错是关于协议认证的XXX ERROR: keyFile /data/mongodb/keyfile has permissions 0644MongoDB对keyFile权限要求极其严格必须为600。这个错误即使你是root用户也会报处理方式就是chmod 600 /data/mongodb/keyfile然后重启mongos。看似是个简单问题实际工作中遇到的人不少因为复制keyFile时经常把权限也带过去了。5.2 查询慢不是mongos的锅定位数据分布与分片键很多人一遇到“通过mongos查得慢”第一反应就是mongos性能不行。其实多数时候问题根源在数据分布和查询方式。我处理过一个典型的场景某个集合用user_id做哈希分片但业务人员经常按create_time范围查询。这类查询注定要被mongos广播到所有分片。为了确认根因我在shard节点上直接执行同样的查询发现单个shard查询很快就完成了说明慢不在存储层而在于mongos要把几十个shard的结果合并、排序再返回给应用这个动作消耗了大量时间和内存。解决方案通常有两种思路。一是尽量让查询带上分片键或者在分片键的某个范围内再叠加过滤条件把大部分查询变成精准路由。如果业务确实经常按另一个字段查询可以考虑更换分片键但这涉及数据重分布成本较高一般要在设计期就定好。二是对确实无法命中分片键的高频查询做缓存层比如用Redis缓存热点数据避免每次都走全集群广播。还有一种情况是查询条件带了分片键但分片键的选择性不好。比如用了一个只有两三个不同值的字段做分片键那么数据其实只落在两三个chunk上分不匀。这时候mongos虽然可以精准路由但热点都集中在一两个shard全集群的并发能力发挥不出来。检查这种问题可以用sh.status()查看各个chunk的数量分布看数据是不是分散到了所有分片。5.3 mongos进程异常退出后的恢复与高可用mongos进程不像mongod那样有数据文件需要恢复它挂了以后只要重启并连上config server就能恢复服务这是无状态带来的好处。但服务恢复不等于应用无感正在进行的请求会报网络错误。所以高可用不是让mongos进程永不退出而是让应用能在mongos挂掉后快速切换到另一个。线上为了保证高可用最好做两件事。第一给mongos进程配systemd托管或supervisor守护一旦进程退出自动拉起。systemd unit可以设置Restartalways而且要在[Service]里设置LimitNOFILE65535和ExecStartPre检查配置。第二应用连接串里配置多个mongos地址并开启驱动层面的重试机制。前面提过官方驱动对retryReads和retryWrites支持已经很成熟mongos滚动重启导致的瞬时失败基本会被自动重试消化掉。你不需要在业务代码里写复杂的故障转移逻辑只要把连接串和驱动参数配对。5.4 运维琐事端口冲突、目录权限、版本注意运维mongos还有一些零碎但常见的坑。端口冲突算一个。很多服务器上已经跑了一个mongod或别的进程占用了27017你再起mongos就会报Address already in use。排查时用ss -lntp | grep 27017看看到底是哪个进程占着端口。如果必须用27017先把旧进程停掉或者给mongos换一个端口。日志目录权限也是个容易被忽略的点。mongos启动时如果没有权限写logpath会在日志还没落地前就退出。注意/var/log/mongodb/目录的属主和写权限要提前准备好尤其在使用systemd时进程用户可能不是root别把日志目录所有权搞错。版本一致性更要说一句。mongos的版本必须和shard、config server的版本对齐不能一个集群里mongod是4.4、mongos是5.0那样会出现协议不兼容或者元数据格式解析失败。升级分片集群时先升级config server再升级shard最后升级mongos而且mongos的升级依赖应用连接串变更需要规划好窗口。另外我在部署时还会特别检查一个细节mongos机器的时间设置。分片集群内部节点之间对时间漂移不敏感到毫秒级但用于认证和票据时如果时间偏差太大会出现握手失败、认证异常。排障时如果遇到“client和server的认证突然失效”顺手date对比一下节点时间常常能省事不少。6. 给后来者的几个实操建议最后分享一些我自己的心得。运行mongos这么多年最大的感受是不要把它当成一个可有可无的旁路进程它值得拥有和mongod一样的重视程度。第一部署前写一份checklist。我现在每次搭建分片集群都会先列一遍config server副本集名称是什么、节点IP和端口是多少、keyFile在哪些节点上、权限是不是600、mongos版本和集群版本是否一致、端口有没有被占用、systemd的LimitNOFILE有没有写。这份checklist解决了我90%的“启动失败”问题。第二mongos日志的verbosity不要乱调。调试时开高调完就关。不然过两天磁盘被日志占满执行df -h一看全是一个组件在刷屏。这个坑我踩过代价是生产环境停服半小时。第三监控告警不要只看mongos进程本身。mongos是路由层它的异常往往反映在连接数、CPU和网络吞吐上。我给mongos设的告警指标很简单进程存活、连接数曲线、读队列/写队列、CPU使用率。连接数突然翻倍、CPU长时间跑到90%以上都是要先看后端shard的征兆而不是只盯着mongos重启。如果你也正在搭MongoDB分片集群或者已经跑起来但遇到各种奇怪问题希望这篇内容能帮你省点时间。mongos这个进程不复杂但正因为不复杂很多人反而没花心思在上面结果被“入口层”的坑反复折磨。把入口层打理顺分片集群的日常运维会轻松很多。