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的可以退散了。


3 回复

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连接),导致操作系统无法分配新的文件描述符。

示例代码及修改建议
  1. 检查和限制并发连接数

    你可以通过限制同时进行的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();
      }
    });
    
  2. 使用连接池

    另一种方法是使用连接池来管理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客户端连接而不关闭它们,那么即使总的文件描述符限制很高,你仍然可能会遇到这种错误。

解决方案

  1. 重用Redis客户端:不要为每次请求都创建一个新的Redis客户端实例,而是创建一个全局的Redis客户端实例,并在应用的生命周期内重用它。这样可以减少文件描述符的消耗。

  2. 正确关闭连接:确保在不再需要Redis客户端时,显式地调用client.quit()来关闭连接。

  3. 优化连接管理:如果你确实需要为每个请求创建不同的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错误。

回到顶部