rs0:PRIMARY> db.serverStatus().globalLock
{
"totalTime" : NumberLong("2651301900000"), //自上次发生lock以来的时间
"currentQueue" : { //锁等待队列信息
"total" : 0, //因为锁而产生的排队的总数
"readers" : 0, //等待读锁而产生的排队数(kQueuedReader)
"writers" : 0 //等待写锁而产生的排队数(kQueuedWriter)
},
"activeClients" : { //活跃连接数信息
"total" : 38, //当前活跃连接数
"readers" : 0, //当前执行读操作的活跃连接数(kActiveReader)
"writers" : 0 //当前执行写操作的活跃连接数(kActiveWriter)
}
}
MongoDB锁:("+" 表示兼容,"-"表示互斥)
锁模式 | MODE_NONE | MODE_IS | MODE_IX | MODE_S | MODE_X |
---|---|---|---|---|---|
MODE_NONE | + | + | + | + | + |
MODE_IS | + | + | + | - | - |
MODE_IX | + | + | + | + | - |
MODE_S | + | + | - | + | - |
MODE_X | + | - | - | - | - |
MongoDB在加锁时是一个层次性的管理方式:
globalLock --> DBlock --> CollectionLock。
MongoDB wiredtiger是文档级别的锁并发。在读写并发时,具体锁的额实现如下:
写操作
1. globalLock (这一层只关注是读还是写,不关注具体是什么LOCK)
2. DBLock MODE_IX
3. Colleciotn MODE_IX
4. pass request to wiredtiger
读操作
1. globalLock MODE_IS (这一层只关注是读还是写,不关注具体是什么LOCK)
2. DBLock MODE_IS
3. Colleciton MODE_IS
4. pass request to wiredtiger
整体流程如下:
1.Client发送请求至MongoDB
2.判断Client状态是kQueuedReader或kQueuedWriter
2.获取ticket(globalLock完成)
正常情况下,如果有没出现锁竞争,所有读写请求都会被pass到存储引擎层
为了限制存储引擎层并发度,可以设置ticket这个值
wiredtiger默认限制传递到引擎层面的最大读写并发数均为128
mmapv1没有ticket的限制
3.Client状态转换为kActiveReader或kActiveWriter
如果该参数长时间不为0,说明服务现在并发较大,负载较高
可以考虑SQL优化、升配来处理
4.lockBegin
加DB、Collection等层次锁
更底层的锁竞争会间接影响到globalLock
总结:
serverStatus.globalLock 或者 mongostat (qr|qw ar|aw指标)能查看mongod globalLock的各个指标情况。
Wiredtiger限制传递到引擎层面的最大读写并发数均为128(合理的经验值,通常无需调整)如果超过这个阈值,排队的请求就会体现在globalLock.currentQueue.readers/writers里。
如果globalLock.currentQueue.readers/writers个值长时间都不为0(此时globalLock.activeClients.readers/writers肯定是持续接近或等于128的),这也说明你的系统并发太高,或者有长时间占用互斥锁的请求比如前台建索引,可以通过优化单个请求的处理时间(比如建索引来减少COLLSCAN或SORT),或升级后端资源(内存、磁盘IO能力、CPU)来优化。
globalLock.activeClients.readers/writers 持续不为0(但没达到128,此时currentQueue为空),并且你觉得请求处理已经很慢了,这时也可以考虑上述中寻找具体慢查询并进行优化处理,或者升级资源。
rs0: PRIMARY > db.serverStatus().connections {
"current": 5, //当前连接数
"available": 814, //剩余可以连接数
"totalCreated": NumberLong(186) //截止到现在创建连接数
}
rs0:PRIMARY> db.serverStatus().mem
{
"bits" : 64, //64位
"resident" : 245, //物理内存消耗
"virtual" : 1262, //虚拟内存消耗
"supported" : true, //支持显示额外内存信息
"mapped" : 0, //映射内存
"mappedWithJournal" : 0 //除了映射内存外还包括journal日志消耗的映射内存
}
rs0: PRIMARY > db.serverStatus().asserts {
"regular": 0, //服务启动后asserts错误个数
"warning": 0, //服务启动后warning个数
"msg": 0, //服务启动后message asserts个数
"user": 22, //服务启动后user asserts格式
"rollovers": 0 //服务启动后重置次数
}
rs0:PRIMARY> db.serverStatus().network
{
"bytesIn" : NumberLong(1013083142), //网络入流量
"bytesOut" : NumberLong(1123552013), //网络处流量
"numRequests" : NumberLong(3592562) //累积请求数
}
rs0:PRIMARY> db.stats()
{
"db" : "test", //数据库名
"collections" : 5, //数据库中集合数
"objects" : 139, //数据库预估数据行
"avgObjSize" : 63.65467625899281, //平均每行数据大小,单位为bytes
"dataSize" : 8848, //当前数据库数据大小,单位为bytes
"storageSize" : 1077248, //当前数据库物理存储大小,单位为bytes
"numExtents" : 5,
"indexes" : 2,
"indexSize" : 16352, //索引空间大小,单位为bytes
"fileSize" : 67108864, //数据库预分配文件大小
"nsSizeMB" : 16,
"extentFreeList" : {
"num" : 1,
"totalSize" : 32768
},
"dataFileVersion" : {
"major" : 4,
"minor" : 22
},
"ok" : 1
}
在会话1执行db.fsyncLock()
在会话2执行db.cc.insert({"name":"aa"})
在会话3执行db.currentOp()
> db.currentOp()
{
"inprog" : [
{
"desc" : "conn10",
"threadId" : "0x3fdf860",
"connectionId" : 10,
"opid" : 380692, //db.killOp使用的就是该opid
"active" : true, //是否活跃
"secs_running" : 4, //执行时间(秒)
"microsecs_running" : NumberLong(4603324),
"op" : "insert", //执行操作类型
"ns" : "test.cc", //执行操作数据库
"insert" : { //执行操作语句
"_id" : ObjectId("5bee323020e268b4d947a580"),
"name" : "aa"
},
"client" : "127.0.0.1:42066", //执行操作客户端
"numYields" : 0,
"locks" : { //执行操作需要持有锁
"Global" : "w"
},
"waitingForLock" : true, //是否锁等待 ?
"lockStats" : {
"Global" : {
"acquireCount" : {
"r" : NumberLong(1),
"w" : NumberLong(1)
},
"acquireWaitCount" : {
"w" : NumberLong(1)
},
"timeAcquiringMicros" : {
"w" : NumberLong(22503637)
}
}
}
}
],
"fsyncLock" : true, //是否全局锁定数据库
"info" : "use db.fsyncUnlock() to terminate the fsync write/snapshot lock"
}
值得注意的是:
1.断开MongoDB Shell后,连接会关闭,但是连接请求的线程并没有结束,直到命令执行完毕,线程给客户端返回结果时,发现连接已经关闭时才会退出线程。
2.MongoDB并不是发送完killOp后请求就会立刻结束
只有当连接对应的服务线程在代码逻辑上存储了killPending字段时,代码会不断调用该参数检查判断killPending的状态。发送killOp后,请求要执行到下一个【检查点】,判断killPending=1后才会杀掉当前会话。
> db.killOp(380692)
{ "info" : "attempting to kill op" }
db.killOp(opid)的实现原理如下:
每个连接对应的服务线程存储了一个killPending的字段,当发送killOp时,会将该字段置1;请求在执行过程中,可以通过不断的调用OperationContext::checkForInterrupt()来检查killPending是否被设置,如果被设置,则线程退出。
一个请求要支持killOp,必须在请求的处理逻辑里加上checkForInterrupt()检查点才行,否则即使发送了killOp,也只能等待请求完全处理完毕线程才会退出。