Node.js:内存使用量持续增加

4
我们正在编写一个脚本,用于读取服务器上的一大批JPG文件(数量无限,因为我们还有另一个进程在同一目录下不断地生成JPG文件),并以固定时间间隔(变量“frameDelay”在下面的代码中)将它们作为MJPEG流发送到用户的浏览器。这类似于IP摄像机的工作方式。
我们发现该脚本的内存使用量不断增加,并最终被系统(Ubuntu)杀死。
我们已经多次检查了这个看似简单的脚本。因此,我在下面发布了代码。非常感谢您的任何评论/建议!
app.get('/stream', function (req, res) {
    res.writeHead(200, {
        'Content-Type':'multipart/x-mixed-replace;boundary="' + boundary + '"',
        'Transfer-Encoding':'none',
        'Connection':'keep-alive',
        'Expires':'Fri, 01 Jan 1990 00:00:00 GMT',
        'Cache-Control':'no-cache, no-store, max-age=0, must-revalidate',
        'Pragma':'no-cache'
    });
res.write(CRLF + "--" + boundary + CRLF);

setInterval(function () {
    if(fileList.length<=1){
        fileList = fs.readdirSync(location).sort();
    }else{
        var fname = fileList.shift();
        if(fs.existsSync(location+fname)){
           var data = fs.readFileSync(location+fname);
            res.write('Content-Type:image/jpeg' + CRLF + 'Content-Length: ' + data.length + CRLF + CRLF);
            res.write(data);
            res.write(CRLF + '--' + boundary + CRLF);                   
            fs.unlinkSync(location+fname);
        }else{
            console.log("File doesn't find")
        }
    }
        console.log("new response:" + fname);
    }, frameDelay);
});

app.listen(port);
console.log("Server running at port " + port);

为了方便故障排除,以下是一个独立的(无第三方库)测试用例。
它有完全相同的内存问题(内存使用持续增长并最终被操作系统杀掉)。
我们认为问题在 setInterval() 循环中 - 可能这些图像在发送后没有从内存中删除,或者仍然存储在变量“res”中。
非常感谢您提供任何反馈/建议!
var http                = require('http');
var fs                  = require('fs');

var framedelay  = 40;
var port                = 3200;
var boundary    = 'myboundary';
var CR                  = '\r';
var LF                  = '\n';
var CRLF                = CR + LF;

function writeHttpHeader(res)
{
        res.writeHead(200,
        {
                'Content-Type': 'multipart/x-mixed-replace;boundary="' + boundary + '"',
                'Transfer-Encoding': 'none',
                'Connection': 'keep-alive',
                'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT',
               'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
                'Pragma': 'no-cache',
        });

        res.write(CRLF + '--' + boundary + CRLF);
}

function writeJpegFrame(res, filename)
{
        fs.readFile('./videos-8081/frames/' + filename, function(err, data)
        {
                if (err)
                {
                        console.log(err);
                }
                else
                {
                        res.write('Content-Type:image/jpeg' + CRLF);
                        res.write('Content-Length:' + data.length + CRLF + CRLF);
                            res.write(data);
                        res.write(CRLF + '--' + boundary + CRLF);
                        console.log('Sent ' + filename);
                }
        });
}
http.createServer(function(req, res)
{
     writeHttpHeader(res)    
     fs.readdir('./videos-8081/frames', function(err, files)
     { 
         var i = -1;
         var sorted_files = files.sort();    
         setInterval(function()
         {
              if (++i >= sorted_files.length)
              {
                i = 0;
              }    
              writeJpegFrame(res, sorted_files[i]);
          }, framedelay);
        });
}).listen(port);
console.log('Server running at port ' + port);                    

