在我的 Node.JS 应用程序中,我使用集群来利用我的多核 CPU。我正在使用节点的 mariasql 库与我的数据库进行通信。由于该node-mariasql
库不支持池化,因此我使用第三方 - generic-pool来维护连接池。
我注意到,每当主线程中的连接在未捕获的异常导致其中一个子集群重新启动后关闭时,我们的 CPU 使用率就会上升到 100%。
每当子集群重新启动时,我都会破坏所有 MySQL 连接。
节点版本 - v4.2.2
MariaDB 版本 - v10.0.15
节点-mariasql 版本 - v0.2.5
可重现的代码 - https://github.com/bsurendrakumar/node-simplex/
代码片段
创建连接池...
var pool = poolModule.Pool({
name: 'mariadb',
create: function(callback) {
var client = new mSQLClient();
client.connect(dbConfig);
client.on('error', function(err) {
callback(err, null);
});
client.on('ready', function() {
callback(null, client);
});
},
destroy: function(client) {
if(cluster.isMaster) {
console.log('Destroying / ending master thread ID -', client.threadId);
}
if(isDraining) {
client.destroy();
} else {
client.end();
}
},
max: dbConfig.maxConn,
min: dbConfig.minConn,
idleTimeoutMillis: dbConfig.idleTimeout
});
在主线程...
console.log('------------------------------------');
console.log('Master Process ID:', process.pid);
console.log('------------------------------------\n\n');
console.log('Creating an extra DB connection on the master thread.\n\n');
getCountries();
// Create a worker for each CPU
for (var i = 0; i < cpuCount; i += 1) {
cluster.fork();
}
// Restarting the thread if something exits...
cluster.on('exit', function () {
cluster.fork();
});
一旦发生未捕获的异常...
// Handle uncaught exceptions...
process.on('uncaughtException', function (err) {
try {
console.log('\n--------------');
console.log(err);
// Stop the HTTP Server
console.log('\n--------------');
console.log('Encountered uncaught exception!');
console.log('Stopping HTTP server ...');
if(httpServer) {
httpServer.close();
}
console.log('Stopped HTTP server, performing cleanup ...');
// Call the cleanup function
cleanUp(function() {
// Exit!!
console.log('Cleanup done!');
restartProcess();
});
} catch (e) {
console.log(e);
restartProcess();
}
function restartProcess() {
console.log('Restarting process ...');
process.exit(1);
}
});
清理功能...
function cleanUp(cbMain) {
isDraining = true;
if(pool.hasOwnProperty('_inUseObjects')
&& Array.isArray(pool._inUseObjects)
&& pool._inUseObjects.length > 0) {
let inUseObjs = pool._inUseObjects;
let inUseObjsLen = pool._inUseObjects.length;
for(let i = 0; i !== inUseObjsLen; ++i) {
inUseObjs[0].destroy();
pool.release(inUseObjs[0]);
}
}
pool.drain(function() {
pool.destroyAllNow(function() {
return cbMain();
});
});
}
池中的最小连接数设置为 5。它的所有配置都可以在这里找到。因此,当服务器启动时,通用池将启动 5 个与 MySQL 的连接并将它们保留在其池中。
池中的idleTimeout
对象的时间已设置为 120 秒。这意味着如果池中的对象超过 5 个(因为最少 5 个),并且其中一个在过去 120 秒内未使用,它将被销毁。
在服务器启动时,我正在对我们的国家模型进行简单调用以获取国家列表。这段代码在这里。这将建立与数据库的新连接,因此现在池中将有 6 个 SQL 连接,其中一个将在 120 秒后被清除。
以下是我认为问题在于我们使用mariasql库的分步过程 -
- 当服务器启动时,我将进程 ID 记录到控制台。获取主进程 ID,例如 - 20584。
使用 - 查看进程正在使用的文件描述符
ls -l /proc/20584/fd
。记下套接字连接。这个的输出看起来像这样 -lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 12 -> socket:[2469914] lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 13 -> socket:[2469917] lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 14 -> socket:[2468106] lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 15 -> socket:[2468109] lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 17 -> socket:[2467206] lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 18 -> socket:[2467208] lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 19 -> socket:[2467210] lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 2 -> /dev/tty lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 20 -> socket:[2467212] lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 21 -> socket:[2467214] lrwx------ 1 abijeet abijeet 64 Jun 9 19:24 22 -> socket:[2467306]
复制一些套接字编号,例如2467212,然后运行
lsof | grep 2467212
。您会注意到这些是与 MySQL 服务器的连接。的输出应该是这样的 -node 20584 abijeet 20u IPv4 2467212 0t0 TCP localhost:57092->localhost:mysql (ESTABLISHED) V8 20584 20585 abijeet 20u IPv4 2467212 0t0 TCP localhost:57092->localhost:mysql (ESTABLISHED) V8 20584 20586 abijeet 20u IPv4 2467212 0t0 TCP localhost:57092->localhost:mysql (ESTABLISHED) V8 20584 20587 abijeet 20u IPv4 2467212 0t0 TCP localhost:57092->localhost:mysql (ESTABLISHED) V8 20584 20588 abijeet 20u IPv4 2467212 0t0 TCP localhost:57092->localhost:mysql (ESTABLISHED)
通过访问http://127.0.0.1:3000/api/v1/country/list使服务器崩溃。这将使其中一个子进程崩溃。每当发生未捕获的异常时,我都会进行一些清理并退出。然后我分叉另一个进程来代替刚刚被杀死的进程。清理包括 -
- 关闭http服务器
- 关闭通用池中的 MySQL 连接
- 关闭 winston 记录器流。
- 等待主线程中的 MySQL 连接关闭。发生这种情况时,我正在向控制台写入日志 -
Destroying / ending master thread ID - 4984
- 检查您的 CPU 使用率,您会注意到其中一个 CPU 已达到 100%。
- 下一次运行,
strace -o log.txt -eepoll_ctl,epoll_wait -p 20584
. 请注意,您可能需要安装strace。此命令记录epoll_ctl, epoll_wait
Node.JS 进程进行的所有系统调用,并将其放入当前工作目录名为log.txt的文件中。 打开log.txt文件,您会注意到与这些类似的日志 -
epoll_wait(5, {{EPOLLIN|EPOLLHUP, {u32=16, u64=16}}}, 1024, 847) = 1 epoll_ctl(5, EPOLL_CTL_DEL, 16, 7ffe441aa850) = -1 EBADF (Bad file descriptor) epoll_wait(5, {{EPOLLIN|EPOLLHUP, {u32=16, u64=16}}}, 1024, 845) = 1 epoll_ctl(5, EPOLL_CTL_DEL, 16, 7ffe441aa850) = -1 EBADF (Bad file descriptor) epoll_wait(5, {{EPOLLIN|EPOLLHUP, {u32=16, u64=16}}}, 1024, 843) = 1 epoll_ctl(5, EPOLL_CTL_DEL, 16, 7ffe441aa850) = -1 EBADF (Bad file descriptor)
这里的文件描述符是16,如果你把它和你之前的
ls -l /proc/20584/fd
and关联起来lsof | grep 2467212
,你会发现这属于刚刚关闭的 MySQL 连接。
这让我相信在某个地方,即使与 MySQL 的连接被释放,也有一个文件描述符挂在那里,它仍在使用中。我在论坛上发现了各种类似问题的主题 -