
MongoDB开启认证是很多团队从开发环境走向生产环境时必经的一步但也是运维事故的高发点。最近我处理了一起很典型的线上问题某服务在MongoDB开启认证后运行一段时间就会出现“断连假死”应用进程还活着但所有请求都卡住除了重启什么都做不了。这个问题折腾了我好几天排查过程涉及的坑不少今天把它完整梳理一遍希望能帮到正在被同类问题折磨的朋友。这个问题的完整链路是MongoDB开启认证 → 应用连接串配置不匹配/连接池参数不合理 → 服务端断开连接 → 驱动反复重连失败 → 业务线程全部阻塞等待 → 应用假死。如果你正在负责MongoDB的认证接入或者已经遇到类似“跑着跑着就断重启就好过一会儿又断”的现象这篇文章值得仔细看完。1. 现象与本质这种“断连假死”到底是什么1.1 我遇到的现场一起来看三个典型特征那天凌晨的告警很明确Java服务健康检查失败所有接口超时CPU却没有飙高内存也没满。看了线程栈大量线程停在同一个位置——等待MongoDB驱动的连接。这就是典型的“假死”进程没挂GC正常但业务线程全部阻塞在数据库连接获取上HTTP自然全部超时。另一个特征也很关键MongoDB服务端的mongod日志里每隔一段时间就会出现一条认证相关报错时间点和应用卡死高度吻合。但服务端负载很轻CPU、磁盘、网络都没有异常。还有第三个特征只要把应用重启一切恢复正常但过几个小时或者第二天又复发。这种“重启就好、反复发作”的规律说明问题不是偶发的网络抖动或单次慢查询而是某个持续存在的配置或者资源问题。如果你的现象也同时满足这三点那基本可以锁定是认证开启后的连接管理问题而不是MongoDB本身性能问题。1.2 用两条命令确认“假死”而不是真死判定假死我建议不要靠猜测直接用数据确认。分两个方向查。应用侧Java服务直接jstack导出线程栈搜索MongoDB驱动的类名。我看到过的最典型的栈是卡在MongoClient的getConnection或者DefaultConnectionPool.awaitAvailable。如果是Node.js服务可以看事件循环是否有大量pending操作或者用process._getActiveHandles()看看socket状态。Go服务则直接抓goroutine栈找net/http和mongo相关的阻塞点。MongoDB侧用mongosh连上admin库执行db.currentOp(true)关注有没有大量waitingForLatch或者msg: waiting for connection的操作再看看db.serverStatus().connections的available值。连接池一般在配置范围内currentOp里也不会有慢查询。这就把数据库本身的性能问题排除了。配合mongostat观察如果connections数值变化不大、qr/qw不高但应用已经卡死基本就能判断瓶颈在客户端到驱动这段链路上。1.3 还原整条因果链断连如何一步步拖垮应用我梳理了一下这个问题通常是这样演化出来的MongoDB开启认证后如果应用侧配置不对可能是连接串没写用户名密码也可能是认证库不对驱动并不会在启动时就立刻失败——有些连接是在运行期间才被服务端验证并断开的。服务端一旦因为认证失败或会话非法主动断开TCP连接驱动连接池会把这个失效连接剔除。问题在于剔除之后新的请求需要重新建立连接并完成认证握手。如果每次认证握手都失败例如密码错了或者认证机制不匹配驱动会按照serverSelectionTimeoutMS和socketTimeoutMS的配置不断重试。在高并发下大量请求同时等待连接连接池被占满新请求只能进入等待队列。等待队列一旦堆积业务线程池耗尽应用对外表现就是“假死”。更麻烦的是有些服务的健康检查也依赖数据库查询数据库连不上健康检查就失败负载均衡器把实例摘掉于是所有流量都压到剩余实例上形成雪崩。这条因果链里最值得注意的一点是根因往往是非常隐蔽的配置错误而不是数据库故障。2. 开启认证后断连的常见根因拆解2.1 最大的坑连接串没有带上认证信息或漏掉authSource我这次事故的直接原因就是应用连接串里写的是mongodb://127.0.0.1:27017/appdb根本没带用户名和密码。开启认证之前这个串完全没问题开启之后MongoDB要求客户端必须认证服务端对未认证连接的处理并不是立即拒绝而是允许建立连接等客户端发起操作时才返回Unauthorized。这种“半开连接”的状态很有迷惑性应用启动时连接池预创建连接可能碰巧成功了或者应用框架在启动阶段没有立刻访问数据库等到运行起来执行真正的业务查询时才报认证错误。而驱动在收到Unauthorized后会把连接标记为不可用并重新建立连接如果新连接依然没有凭据就是不断重复“建连→被拒→再建连”的循环。更隐蔽的是authSource写错。MongoDB的用户名和密码是绑定在指定数据库下的认证时默认使用连接串里/后面的数据库这里叫authSource。如果创建用户是在admin库下创建的连接串写成mongodb://user:pass127.0.0.1:27017/appdb驱动会去appdb库下找这个用户自然找不到认证失败。这类问题要优先排查连接串配置尤其是那些从配置文件、环境变量或者K8s Secret里读取连接串的场景特殊字符转义也是一个高频出错点。2.2 副本集内部认证与keyFile问题如果你用的是副本集而不是单节点开启认证的坑还要再多一层。MongoDB副本集在开启auth之后节点之间必须使用内部认证通常是通过keyFile实现。keyFile本质上是一个共享密钥文件所有副本集节点内容必须完全一致权限不能超过600而且长度要求是6到1024个Base64编码字符。我遇到过一种情况三个节点其中两个节点的keyFile正常一个节点因为部署脚本重新生成过keyFile导致内容与其他节点不一致。从应用的角度看副本集的primary角色会变得不稳定有时能连上有时连不上。客户端连接池里原本可用的连接会随着节点角色变化被服务端重置应用就会出现间歇性的断连。这种情况下MongoDB日志里通常会出现KeyFile或者ReplicaSetMonitor相关的错误但很多人只盯着应用日志看忽略了服务端日志里的内部认证报错。还有一点要注意使用keyFile的前提是security.authorization: enabled两者必须同时配置。只开认证、不配keyFile副本集节点之间会无法同步表现也是连接异常。2.3 SCRAM机制不匹配与旧驱动兼容性MongoDB从4.0开始支持SCRAM-SHA-256认证机制4.0之前是SCRAM-SHA-1。老版本的驱动默认使用SCRAM-SHA-1而在MongoDB 7.0中如果用户创建时没有显式指定mechanisms默认会同时支持两种。但如果你显式创建了只使用SCRAM-SHA-256的用户而应用驱动只支持SCRAM-SHA-1认证就会失败。这类问题的排查方式是用mongosh登录admin库执行db.getUser(用户名)查看mechanisms字段再查一下驱动版本的官方文档确认支持哪种机制。我见过因为驱动版本太旧在MongoDB升级到7.0后突然出现认证失败的情况就是因为新版本把SCRAM-SHA-1默认禁用了。类似的还有LDAP、Kerberos认证如果配置了企业版认证模块客户端连接时的认证流程完全不一样这种出现断连优先检查服务端setParameter里的authenticationMechanisms是否包含客户端使用的机制。2.4 连接池耗尽与重试风暴连接池耗尽本身不是根因它更像一个放大器。认证信息错误或者网络不稳定时驱动会不断尝试重连而每一次重连失败都会让一个新请求进入等待队列。MongoDB驱动的默认maxPoolSize通常是100如果应用有200个线程同时访问数据库超过100的部分全部要排队。更糟的是当连接池满了驱动不会立刻失败它会等待重试直到超过waitQueueTimeoutMS有的驱动叫maxWaitTime。这个等待时间默认往往比较久在Java驱动里默认是120秒。也就是说一旦连接池被无效连接占满后续请求可能要在队列里挂两分钟才报超时。在线程堆积的过程中应用内存里还会积压大量待处理的请求对象进一步增加GC压力整个应用看起来就像冻住了一样。所以看“假死”问题不能只看MongoDB的连接池还要看应用自身的线程池配置和等待超时时间。2.5 会话和游标泄漏隐蔽的连接占坑MongoDB 3.6之后引入了显式会话session如果应用代码里创建了session但忘记关闭这些session会一直占用服务端的资源。类似的还有未关闭的游标cursor。表面上连接池里还有空闲连接但服务端处理新请求的能力在下降。时间长了服务端可用会话耗尽新连接虽然能建上但所有操作都排队等待会话释放应用同样表现为“连上了但请求不返回”。这种问题不像认证错误那样有明确报错需要查服务端db.serverStatus().sessions以及db.currentOp(true).inprog.filter(op op.cursor op.active false)如果看到大量空闲但未释放的游标或者会话就该去应用代码里查那些没有用try-with-resources或者finally块关闭资源的路径了。3. 系统性排查步骤从日志到参数一个都不放过3.1 服务端日志怎么查三种关键报错特征遇到断连假死我习惯先看服务端日志因为客户端日志往往不完整。MongoDB的系统日志一般在/var/log/mongodb/mongod.log开启认证后以下几类日志要重点找。第一类是认证失败Authentication failed for user xxx on db admin这类日志直接指向用户名、密码或者authSource的问题。第二类是权限不足not authorized on admin to execute command { find: xxx }这类说明认证成功了但用户角色权限不够覆盖应用实际执行的操作。有些应用框架启动时会创建索引或者读取system.profile用的用户没有相应权限就会报错。第三类是连接被重置connection() connection closed如果关闭频率很高说明连接生命周期管理异常可能是驱动空闲超时、服务端net.maxIncomingConnections耗尽或者负载均衡器的空闲连接回收。在日志里搜索这些关键词时建议直接按时间窗过滤grep -E Unauthorized|Authentication failed|not authorized|terminated /var/log/mongodb/mongod.log | tail -2003.2 用mongosh逐段验证认证链路拿到报错后先用mongosh手动验证认证链路。这一步骤看起来很基础但能帮你快速把问题范围缩小到应用配置还是MongoDB用户配置。第一步确认用户创建在哪个库mongosh mongodb://127.0.0.1:27017/admin --username admin --password yourpassword如果这个能登录说明MongoDB层面认证是通的。然后再试应用实际指定的authSourcemongosh mongodb://127.0.0.1:27017/appdb --username admin --password yourpassword如果第二个能通而第一个不通多半是应用连的是appdb但你用户建在admin或者反过来。第二步检查应用用户实际需要的权限。很多应用只用读写权限但业务代码里可能会用到createIndex、aggregate、drop等操作。用mongosh登录后执行db.getUser(应用用户名)看roles数组里有没有对应权限。如果应用代码里有创建索引的逻辑用户必须要有dbAdmin或者dbOwner权限否则运行到那一步报错连接虽然没断但应用可能因为异常处理不当导致线程挂住。第三步验证认证机制查看用户文档里的mechanisms和服务端配置的authenticationMechanisms做对比。如果服务端只开了SCRAM-SHA-256而你的驱动代码里强制指定了SCRAM-SHA-1那连接字符串再对也白搭。3.3 客户端视角最小复现脚本与关键连接池指标服务端验证完我会写一个最小脚本去模拟应用的真实行为。原则很简单用和线上完全相同的连接串、相同的用户、相同的认证库、相同的操作类型复现断连。以Java为例String uri mongodb://user:pass127.0.0.1:27017/appdb?authSourceadminserverSelectionTimeoutMS5000; MongoClient mongoClient MongoClients.create(uri); MongoDatabase db mongoClient.getDatabase(test); MongoCollectionDocument coll db.getCollection(test); // 模拟业务操作 for (int i 0; i 100; i) { try { coll.find().first(); System.out.println(ok: i); } catch (MongoException e) { System.err.println(fail: i , e.getClass().getName() : e.getMessage()); } Thread.sleep(1000); }Python版本类似from pymongo import MongoClient uri mongodb://user:pass127.0.0.1:27017/appdb?authSourceadminserverSelectionTimeoutMS5000 client MongoClient(uri) db client.test coll db.test for i in range(100): try: coll.find_one() print(ok, i) except Exception as e: print(fail, i, type(e).__name__, e) time.sleep(1)跑这个脚本的过程中观察连接池指标。Java驱动可以通过MongoClientSettings注册ConnectionPoolListener来打印连接池事件重点看ConnectionPoolOutEvent的数量是否等于新连接建立的次数ConnectionCheckOutFailedEvent的失败原因ConnectionClosedEvent的关闭原因如果是Python的pymongo可以通过serverStatus().metrics间接观察或者给MongoClient传一个事件监听器。这个脚本跑完基本能分清是认证配置错误、还是连接池参数不合理、还是代码里异常处理丢了连接。3.4 按场景分类定位启动即断、运行中周期性断、高并发时断同样都是“断连假死”不同的触发时机对应不同的根因在排查时要分开处理。启动阶段就反复断连优先查连接串本身是否有语法错误、用户名密码是否对、authSource是否指定、用户是否建在正确的库下。运行过程中周期性断连并且间隔时间比较规律优先怀疑空闲连接被服务端或网络设备回收而驱动没有感知到。此时要检查maxIdleTimeMS、Socket的keepAlive、负载均衡器的空闲超时。高并发时突然断连优先看连接池耗尽、文件描述符耗尽、服务端net.maxIncomingConnections打满。用ss -s看系统的TCP连接数用lsof -p 进程号 | wc -l看文件描述符使用量往往能一锤定音。4. 修复与加固参数配置与长期监控4.1 正确的连接串与URI编码连接串是最容易出事的配置点。先说特殊字符转义。如果密码里包含、:、/、%等URI保留字符必须做URL编码否则MongoDB驱动会解析错误。比如密码是Pssw:rd在连接串里应该写成mongodb://user:P%40ssw%3Ard127.0.0.1:27017/appdb?authSourceadmin编码为%40:编码为%3A。如果不确定可以先用脚本把密码转义一下再拼连接串。第二个容易踩的坑是把整个密码直接明文写在代码里。我强烈建议从环境变量或配置中心读取别提交到Git仓库这是安全问题也是事故隐患。第三个坑是连接串里的数据库名和authSource的区别。连接串里的数据库名是“要访问的业务库”authSource是“认证使用的用户库”。常规做法是用户建在admin下业务库是appdb连接串形如mongodb://user:passhost:27017/appdb?authSourceadmin这样写驱动会先在admin库认证然后访问appdb。4.2 驱动连接池与超时参数推荐连接池参数没有绝对标准要根据业务特征去调但我可以给一套经过实践检验的起步配置再解释一下逻辑。maxPoolSize: 50 minPoolSize: 5 maxIdleTimeMS: 30000 waitQueueTimeoutMS: 5000 serverSelectionTimeoutMS: 5000 socketTimeoutMS: 10000 connectTimeoutMS: 5000maxPoolSize不要盲目调大。200个线程不一定要配200个连接如果单次查询也就几毫秒50个连接足够支撑每秒几千次查询。连接数过多反而会增加MongoDB服务端的上下文切换压力。maxIdleTimeMS建议显式设置不要依赖默认值。默认情况下驱动可能长期保留空闲连接这些连接被交换机或者云平台的keepalive策略静默回收后应用还傻乎乎地拿出来用就会触发断连。设置成30秒让驱动主动淘汰超时空闲连接反而稳定。waitQueueTimeoutMS建议设置成5秒以内。默认值太长会让业务线程长时间挂起放大假死影响。宁可让请求快速失败返回错误也别让所有线程阻塞几分钟。serverSelectionTimeoutMS决定驱动找不到可用服务器的重试时间我通常设置5秒。这个值设置太短遇到一次网络闪断就直接报错设置太长故障时请求全部堆积。还有一个容易被忽略的参数是retryWrites和retryReads。MongoDB 4.2驱动默认开启重试写入但重试的前提是会话可用。如果连接频繁断开重试逻辑可能掩盖真实问题建议排障阶段先关闭重试让报错快速暴露出来。4.3 副本集keyFile配置与安全管理如果使用副本集开启认证时keyFile这一步不能省。我的做法是这样的用OpenSSL生成随机密钥openssl rand -base64 756 mongodb-keyfile chmod 600 mongodb-keyfile chown mongod:mongod mongodb-keyfile然后将这个文件分发到所有副本集节点路径保持一致内容必须一模一样。在mongod.conf中security: authorization: enabled keyFile: /etc/mongodb/mongodb-keyfile更新完配置后逐个节点滚动重启不要同时重启所有节点避免副本集长时间无主。keyFile权限如果不是600MongoDB服务甚至会拒绝启动这一点在部署脚本里要写好检查逻辑。我踩过一次坑用Ansible部署时模板渲染把权限改成了644所有节点全部启动失败排查了很久才发现是权限问题。还有一个进阶建议不要把keyFile放在应用代码仓库里密钥文件和配置分开管理定期轮换。轮换方式通常是先给所有节点换成新keyFile并滚动重启确认副本集状态正常后再删旧文件。4.4 长期监控与巡检少不了的几个指标事后修复只能治标要治本还得靠监控把这些隐藏问题提前暴露出来。MongoDB侧通过db.serverStatus()能拿到的关键指标connections.current和connections.available连接数的健康度metrics.connections.totalCreated新连接创建速率。这个值如果一直增长说明连接在频繁重建多半有连接生命周期问题metrics.commands.failed命令失败次数认证错误和权限错误都会体现在这metrics.security.authentication认证成功率应用侧如果能接入驱动的事件监听器把连接池的waitQueueSize、checkedOutCount、pendingCount上报到监控平台遇到假死能提前预警。比如waitQueueSize连续5分钟大于10就触发告警。告警阈值不用太精细重要的是别等到服务完全卡死才收到通知。配合日志系统把MongoDB驱动打出来的WARN和ERROR级别日志单独采集到一个索引/文件中出了问题可以快速检索不用再一台台机器翻日志。5. 常见问题速查与避坑心得5.1 故障场景速查表我把这次排查过程中遇到的典型场景整理成了表格方便后续快速对照。现象可能原因首查目标解决思路启动后连接就报Unauthorized连接串没有用户名密码mongod日志中的Authentication failed修正连接串确认authSource周期性断连间隔稳定空闲连接被服务端/网络设备回收maxIdleTimeMS、负载均衡超时设置maxIdleTimeMS开启TCP keepAlive高并发时连接池耗尽maxPoolSize过小/慢查询占连接应用日志的MongoTimeoutException调大连接池优化慢查询副本集primary不稳定keyFile不一致/认证失败mongod日志中的内部认证报错统一keyFile滚动重启建连成功但操作报not authorized用户角色权限不足mongosh查看db.getUser调整roles补充权限应用假死后短暂恢复又再犯连接池被无效连接占满waitQueueTimeoutMS配置缩短等待超时让请求快速失败服务端慢操作和认证无直接关联游标/会话泄漏serverStatus().sessions修复代码确保释放资源5.2 几条保命经验第一开启认证这件事不要在生产上直接改配置重启。先弄一个预发环境用和线上完全一致的连接串和用户配置验证一遍确认应用代码不需要任何改动再上生产。认证开启后连接串、用户权限、驱动版本、认证机制任何一环出错都可能导致线上事故。第二MongoDB日志保留策略要检查一遍。我遇到过有的节点只保留最近几小时日志事故发生后想追查根因日志已经被覆盖了。至少保留7天并且对Authentication failed、Unauthorized这类关键词做独立的日志切割和报警。第三排障的时候保持测试脚本和线上环境一致。不要拿mongosh能连上就断定应用没问题mongosh默认使用的认证机制、驱动版本和应用完全可能不同甚至mongosh连的是admin库应用连的是业务库结果完全不同。第四如果有条件提前把连接池监控接上。这次事故如果早有连接池等待指标完全可以在影响业务之前定位到连接串配置问题不用等凌晨被监控喊起来。最后再分享一个我自己的习惯每次配置完MongoDB认证我都会主动执行一次“破坏性演练”。临时给应用配一个错误的密码观察驱动和应用的报错行为确认错误能被正确捕获和上报而不是静默堆积导致假死。这个操作看似简单却能在真正出事时帮你省掉大量定位时间。这次踩坑之后我把MongoDB认证接入的检查流程沉淀成了一份清单包含了用户创建、连接串拼接、权限核对、驱动版本、连接池参数、副本集keyFile、监控指标这几个固定项目。每次新服务上线前照着清单过一遍能挡住绝大多数认证相关的坑。希望这篇记录也能成为你的排障清单之一。