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

时间:2015-02-08 23:43:48

标签: node.js shell grep

我有一个运行时间很长的node.js进程,我需要扫描日志文件中的模式。我至少有两个明显的选择:spawn a grep process或使用fs.read*读取文件并解析node.js中的缓冲区/流。我还没有在intarwebs上找到两种方法的比较。我的问题有两个:

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

3 个答案:

答案 0 :(得分:12)

这是我的nodejs实现,结果非常符合预期: 小文件运行速度比分叉grep(文件长达2-3k短线)快, 大文件运行速度较慢。文件越大,差异越大。 (也许正则表达式越复杂,差异越小 - 见 下文)。

我使用自己的qfgets包快速 一次一行文件i / o;那里可能有更好的,我不知道。

我看到一个意外的异常,我没有调查:下面的时间 用于常量字符串regexp /foobar/。当我把它改成 /[f][o][o][b][a][r]/实际运用正则表达式引擎,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万行,搜索RegExp / [f] [o] [o] [b] [a] [r] /:

grepWithFs

已过去:.83秒

grepWithFork

已过去:.42秒

答案 1 :(得分:3)

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

#!/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

文件系统是一对镜像固态硬盘,它们通常足够快,不会成为瓶颈。结果如下:

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 *方法。如果有更好的方法,请指出(最好使用类似的测试方案和结果)。

答案 2 :(得分:0)

分支grep更简单,更快,grep很可能运行得更快并且使用更少的cpu。尽管fork具有适度高的开销(远远超过打开文件),但您只需分叉一次并传输结果。此外,从节点的文件i / o中获得良好的性能可能很棘手。