【问题标题】:From node.js, which is faster, shell grep or fs.readFile?从 node.js,哪个更快,shell grep 或 fs.readFile?
【发布时间】:2015-04-08 15:48:36
【问题描述】:

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

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

【问题讨论】:

  • 为什么不尝试两者并进行比较?
  • 一般来说,答案是:您觉得舒服并且足够快
  • 如果您追求大文件或大文件树的纯速度,请尝试执行 Silver Searcher ag。比grep快很多,和手写的javascript应该很难匹配

标签: node.js shell grep


【解决方案1】:

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

#!/usr/local/bin/node
'use strict';

const fs = require('fs');
const log = '/var/log/maillog';
const fsOpts = { flag: 'r', encoding: 'utf8' };
const wantsRe = new RegExp(process.argv[2]);

function handleResults (err, data) {
    console.log(data);
}

function grepWithFs (file, done) {
    fs.readFile(log, fsOpts, function (err, data) {
        if (err) throw (err);
        let res = '';
        data.toString().split(/\n/).forEach(function (line) {
            if (wantsRe && !wantsRe.test(line)) return;
            res += line + '\n';
        });
        done(null, res);
    });
};

function grepWithShell (file, done) {
    const spawn = require('child_process').spawn;
    let res = '';

    const 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 (let i=0; i < 10; i++) {
    // grepWithFs(log, handleResults);
    grepWithShell(log, handleResults);
}

然后我在循环中交替运行这两个函数 10 倍,并测量它们从代表我的用例的日志文件中 grep 结果所用的时间:

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

【讨论】:

    【解决方案2】:

    这是我的 nodejs 实现,结果和预期的差不多: 小文件比分叉的 grep 运行得更快(文件最多 2-3k 短行), 大文件运行速度较慢。文件越大,差异越大。 (也许正则表达式越复杂,差异越小——见 下面。)

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

    我看到了一个我没有调查的意外异常:以下时间 用于常量字符串正则表达式/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

    经过:0.83 秒

    grepWithFork

    经过:0.42 秒

    【讨论】:

    • i&lt;40 中的 40 是多少,这是否意味着它只读取前 40 行?
    【解决方案3】:

    派生一个 grep 更简单、更快捷,而且 grep 很可能运行得更快并且使用更少的 CPU。尽管 fork 的开销适中(比打开文件要多得多),但您只需 fork 一次并流式传输结果。另外,从节点的文件 i/o 中获得良好的性能可能会很棘手。

    【讨论】:

      猜你喜欢
      • 2014-04-16
      • 2010-11-10
      • 2012-10-03
      • 2011-02-15
      • 2012-07-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多