一次 mongoDB 异常崩溃

Posted 小米运维

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一次 mongoDB 异常崩溃相关的知识,希望对你有一定的参考价值。

本文记述一次 MongoDB 故障以及相应的处理过程。线上 MongoDB 分片集群中的路由节点 mongos 进程某一时刻异常退出,我们通过分析日志、复现故障并结合源码,定位故障原因,给出解决方法。

回顾上篇文章


故障描述


mongos 退出时的日志如下表所示,报 “Failed to mlock: Cannot allocate memory” 错误。同时,分析故障发生时分片集群中多个主节点的日志,发现都有多个连接在执行耗时较长的查询操作。

2017-12-14T18:01:43.047+0800 I ASIO     [NetworkInterfaceASIO-TaskExecutorPool-19-0] Successfully connected to 192.168.1.100:27017, took 291ms (10 connections now open to 192.168.1.100:27017)
2017-12-14T18:01:43.048+0800 I ASIO     [NetworkInterfaceASIO-TaskExecutorPool-19-0] Connecting to 192.168.1.200:27017
2017
-12-14T18:01:43.048+0800 I ASIO     [NetworkInterfaceASIO-TaskExecutorPool-19-0] Connecting to 192.168.1.200:27017
2017
-12-14T18:01:43.050+0800 F -       [NetworkInterfaceASIO-TaskExecutorPool-22-0] Failed to mlock: Cannot allocate memory
2017-12-14T18:01:43.050+0800 I -        [NetworkInterfaceASIO-TaskExecutorPool-22-0] Fatal Assertion 28832 at src/mongo/base/secure_allocator.cpp 246
2017
-12-14T18:01:43.050+0800 I -        [NetworkInterfaceASIO-TaskExecutorPool-22-0] …
2017-12-14T18:01:43.054+0800 F -        [NetworkInterfaceASIO-TaskExecutorPool-22-0] Got signal: 6 (Aborted).

故障分析及重现


初步分析触发故障的原因是大量慢查询导致了 mongos 与 mongod 之间的连接长时间没有释放,使得连接池中的连接无法复用,开始新建连接,而新建连接需要分配一定的锁定内存。mongos 需要分配的锁定内存超过 max locked memory 的限制,于是程序崩溃退出。max locked memory 的默认值如下图所示是 64KB(这个限制是对整个进程的,而且限制只对普通用户起作用,对 root 用户不起作用)。




为了证实以上分析,我们通过以下程序模拟故障发生时的场景,测试程序用多线程连接 mongos,然后使用非 shard key 执行查询操作(会导致慢查询),并且查询结束不释放连接。结果触发了与线上相同的故障。

package main
import (       
      "fmt"       "time"       "gopkg.in/mgo.v2"       "gopkg.in/mgo.v2/bson"
) var cnum chan int
func mongodbConnect(i int) {       var session, errConnect = mgo.DialWithTimeout("192.168.1.99"+":"+"40000"+"?connect=direct", time.Duration(5*time.Second))      
      if errConnect != nil {              panic(errConnect)       }       cred := &mgo.Credential{              Username: "****",              Password: "******",       }      
      if errCred := session.Login(cred); errCred != nil {              panic(errCred)       }       serverStatus := make(map[string]interface{})      
      if errSession := session.DB("admin").Run(bson.D{{"serverStatus", 1}}, &serverStatus); errSession != nil {              panic(errSession)       }       result := bson.M{}      
      if err := session.DB("testDB").C("testC").Find(bson.M{"NUM": i}).One(&result); err != nil {              fmt.Println("query error:", err)       } else {              fmt.Println(result)       }      
      //session.Close()       cnum <- 1
} func main() {       cnum = make(chan int, 1200)      
      for i := 1; i < 1200; i++ {              go mongodbConnect(i)       }      
      for i := 0; i < 1200; i++ {              <-cnum       } }

解决方法


方法一:

调整 max locked memory 的大小。根据之前的分析发现是 mongos 对锁定内存的分配超出了操作系统的限制,所以可以通过调整该限制来解决问题。官方在修复 bug 之前,也是推荐增加 max locked memory 的限制。调整方式如下:ulimit -l 96,该命令将限制值设置为 96KB。具体调整为多少官方没有说明,之后分析源码发现每一个连接占用的锁定内存是 60 个字节,但锁定内存不仅仅在此处用到。所以如果每个连接的请求都发生慢查询,那么 64KB 理论上支持的最大连接数是 1092 个。