你在客户端得到输出了吗?你使用的是哪个模块? - xiaoyi
你是想把 fileList 设置为全局变量(被所有 /stream 请求共享)吗?此外,你应该使用异步调用,否则它的可扩展性会很差。 - JohnnyHK
你所看到的是一个恒定、稳定的70帧每秒还是一些变量? - guillaume
感谢大家的快速反馈 - 非常感激。 - user1673454
主要问题是内存使用量。我编辑了原帖,加入了一个更简单的示例,不使用任何第三方库。 - user1673454
你尝试过查找内存泄漏吗? - guillaume
2个回答

4
当然会有内存泄漏。你需要注意。
 setInterval(...)

在每个请求中,你都会创建一个时间间隔,但是你从不清除这些时间间隔。这意味着,在(例如)20次请求后,你有20个间隔在后台运行,即使客户端/连接已经关闭,它们也会一直运行 解决方案之一如下:

var my_interval = setInterval(function() {
    try {
        // all your code goes here
    } catch(e) {
        // res.write should throw an exception once the connection is dead
        // do the cleaning now
        clearInterval( my_interval );
    }
}, frameDelay);

req.on( "close", function() {
    // just in case
    clearInterval( my_interval );
});

这样可以保证一旦连接关闭,my_interval ( 以及所有相关数据 ) 就会被清除。

附言:我建议使用 setTimeout 而不是 setInterval,因为加载文件可能需要比 frameDelay 更多的时间,这将导致问题。

附言2:请使用 fs 函数的异步版本。Node.JS 的全部优势在于非阻塞操作,您在这里失去了主要优势(性能)。


同时,fs.unlinkSync 应该真正成为一个 cron 作业。 - wayne

2

这几个原因导致了问题:

  • MJPEG不适合在高频率(例如25fps)下发送高分辨率动态图片。对于320x240的帧来说,25fps可能还行,但是对于720p就不行了。考虑一下输出吞吐量:25fps*70KB = 1750KBps = 14Mbps,这比全高清视频还要高。
  • 当客户端无法接收数据时,Node.js会将输出缓存到缓冲区中。由于你向客户端发送了太多数据,所以Node.js为你保存了它们。这就是为什么你的内存使用量从未下降,而且这并不是内存泄漏。要检测输出缓冲区是否被堆积,请检查res.write()的返回值。
  • setInterval()可以使用,并且只要客户端保持连接,就不会出现任何问题。但是当客户端断开连接时,你需要停止它。为此,你需要监视“close”事件。
  • 使用MJPEG不能保持稳定的fps,因为它不是为此目的设计的,所以无论你怎么努力,都无法控制客户端的fps。但是通过精心设计的代码,你可以使用setTimeout()使平均fps保持稳定。

这是修正后的代码。

var http = require('http');
var fs = require('fs');
var framedelay = 40;
var port = 3200;
var boundary = 'myboundary';
var CR = '\r';
var LF = '\n';
var CRLF = CR + LF;

http.createServer(function(req, res) {

  var files = fs.readdirSync('./imgs');
  var i = -1;
  var timer;
  var sorted_files = files.sort();

  res.writeHead(200, {
    'Content-Type': 'multipart/x-mixed-replace;boundary="' + boundary + '"',
    'Transfer-Encoding': 'none',
    'Connection': 'keep-alive',
    'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT',
    'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
    'Pragma': 'no-cache',
  });

  res.write(CRLF + '--' + boundary + CRLF);

  var writePic = function() {

    if (++i >= sorted_files.length)
      i = 0;

    var data = fs.readFileSync('./imgs/' + sorted_files[i]);
    res.write('Content-Type:image/jpeg' + CRLF);
    res.write('Content-Length:' + data.length + CRLF + CRLF);
    res.write(data);
    var ok = res.write(CRLF + '--' + boundary + CRLF);
    console.log('Sent ' + sorted_files[i], ok);

    if (ok)
      timer = setTimeout(writePic, framedelay);
  };

  res.on('close', function() {
    console.log('client closed');
    clearTimeout(timer);
  });

  res.on('drain', function() {
    console.log('drain');
    timer = setTimeout(writePic, framedelay);
  });

}).listen(port);

console.log('Server running at port ' + port);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接