PAYATHON 2026

Node.js 为什么需要在内核缓冲之外再设置自己的缓冲?

数据小许

为什么还需要用户态缓冲?

Node.js 的缓冲和内核缓冲都占用 RAM,有时还会暂存同一批数据,但它们解决的问题不同。即使内核已有缓存,也不意味着应用能直接使用其中的内存完成所有处理。

应用仍需要读取、解析和转换数据,还要协调生产者和消费者的速度。用户态缓冲为这些操作提供了可访问的数据和等待空间。JVM 等运行时也有类似需求,具体实现取决于所用的 I/O API。

page cache 和应用缓冲分别负责什么?

以 Linux 上普通的文件读取为例,数据路径可以简化为:

存储设备 → 内核 page cache → 用户态缓冲 → 应用处理

page cache 缓存文件内容,让后续读取有机会直接从 RAM 获取数据,减少访问存储设备的次数。使用普通的 read 一类接口时,内核通常仍需将数据复制到调用方提供的用户态缓冲中,应用才能通过该接口访问数据。

Node.js 中的 Buffer 可以承载这些字节,流也可能将尚未消费的数据保存在内部队列中。Buffer 是字节存储对象;缓冲机制则涉及数据何时读取、排队和交付。

网络 I/O 的底层机制有所不同,通常涉及 socket 的发送和接收缓冲,不能统一用文件的 page cache 来解释。

额外缓冲有什么实际作用?

如果应用每次只处理几个字节,每次都向内核发起读取,就可能产生大量系统调用。一次读取较大的数据块,再在用户态逐步处理,可以分摊这些开销。即使数据已经在 page cache 中,系统调用仍有成本。

消息、字符编码序列或压缩数据可能跨越多次读取。应用需要保留前一次读取的剩余内容,等后续数据到达后继续处理。内核通常不知道这些应用层边界。

文件读取可能快于网络发送,网络接收也可能快于解析。缓冲可以容纳短暂的速度差;如果消费者持续跟不上,则需要背压限制上游继续生产。两者需要配合,单纯增加缓冲只能延后内存压力出现的时间。

这是否会浪费 RAM?

额外缓冲有成本,包括内存占用、数据复制,以及可能增加的等待时间。缓冲大小并非越大越好。

各层内存的回收方式也不同。干净的文件 page cache 通常可以在内存紧张时被内核回收;仍被应用引用的用户态数据,需要由应用或运行时释放。两者都占用 RAM,但不能因此认为它们可以互相替代。

如果应用一直把读取结果放进数组,即使底层流支持背压,内存也仍然可能持续增长。

在 Node.js 中应该怎么处理?

先确认数据是否需要全部驻留内存。如果任务允许分块处理,可以使用流,并让背压沿处理链传递。例如,复制文件可以写成:

const { createReadStream, createWriteStream } = require('node:fs');
const { pipeline } = require('node:stream/promises');

async function main() {
  await pipeline(
    createReadStream('input.dat'),
    createWriteStream('output.dat')
  );
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

这个示例使用流传输文件,避免应用主动把整个文件收集到一个 Buffer 中。pipeline 会协调流之间的背压,并处理传输过程中的错误。

如果手动向 Writable 写入数据,write() 返回 false 时,应等待 drain 事件再继续写入。调整 highWaterMark 前,应先观察吞吐量、内存占用和延迟。它是触发背压的阈值,不能当作整个进程的内存硬上限。

能否直接使用内核中的数据?

某些场景可以减少复制或绕过部分用户态缓冲。例如,mmap 可以将文件映射到进程地址空间;sendfile 等机制可以在适用的文件到 socket 传输场景中减少数据复制。这些方式需要相应的操作系统和 API 支持,也各有适用条件。

如果应用需要解析、修改或转换内容,仍然需要访问数据并维护处理状态。是否能减少某一层缓冲,要根据具体数据路径判断,不能仅凭内核已经有缓存就得出结论。

备注:内容仅供参考。