方法二: 

升级到 3.4.6 及以上版本。当时线上 mongos 和 mongod 版本是 3.4.4,我们将 mongos 和 mongod 升级到 3.4.9,使用相同的方式进行测试,发现不会发生类似故障。

MongoDB 官网有 issue 提到这个问题 SERVER-28997,这是 3.4.4 版本的 bug,而我们当时线上环境使用的正好也是 3.4.4 版本,之后 3.4.6 版本已经修复。官方描述是:


SaslSCRAMSHA1ClientConversations have a SCRAMSecrets which they 'll pull out of the cache.
SCRAMSecrets allocate secure storage in their default constructor, so they may be populated.
Instead, SaslSCRAMSHA1ClientConversation and the cache should store shared_ptrs to SCRAMSecret.



源码分析


分析源码发现,在 mongos 与 mongod 建立连接的过程中会对发送请求的 client 进行身份认证,而从 3.0 开始默认的认证方式是 SCRAM-SHA-1。在 SCRAM-SHA-1 身份认证的实现过程中会用到一个 SCRAMSecrets 对象。每个连接开始都先从 SCRAMSHA1ClientCache 中查找有没有目标节点的认证信息被缓存下来,如果有就使用缓存中的 SCRAMSecrets 对象。3.4.4 版本 SCRAMSecrets 的实现方式如下图所示,这种方式导致多个连接使用缓存中同一 SCRAMSecrets 对象的时候会拷贝多份 SCRAMSecrets 数据。而 SCRAMSecrets 对象申请的是锁定内存,这样即使认证信息被缓存下来,也会不断地申请锁定内存,又没有及时解除锁定释放内存,就导致超过了 max locked memory 的限制。与之前初步分析的结论有点偏差,不是每个新连接都要分配锁定内存,而是多个连接会拷贝复用同一 SCRAMSecrets 对象,导致所需的锁定内存增加。

struct SCRAMSecrets {
     SecureHandle<SHA1Block> clientKey;
     SecureHandle<SHA1Block> storedKey;
     SecureHandle<SHA1Block> serverKey;
};

从 3.4.6 开始改用引用计数的方式管理认证秘钥。如下图所示,可以发现类中设置了一个 shared_ptr 成员来管理内存的分配,这样多个连接在使用缓存中的同一个 SCRAMSecrets 对象的时候就可以共享一份 SCRAMSecretsHolder 数据。shared_ptr 成员会记录有多少个连接共享相同的 SecureHandle<SCRAMSecretsHolder> 对象,在最后一个连接被销毁时释放它。这种方式减少了锁定内存的分配,从而避免 max locked memory 超出限制导致程序出错。


class SCRAMSecrets {
private:    
   struct SCRAMSecretsHolder {        SHA1Block clientKey;        SHA1Block storedKey;        SHA1Block serverKey;    };

public
:    
   // Creates an unpopulated SCRAMSecrets object.    SCRAMSecrets() = default;    
   // Creates a populated SCRAMSecrets object. First, allocates secure storage, then provides it    // to a callback, which fills the memory.    template <typename T>    
   explicit SCRAMSecrets(T initializationFun)        : _ptr(std::make_shared<SecureHandle<SCRAMSecretsHolder>>()) {        initializationFun((*this)->clientKey, (*this)->storedKey, (*this)->serverKey);    }    …

private
:    std::shared_ptr<SecureHandle<SCRAMSecretsHolder>> _ptr; };



MongoDB 将 SCRAMSecrets 对象 lock 起来,主要是因为这些对象对程序性能影响较大,锁定内存可以避免页面换入 / 换出而引起性能波动。官方声明,之后 3.7 版本将会使用 LRU 算法来管理 SCRAMSHA1ClientCache 中的内存。






悄悄关注,小米运维(๑╹◡╹)ノ


以上是关于一次 mongoDB 异常崩溃的主要内容,如果未能解决你的问题,请参考以下文章

SupportPlaceAutocompleteFragment null异常

mongodb在try catch中出现异常崩溃的节点

对话框片段已添加异常未抛出

片段在重新加载时崩溃

片段中的 EditText 上的空指针异常 [重复]

在片段内使用 GridView 时应用程序崩溃