NodeJs的Redis客户端遇到failed - connect EMFILE错误
NodeJs的Redis客户端遇到failed - connect EMFILE错误
程序很简单,就是得到http请求后,用一个参数作为Key去blpop一个Redis实例,如果超时就关掉Redis客户端返回空字典的json,如果超时前得到了push的数据就返回数据,最后关掉Redis连接。 结果在Redis用info得到connected client数量才2000多的时候,Nodejs的redis客户端就开始连接出错,然后报
error:Error: Redis connection to xxx.xxx.xxx.xxx - connect EMFILE
events.js:72 throw er; // Unhandled ‘error’ event ^ Error: Redis connection to xxx.xxx.xxx.xxx:6379 failed - connect EMFILE at RedisClient.on_error (/var/www/ncodoon/message/broker/node_modules/redis/index.js:185:24) at Socket. (/var/www/ncodoon/message/broker/node_modules/redis/index.js:95:14) at Socket.EventEmitter.emit (events.js:95:17) at net.js:440:14 at process._tickCallback (node.js:415:13)
Redis所在服务器的ulimit -n 是102400,Nodejs所在服务器的也是这个数。离限制还早得很呢。所以叫改ulimit的可以退散了。
NodeJs的Redis客户端遇到failed - connect EMFILE
错误
现象描述
程序逻辑非常简单:接收到HTTP请求后,使用一个参数作为Key去执行Redis的BLPOP
操作。如果超时,则关闭Redis客户端并返回空字典的JSON;如果在超时前获取到数据,则返回该数据,并最终关闭Redis连接。
然而,在Redis客户端连接数达到约2000多个时,NodeJS应用开始出现连接错误,具体错误信息如下:
error: Error: Redis connection to xxx.xxx.xxx.xxx - connect EMFILE
错误详细信息如下:
events.js:72
throw er; // Unhandled 'error' event
^
Error: Redis connection to xxx.xxx.xxx.xxx:6379 failed - connect EMFILE
at RedisClient.on_error (/var/www/ncodoon/message/broker/node_modules/redis/index.js:185:24)
at Socket.<anonymous> (/var/www/ncodoon/message/broker/node_modules/redis/index.js:95:14)
at Socket.EventEmitter.emit (events.js:95:17)
at net.js:440:14
at process._tickCallback (node.js:415:13)
解决方案
尽管Redis服务器和NodeJS服务器的ulimit -n
设置为102400,远未达到上限,但问题依然存在。这通常是由于NodeJS应用打开了过多的文件描述符(包括TCP连接),导致操作系统无法分配新的文件描述符。
示例代码及修改建议
-
检查和限制并发连接数
你可以通过限制同时进行的Redis连接数来解决这个问题。例如,你可以使用一个队列来管理连接,确保不会超过一定数量的连接。
const redis = require('redis'); const Queue = require('async-lock').Lock; const lock = new Queue(); const MAX_CONCURRENT_CONNECTIONS = 100; // 最大并发连接数 function getRedisClient() { return new Promise((resolve, reject) => { lock.acquire('redis', async () => { try { const client = redis.createClient({ host: 'xxx.xxx.xxx.xxx', port: 6379, }); await new Promise(resolve => client.on('ready', resolve)); resolve(client); } catch (err) { reject(err); } }); }); } app.get('/your-endpoint', async (req, res) => { try { const client = await getRedisClient(); const data = await client.blpop('your-key', 10); // 设置超时时间为10秒 res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } finally { client.quit(); } });
-
使用连接池
另一种方法是使用连接池来管理Redis连接,这样可以更好地控制连接的数量。
const redis = require('redis'); const pool = require('generic-pool'); const poolConfig = { max: 100, // 最大连接数 min: 10, // 最小连接数 idleTimeoutMillis: 30000, // 连接空闲超时时间 }; const factory = { create: () => { return redis.createClient({ host: 'xxx.xxx.xxx.xxx', port: 6379, }); }, destroy: client => { client.quit(); }, validate: client => { return client.connected; }, }; const clientPool = pool.Pool(poolConfig, factory); app.get('/your-endpoint', async (req, res) => { try { const client = await clientPool.acquire(); const data = await client.blpop('your-key', 10); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } finally { clientPool.release(client); } });
通过以上方法,你可以有效地管理Redis连接,避免因连接过多而导致的EMFILE
错误。
这个错误貌似是文件描述符用完了
在Node.js中使用Redis客户端时遇到EMFILE
错误,通常是由于文件描述符(file descriptor)耗尽导致的。尽管你的系统设置允许打开更多文件描述符,但每个进程能够使用的文件描述符数是有限制的。如果你的应用程序频繁地创建新的Redis客户端连接而不关闭它们,那么即使总的文件描述符限制很高,你仍然可能会遇到这种错误。
解决方案
-
重用Redis客户端:不要为每次请求都创建一个新的Redis客户端实例,而是创建一个全局的Redis客户端实例,并在应用的生命周期内重用它。这样可以减少文件描述符的消耗。
-
正确关闭连接:确保在不再需要Redis客户端时,显式地调用
client.quit()
来关闭连接。 -
优化连接管理:如果你确实需要为每个请求创建不同的Redis客户端连接,可以考虑使用连接池来管理这些连接,以避免一次性创建过多的连接。
示例代码
以下是一个简单的示例,展示如何重用一个全局的Redis客户端:
const redis = require('redis');
const http = require('http');
// 创建一个全局的Redis客户端实例
const redisClient = redis.createClient();
http.createServer((req, res) => {
if (req.url === '/get-data') {
redisClient.blpop('your-list-key', 10, (err, reply) => {
if (err) {
console.error(err);
res.writeHead(500);
res.end(JSON.stringify({ error: 'Internal server error' }));
return;
}
if (!reply) {
res.writeHead(200);
res.end(JSON.stringify({}));
return;
}
res.writeHead(200);
res.end(JSON.stringify(reply));
});
} else {
res.writeHead(404);
res.end();
}
}).listen(3000);
// 在应用结束时关闭Redis客户端
process.on('SIGINT', () => {
redisClient.quit(() => {
process.exit();
});
});
在这个例子中,我们只创建了一个全局的Redis客户端实例,并在应用退出时正确地关闭了它。这有助于减少文件描述符的消耗,从而避免EMFILE
错误。