首页 > 解决方案 > 为什么流式传输响应比在 Node.js / Express 中发送常规响应慢得多?

问题描述

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

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.3 MB 文件。所以,我尝试用更传统的方式做事:

// 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.jsexpressnode-streams

解决方案


我遇到了同样的问题,似乎原因与 node-pg 无关,而是与 Node Streams 工作流程有关。问题在这里描述:NodeJS Copying File over a stream is very slow

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

Writable 和 Readable 流都将数据存储在内部缓冲区中,可以分别使用 writable.writableBuffer 或 readable.readableBuffer 检索。

可能缓冲的数据量取决于传递给流的构造函数的 highWaterMark 选项。对于普通流,highWaterMark 选项指定总字节数。对于在对象模式下运行的流,highWaterMark 指定对象的总数....

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

根据此处的 QueryStream 构造函数定义:https ://github.com/brianc/node-postgres/blob/master/packages/pg-query-stream/src/index.ts#L24

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

new QueryStream(query, [], {highWaterMark: 1000});

或者在你的情况下:

const stream = client.query(new QueryStream(q.text, q.values, {highWaterMark: 1000}));

此外,您应该确保没有其他通过它管道传输的流具有highWaterMark可能导致进程减慢的较低值。

对我来说,这提高了速度,与直接从数据库下载一样快。


另外,我发现在低性能 CPU 下,我的流仍然太慢。就我而言,问题是 Express 的 compression() 使用 gzip 压缩响应。我在反向代理端(traefik)设置了压缩,一切运行良好。


推荐阅读