一次 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 异常崩溃的主要内容,如果未能解决你的问题,请参考以下文章