Nodejs:如何优化编写许多文件



我在Windows上的Node环境中工作。我的代码每秒接收 30 个Buffer对象(每个对象 ~500-900kb),我需要尽快将此数据保存到文件系统,而不从事任何阻止接收以下Buffer的工作(即目标是保存来自每个缓冲区的数据,~30-45 分钟)。就其价值而言,数据是来自 Kinect 传感器的顺序深度帧。

我的问题是:在 Node 中写入文件的性能最高的方法是什么?

下面是伪代码:

let num = 0
async function writeFile(filename, data) {
fs.writeFileSync(filename, data)
}
// This fires 30 times/sec and runs for 30-45 min
dataSender.on('gotData', function(data){
let filename = 'file-' + num++
// Do anything with data here to optimize write?
writeFile(filename, data)
}

fs.writeFileSync似乎比fs.writeFile快得多,这就是我在上面使用它的原因。但是,是否有其他方法可以对数据进行操作或写入文件,从而加快每次保存的速度?

首先,您永远不希望在处理实时请求时使用fs.writefileSync(),因为这会阻止整个节点.js事件循环,直到文件写入完成。

好的,基于将每个数据块写入不同的文件,您希望允许同时进行多个磁盘写入,但不允许无限的磁盘写入。 因此,使用队列仍然是合适的,但这次队列不仅一次只有一个写入进程,而且同时具有一定数量的写入进程:

const EventEmitter = require('events');
class Queue extends EventEmitter {
constructor(basePath, baseIndex, concurrent = 5) {
this.q = [];
this.paused = false;
this.inFlightCntr = 0;
this.fileCntr = baseIndex;
this.maxConcurrent = concurrent;
}
// add item to the queue and write (if not already writing)
add(data) {
this.q.push(data);
write();
}
// write next block from the queue (if not already writing)
write() {
while (!paused && this.q.length && this.inFlightCntr < this.maxConcurrent) {
this.inFlightCntr++;
let buf = this.q.shift();
try {
fs.writeFile(basePath + this.fileCntr++, buf, err => {
this.inFlightCntr--;
if (err) {
this.err(err);
} else {
// write more data
this.write();
}
});
} catch(e) {
this.err(e);
}
}
}
err(e) {
this.pause();
this.emit('error', e)
}
pause() {
this.paused = true;
}
resume() {
this.paused = false;
this.write();
}
}
let q = new Queue("file-", 0, 5);
// This fires 30 times/sec and runs for 30-45 min
dataSender.on('gotData', function(data){
q.add(data);
}
q.on('error', function(e) {
// go some sort of write error here
console.log(e);
});

需要考虑的事项:

  1. 试验传递给队列构造函数的concurrent值。 从值 5 开始。 然后看看提高该值是否会给您带来更好或更差的性能。 节点.js文件 I/O 子系统使用线程池来实现异步磁盘写入,因此有最大数量的并发写入,因此将并发数提高到非常高可能不会使事情进展得更快。

  2. 在启动节点应用之前,可以通过设置UV_THREADPOOL_SIZE环境变量来增加磁盘 I/O 线程池的大小.js体验。

  3. 您最大的朋友是磁盘写入速度。 因此,请确保您有一个带有良好磁盘控制器的快速磁盘。 快速总线上的快速 SSD 是最好的。

  4. 如果可以将写入分散到多个实际物理磁盘上,则可能还会增加写入吞吐量(工作时有更多的磁盘磁头)。


这是基于对问题的最初解释(在编辑更改它之前)的先前答案。

由于您似乎需要按顺序进行磁盘写入(全部写入同一文件),因此我建议您使用写入流并让流对象为您序列化和缓存数据,或者您可以自己创建一个队列,如下所示:

const EventEmitter = require('events');
class Queue extends EventEmitter {
// takes an already opened file handle
constructor(fileHandle) {
this.f = fileHandle;
this.q = [];
this.nowWriting = false;
this.paused = false;
}
// add item to the queue and write (if not already writing)
add(data) {
this.q.push(data);
write();
}
// write next block from the queue (if not already writing)
write() {
if (!nowWriting && !paused && this.q.length) {
this.nowWriting = true;
let buf = this.q.shift();
fs.write(this.f, buf, (err, bytesWritten) => {
this.nowWriting = false;
if (err) {
this.pause();
this.emit('error', err);
} else {
// write next block
this.write();
}
});
}
}
pause() {
this.paused = true;
}
resume() {
this.paused = false;
this.write();
}
}
// pass an already opened file handle
let q = new Queue(fileHandle);
// This fires 30 times/sec and runs for 30-45 min
dataSender.on('gotData', function(data){
q.add(data);
}
q.on('error', function(err) {
// got disk write error here
});

您可以使用 writeStream 而不是这个自定义队列类,但问题是 writeStream 可能会填满,然后您必须有一个单独的缓冲区作为放置数据的地方。 像上面一样使用您自己的自定义队列可以同时解决这两个问题。

其他可伸缩性/性能注释

  1. 由于您似乎将数据串行写入同一文件,因此磁盘写入不会从群集或并行运行多个操作中受益,因为它们基本上必须序列化。

  2. 如果您的节点.js服务器除了执行这些写入之外还有其他事情要做,那么创建第二个节点.js进程并在另一个进程中执行所有磁盘写入可能会有一点优势(必须通过测试来验证)。 您的主节点.js进程将接收数据,然后将其传递给将维护队列并执行写入的子进程。

  3. 您可以尝试的另一件事是合并写入。 当队列中有多个项目时,可以将它们合并为一个写入。 如果写入已经很大,这可能没有太大区别,但如果写入很小,这可能会产生很大的不同(将大量小磁盘写入合并为一个较大的写入通常更有效)。

  4. 您最大的朋友是磁盘写入速度。 因此,请确保您有一个带有良好磁盘控制器的快速磁盘。 快速的SSD将是最好的。

我已经编写了一个广泛执行此操作的服务,您可以做的最好的事情是将输入数据直接传输到文件(如果您有输入流)。 一个简单的示例,您可以通过以下方式下载文件:

const http = require('http')
const ostream = fs.createWriteStream('./output')
http.get('http://nodejs.org/dist/index.json', (res) => {
res.pipe(ostream)                                                                                                                                                                                              
})
.on('error', (e) => {
console.error(`Got error: ${e.message}`);
})

因此,在此示例中,不涉及整个文件的中间复制。当文件从远程 http 服务器分块读取时,它会写入磁盘上的文件。这比从服务器下载整个文件,将其保存在内存中,然后将其写入磁盘上的文件要高效得多。

流是 Node 中许多操作的基础.js因此您也应该研究这些操作。

根据您的方案,您应该调查的另一件事是UV_THREADPOOL_SIZE因为 I/O 操作使用 libuv 线程池,默认情况下设置为 4,如果您进行大量写入,您可能会填满它。