笔者:李大
有任何疑问欢迎关注微信公众号:网易游戏运维平台。(长按识别上图二维码)
微信公众号原文链接:mongos 连接数问题解析
文章目录
Ocean 项目的 mongo 分片架构中,mongos 作为 mongo 分片架构的路由选择器,目前有两种使用方式,lbc 模式和传统的直连模式。直连模式下,mongos 由项目 SRE 维护管理,在使用中,项目 SRE 提到的比较多的一个问题是 : 客户端到 mongos 和 mongos 到 mongod 的连接数是什么样的关系?是 1: 1 的关系吗?
基于以上疑问,本文深度解析 mongos 的连接数问题 。
客户端到 mongos 的连接
每连接每线程
由于传输层 IO 模式的不同(ASIO 和 legacy), 不同版本 mongos 接收客户端连接的模块代码有些许差异,但是无论底层是 asio 还是 legacy , 默认情况下 mongos 对客户端连接都是【 每连接每线程 】的模型,每个线程分配 1 M 的内存。
static const size_t STACK_SIZE =
1024 * 1024; // if we change this we need to update the warning // 分配的栈空间,1M
struct rlimit limits;
invariant(getrlimit(RLIMIT_STACK, &limits) == 0);
if (limits.rlim_cur > STACK_SIZE) {
size_t stackSizeToSet = STACK_SIZE;
#if !__has_feature(address_sanitizer)
if (kDebugBuild)
stackSizeToSet /= 2;
#endif
int failed = pthread_attr_setstacksize(&attrs, stackSizeToSet);
if (failed) {
const auto ewd = errnoWithDescription(failed);
warning() << "pthread_attr_setstacksize failed: " << ewd;
}
} else if (limits.rlim_cur < 1024 * 1024) {
warning() << "Stack size set to " << (limits.rlim_cur / 1024) << "KB. We suggest 1MB";
}
pthread_t thread;
int failed = pthread_create(&thread, &attrs, runFunc, ctx.get()); // 创建新的线程
所以,当 mongos 接收大量的连接时,对内存的消耗还是挺大的。
adaptive 线程池
为了减少每次新连接时创建与销毁线程的消耗,mongos 在 3.6 版本引入了 一种 adaptive 的线程池。预先创建 adaptiveServiceExecutorReservedThreads 个 worker 线程 和一个 controller 线程,当新的连接时,线程池内分配一个空闲 woker 线程。如果线程池内无空闲线程时,则会创建新的 worker 线程。当 worker 线程在一个执行内周期结束时,检测到真正执行 IO 操作的时间小于 adaptiveServiceExecutorIdlePctThreshold 比例时,则会自动销毁线程。
adaptiveServiceExecutorReservedThreads : 线程池预创建线程数。默认 CPU 核数 / 2 个。
adaptiveServiceExecutorIdlePctThreshold : 线程池 worker 线程空闲时间的百分比,小于该百分比时自动销毁。
3.6 及以上版本才有 adaptive 的线程池功能,而且默认不开启,可以设置以下参数启动线程池功能。
//yaml
net:
serviceExecutor: adaptive
//ini
serviceExecutor = adaptive
adaptive 线程池模式下,每个 worker 线程需要处理多个连接的 IO 操作,具体的连接数情况和 worker 线程无固定的比例关系,mongos 根据内部worker线程的执行情况,自适应的新建或销毁线程。可以通过 mongo shell 执行 db.serverStatus().network.serviceExecutorTaskStats
查看线程池的使用情况。
mongos> db.serverStatus().network.serviceExecutorTaskStats
{
"executor" : "adaptive",
"totalQueued" : NumberLong(25),
"totalExecuted" : NumberLong(25),
"threadsInUse" : 1, // 正在使用(有IO操作)的worker线程数
"totalTimeRunningMicros" : NumberLong(282344119),
"totalTimeExecutingMicros" : NumberLong(11322),
"totalTimeQueuedMicros" : NumberLong(180),
"threadsRunning" : 8, // 当前的 worker 线程数
"threadsPending" : 0, // 创建中的wroker 数量
"threadCreationCauses" : { // 3.6.4 版本新增模块 创建worker线程的原因情况统计
"stuckThreadsDetected" : NumberLong(0), // 因为worker卡住而创建新的worker的次数
"starvation" : NumberLong(0), //线程池饥饿(即worker不够用)而创建worker的次数
"belowReserveMinimum" : NumberLong(8), // 保持最低worker数量(adaptiveServiceExecutorReservedThreads) 而创建worker的数量
"replacingCrashedThreads" : NumberLong(0) // worker crash 而创建新的worker的数量
},
"metricsByTask" : {
/* 3.6.4 版本新增模块,按照执行任务的种类汇总所有worker线程执行情况。所谓不同的任务种类,就是不同的函数。
1、一个连接进来时,首先要经过 startSession 开启一个回话 (session)。所以startSession可以理解为所有接受过的连接的总数。
2、开启 session 以后进去 sourceMessage ,等待接受客户端的命令(message)
3、收到客户端的命令(message) 以后就要处理对应命令,进入 processMessage
4、processMessage 处理完消息以后,如果正常的执行完,得到正常的回复(response)或者无回复则循环执行 sourceMessage 等待新的命令;如果response 是一个 exhaust 的游标,那么继续执行 processMessage, 并记录一次 exhaustMessage的任务种类。
5、对每个任务种类:
totalQueued : 在线程池内排队的次数
totalExecuted : 在线程池内执行的次数
totalTimeExecutingMicros : 在线程池内执行的时间,单位微秒。注意这个时间是所有worker的线程的汇总信息,包含历史worker的统计时间。
totalTimeQueuedMicros : 在线程池内排队的时间,单位微秒。同上。
*/
"processMessage" : {
"totalQueued" : NumberLong(12),
"totalExecuted" : NumberLong(12),
"totalTimeExecutingMicros" : NumberLong(11114),
"totalTimeQueuedMicros" : NumberLong(5)
},
"sourceMessage" : {
"totalQueued" : NumberLong(12),
"totalExecuted" : NumberLong(12),