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在本地过滤出status=pending的文档,把结果返回给mongos,mongos再在内存里统一合并、排序、分页。要命的是,这种广播查询会把每个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文件里加上LimitNOFILE=65535或更大的值,否则进程很容易被文件描述符限制卡住。文件描述符不够时,应用侧看到的现象是“连接建立后马上断开”或者“连接超时”,mongos日志里则会出现Failed to accept new connection之类的报错。
3.3 部署多个mongos:接入层和连接池该怎么做
生产环境不要只部署一个mongos。不是因为mongos本身会挂,而是单点入口一旦重启或繁忙,所有客户端都会受影响。常见的做法是部署两个或更多mongos,然后让应用侧通过两种方式接入。
第一种方式是把多个mongos地址直接写进连接串,例如:
mongodb://mongos1:27017,mongos2:27017,mongos3:27017/?maxPoolSize=200MongoDB官方驱动本身支持多地址连接串,会自动做故障转移和负载分发。这样不需要额外引入负载均衡器,部署更简单,适合大多数中大型应用。
第二种方式是前面加一层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“透明路由”的好处。但不少人在这一步被认证卡住:开启了用户认证的集群,连接串里必须带authSource=admin或者对应的认证数据库。有些人直接在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以上内存,根据连接数、聚合量再往上加。
文件描述符更是重灾区。每建立一条客户端连接到mongos,mongos又会向后端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可以设置Restart=always,而且要在[Service]里设置LimitNOFILE=65535和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这个进程不复杂,但正因为不复杂,很多人反而没花心思在上面,结果被“入口层”的坑反复折磨。把入口层打理顺,分片集群的日常运维会轻松很多。