从node.js,更快,shell grep或fs.readFile?

我有一个很长的node.js进程,我需要扫描日志文件的模式。 我至less有两个明显的select: 产生一个grep进程或使用fs.read *读取文件,并parsingnode.js中的缓冲区/stream。 我没有发现两种方法比较intarwebs。 我的问题是双重的:

  1. 哪个更快?
  2. 为什么我更喜欢一种技术而不是另一种呢?

这里是我的nodejs实现,结果几乎如预期的那样:小文件的运行速度比分叉的grep快(文件长达2-3k短文件),大文件运行速度较慢。 文件越大,差异越大。 (也许正则expression式越复杂,差异越小 – 见下文。)

我用我自己的qfgets包快速的一次性文件I / O; 那里可能会有更好的,我不知道。

我看到一个意外的exception,我没有调查:下面的时间是对于常量stringregexp /foobar/ 。 当我将其改为/[f][o][o][b][a][r]/来实际运用正则expression式引擎时,grep减慢了3倍,而节点加速了! 在命令行中,grep的3倍减速是可重现的。

 filename = "/var/log/apache2/access.log"; // 2,540,034 lines, 187MB //filename = "/var/log/messages"; // 25,703 lines, 2.5MB //filename = "out"; // 2000 lines, 188K (head -2000 access.log) //filename = "/etc/motd"; // 7 lines, 286B regexp = /foobar/; child_process = require('child_process'); qfgets = require('qfgets'); function grepWithFs( filename, regexp, done ) { fp = new qfgets(filename, "r"); function loop() { for (i=0; i<40; i++) { line = fp.fgets(); if (line && line.match(regexp)) process.stdout.write(line); } if (!fp.feof()) setImmediate(loop); else done(); } loop(); } function grepWithFork( filename, regexp, done ) { cmd = "egrep '" + regexp.toString().slice(1, -1) + "' " + filename; child_process.exec(cmd, {maxBuffer: 200000000}, function(err, stdout, stderr) { process.stdout.write(stdout); done(err); }); } 

考试:

 function fptime() { t = process.hrtime(); return t[0] + t[1]*1e-9 } t1 = fptime(); if (0) { grepWithFs(filename, regexp, function(){ console.log("fs done", fptime() - t1); }); } else { grepWithFork(filename, regexp, function(err){ console.log("fork done", fptime() - t1); }); } 

结果:

 /** results (all file contents memory resident, no disk i/o): times in seconds, best run out of 5 /foobar/ fork fs motd .00876 .00358 0.41 x 7 lines out .00922 .00772 0.84 x 2000 lines messages .0101 .0335 3.32 x 25.7 k lines access.log .1367 1.032 7.55 x 2.54 m lines /[f][o][o][b][a][r]/ access.log .4244 .8348 1.97 x 2.54 m lines **/ 

(上面的代码都是一个文件,我把它分开以避免滚动条)

编辑:突出关键的结果:

185MB,254万行,searchRegExp / [f] [o] [o] [b] [a] [r] /:

grepWithFs

经过:.83秒

grepWithFork

经过:.42秒

为了回答这个问题,我写了这个小程序。

 #!/usr/local/bin/node 'use strict'; var fs = require('fs'); var log = '/var/log/maillog'; var fsOpts = { flag: 'r', encoding: 'utf8' }; var wantsRe = new RegExp(process.argv[2]); var handleResults = function (err, data) { console.log(data); }; var grepWithFs = function (file, done) { fs.readFile(log, fsOpts, function (err, data) { if (err) throw (err); var res = ''; data.toString().split(/\n/).forEach(function (line) { if (wantsRe && !wantsRe.test(line)) return; res += line + '\n'; }); done(null, res); }); }; var grepWithShell = function (file, done) { var spawn = require('child_process').spawn; var res = ''; var child = spawn('grep', [ '-e', process.argv[2], file ]); child.stdout.on('data', function (buffer) { res += buffer.toString(); }); child.stdout.on('end', function() { done(null, res); }); }; for (var i=0; i < 10; i++) { // grepWithFs(log, handleResults); grepWithShell(log, handleResults); } 

然后,我交替地在循环10x中运行这两个函数,并测量他们花费的时间来表示我的用例的日志文件的结果:

 $ ls -alh /var/log/maillog -rw-r--r-- 1 root wheel 37M Feb 8 16:44 /var/log/maillog 

文件系统是一对镜像SSD,通常足够快而不是瓶颈。 结果如下:

grepWithShell

 $ time node logreader.js 3E-4C03-86DD-FB6EF real 0m0.238s user 0m0.181s sys 0m1.550s 

grepWithFs

 $ time node logreader.js 3E-4C03-86DD-FB6EF real 0m6.599s user 0m5.710s sys 0m1.751s 

不同的是巨大的。 使用shell grep过程显着加快。 正如Andras指出的那样,节点的I / O可能会非常棘手,而且我没有尝试任何其他的fs.read *方法。 如果有更好的方法,请指出(最好使用类似的testing场景和结果)。

分叉grep更简单,更快,grep运行速度更快,使用更less的cpu。 虽然fork有一个比较高的开销(远远多于打开一个文件),但是你只需要fork一次并且输出结果。 另外,从节点的文件I / O中获得良好的性能可能会非常棘手。