为什么在Node.js/Express中,流式传输响应比发送常规响应慢得多?

为什么在Node.js/Express中,流式传输响应比发送常规响应慢得多?,node.js,express,node-streams,Node.js,Express,Node Streams,我正在使用Node.js/Express服务器查询Postgres数据库,并将结果作为CSV文件发送到浏览器。结果集可能会变得相当大(例如50+MB),因此我认为将结果直接从DB流式传输到浏览器是明智的做法,如下所示: const QueryStream = require('pg-query-stream'); const { Transform } = require('json2csv'); const pool = require('./pool-instance'); // ...

我正在使用Node.js/Express服务器查询Postgres数据库,并将结果作为CSV文件发送到浏览器。结果集可能会变得相当大(例如50+MB),因此我认为将结果直接从DB流式传输到浏览器是明智的做法,如下所示:

const QueryStream = require('pg-query-stream');
const { Transform } = require('json2csv');

const pool = require('./pool-instance');

// ...some request handling code...

const client = await pool.connect();
const stream = client.query(new QueryStream(q.text, q.values));

stream.on('end', () => {
  client.release();
});

const json2csv = new Transform({}, {objectMode: true});
res.set('Content-Type', 'text/csv');
res.set('Content-Disposition', 'attachment;filename=export.csv');

// pipe the query results to the Express response object. 
stream.pipe(json2csv).pipe(res);
这在本地测试时效果很好,但是当我在一台小型服务器上通过网络测试它时,花了超过20秒的时间来传输1.3MB的文件。所以,我试着用更传统的方式做事:

// Just load the full query results in memory
const results = await pool.query(q);

// Create the full csv text string from the query results
const csv = await parseAsync(results.rows);

res.set('Content-Type', 'text/csv');
res.set('Content-Disposition', 'attachment;filename=export.csv');

res.send(csv);
对于同一个文件,这只花了2秒钟


为什么会这样?为什么流式处理的速度要慢得多?

我遇到了同样的问题,原因似乎与node pg无关,而是与node Streams工作流有关。 此处描述了该问题:

在Node.js关于流缓冲的文档中,它说:

可写流和可读流都将数据存储在内部数据库中 可使用writable.writableBuffer或 readable.readableBuffer

潜在缓冲的数据量取决于highWaterMark 选项传递到流的构造函数中。对于正常流 highWaterMark选项指定总字节数。溪流 在对象模式下运行时,highWaterMark指定一个总数 物体的形状

stream API的一个关键目标,特别是stream.pipe()方法, 将数据缓冲限制在可接受的水平,以便 不同速度的来源和目的地不会压倒 可用内存

根据此处的QueryStream构造函数定义:

您可以将highWaterMark设置覆盖为更高的值(默认值为100)。 在这里,我选择了1000,但在某些情况下,您可能希望增加或减少该值。我建议您小心使用它,因为如果您在生产中运行它,可能会导致OOM问题

new QueryStream(query, [], {highWaterMark: 1000});
或者在您的情况下:

const stream = client.query(new QueryStream(q.text, q.values, {highWaterMark: 1000}));
此外,您应该确保与它一起传输的其他流没有较低的
highWaterMark
值,这可能会导致进程变慢

对我来说,这提高了从db直接下载的速度



此外,我发现在低性能CPU下,我的流仍然太慢。在我的例子中,问题是Express'compression()使用gzip压缩响应。我在反向代理端(traefik)设置了压缩,一切正常。

如果在流式代码中省略
json2csv
转换,结果如何?从节点postgres'
QueryStream
返回的流是一个对象流,因此,我必须将其转换为字符串,以便跳过csv转换并将其传输到
res
。当我这样做并跳过csv时,它仍然很慢-不到20秒。如果它起作用(我认为它不应该),我将使用
res.blob
在客户端使用响应,并使用它创建一个
objectURL
。您是如何测量这2秒的?您是否测量了客户实际接收全部内容的时间。如果您只是测量了
res.send()
运行的时间,那么这并不代表客户端接收它的实际时间,因为
res.send()
是异步的。它在内容全部发送之前返回。另外,您的两个示例看起来根本不像相似的代码。第一个似乎有一个对象模式转换流,第二个调用一些未知的
parseAsync()
函数。这些看起来不像可比较的代码路径。@jfriend00我是通过查看Chrome的DevTools(特别是瀑布)中的网络选项卡来衡量的
parseAsync
只是一个将json转换为CSV字符串的异步方法(来自同一个json2csv库)。结果是我创建了完整的字符串,并在第二条路径中一次性将其发送到浏览器。谢谢@Raphiki。我试过你的方法,它让我非常接近。然而,仍然有两个问题。(1) 因为这些是对象流,
highWaterMark
指的是对象的数量,所以4096太高了。(2) json2csv流的默认值
highWaterMark
为16,这与查询流有点不匹配。将两个流的
highWaterMark
设置为500就可以了。如果你相应地编辑你的答案,我会接受它。现在没有理由将它设置为2的幂值作为它的objectMode(我可以设置4000)。因此,在我的例子中,将
highWatermark
从100设置为8000可以加快处理速度。8000以上,我没有注意到任何差异。我相信这与所使用的脚本和运行它的系统高度相关。“我可以设置为1000,你觉得呢?500对我来说很好。”马克·麦凯维更新了我的帖子。另外,我添加了一个额外的部分,它在最后肯定解决了速度性能问题