先记住这个答案
Writable.cork 让随后写入的数据暂存在流内,uncork 解除相应暂存层级,允许向底层处理。如果实现提供 _writev,可以把积累的多块数据批量交付,适合集中发生的小写入。多次 cork 需要匹配相应 uncork;end 也会处理剩余暂存写入,但不能用它代替仍要继续写入时的正常释放。暂存期间同样会积累缓冲并触发背压,若先等 drain 再 uncork,可能因为底层根本没开始消费而互相等待。
- 批量收益依赖底层是否支持合适的 writev
- 嵌套 cork 要配对解除
- 不要把 uncork 放在必须先等 drain 的后面
先让小写入形成一组,再解除暂存
连续向底层提交很小的数据可能产生额外调用开销,暂存后由 _writev 一次接收多块,可以给底层实现批处理机会。没有合适的批处理实现时,额外暂存反而可能增加等待,不能只因为调用了 cork 就宣布更快。
下面在同一个同步阶段写入两段短文本,再解除暂存。writev 记录收到的一组数据,测试可验证它们被一起交付;这只是自定义 Writable 的批处理行为,不是对文件系统或网络实际调用次数的基准测试。
import { Writable } from 'node:stream';
import { finished } from 'node:stream/promises';
export async function collectBatches() {
const batches: string[][] = [];
const sink = new Writable({
write(chunk, _encoding, callback) {
batches.push([chunk.toString()]);
callback();
},
writev(chunks, callback) {
batches.push(chunks.map(item => item.chunk.toString()));
callback();
}
});
sink.cork();
sink.write('A');
sink.write('B');
sink.uncork();
const done = finished(sink, { cleanup: true });
sink.end();
await done;
return batches;
}示例结果应包含一组 A、B,而不是把批量行为建立在不确定的网络分包上。真实程序还应限制每批规模、处理写入失败,并根据任务是否继续写入安排释放或结束。
嵌套调用与释放时机必须对应
多次 cork 会增加暂存计数,只调用一次 uncork 未必真正解除全部层级。封装工具时应明确哪一层负责解除,避免两个模块都认为对方会释放,导致缓冲持续增长却没有底层写入。
如果希望把当前事件循环阶段的多次写入归到一批,可以按文档建议安排稍后解除,但仍要限制批量范围。不要无限延后释放,也不要在已经 end 之后再继续 write;结束意味着生产者不再提交新数据。
背压等待不能挡住解除暂存
cork 后 write 返回 false,说明缓冲压力已达到阈值,但暂存若尚未解除,底层无法按预期消耗这些数据。若代码先等待 drain 才调用 uncork,就可能让恢复条件依赖一个永远排在等待之后的动作。
应先安排确定会执行的解除路径,再根据正常背压控制后续生产。异常分支也要处理暂存状态和流销毁,不能只为成功路径配对;批处理优化必须保持失败时仍能结束任务。
容易答错的地方
- 忘记与多次 cork 对应的解除次数
- 暂存计数没有归零时,底层可能继续等待。应让封装明确拥有配对责任,并检查异常路径和提前返回;不能只看代码里出现了一个 uncork 就认为所有暂存都已释放。
- 等 drain 后才准备 uncork
- 暂存本身可能阻止数据向底层流动,drain 的发生又依赖消费,顺序写反就造成等待环。应先保证解除动作不受这次等待阻塞,再控制下一批生产,而不是用更多定时器掩盖依赖问题。
面试官还会怎么问?
调用 end 会处理尚未 uncork 的数据吗?
Writable.end 会处理剩余暂存写入并结束生产,但它表示以后不再写入。如果业务还要继续产生数据,应正常安排 uncork,不能把 end 当作临时刷新按钮之后继续 write。
writev 是否保证一次系统调用写完全部数据?
不能这样推断。它向底层实现提供批量数据,具体如何调用操作系统、处理部分写入或网络发送由实现决定。验证自定义回调的批次与证明系统层性能,是两个不同范围的证据。
没有 writev 时还有必要使用 cork 吗?
需要具体评估,缺少有效批处理实现时可能只是增加缓冲与延迟。应先了解目标流的实现和负载,再对照吞吐与延迟,不要把 cork 当成所有 Writable 都应打开的通用开关。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。