来自node.js,哪个更快,shell grep 还是fs.readFile?
From node.js, which is faster, shell grep or fs.readFile?
我有一个很长的 运行 node.js 过程,我需要扫描日志文件以寻找模式。我至少有两个明显的选择:spawn a grep process or read the file using fs.read* 和解析 node.js 中的 buffer/stream。我还没有在 intarwebs 上找到这两种方法的比较。我的问题是双重的:
- 哪个更快?
- 为什么我可能更喜欢一种技术而不是另一种?
分叉 grep 更简单、更快捷,而 grep 很可能 运行 更快并且使用更少 cpu。尽管 fork 的开销相当高(比打开文件多得多),但您只会 fork 一次并流式传输结果。此外,从节点文件 i/o.
中获得良好的性能可能很棘手
为了回答这个问题,我写了这个小程序。
#!/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 进程要快得多。正如安德拉斯指出的那样,节点的 I/O 可能很棘手,我没有尝试任何其他 fs.read* 方法。如果有更好的方法,请指出(最好有类似的测试场景和结果)。
这是我的 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
已用:.83 秒
grepWithFork
已用:.42 秒
我有一个很长的 运行 node.js 过程,我需要扫描日志文件以寻找模式。我至少有两个明显的选择:spawn a grep process or read the file using fs.read* 和解析 node.js 中的 buffer/stream。我还没有在 intarwebs 上找到这两种方法的比较。我的问题是双重的:
- 哪个更快?
- 为什么我可能更喜欢一种技术而不是另一种?
分叉 grep 更简单、更快捷,而 grep 很可能 运行 更快并且使用更少 cpu。尽管 fork 的开销相当高(比打开文件多得多),但您只会 fork 一次并流式传输结果。此外,从节点文件 i/o.
中获得良好的性能可能很棘手为了回答这个问题,我写了这个小程序。
#!/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 进程要快得多。正如安德拉斯指出的那样,节点的 I/O 可能很棘手,我没有尝试任何其他 fs.read* 方法。如果有更好的方法,请指出(最好有类似的测试场景和结果)。
这是我的 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
已用:.83 秒
grepWithFork
已用:.42 秒