Node.js读取ClickHouse触发JavaScript heap out of memory排查
ClickHouse读取OOM问题排查方案
核心结论
你怀疑的「HTTP响应未正常流式传输、全量读入内存」是该故障的最高概率诱因。
你测试的两款第三方Node.js ClickHouse驱动(@makeomatic/clickhouse、@apla/clickhouse)均为长期未迭代的社区旧版实现,默认未开启HTTP响应流式解析,会在驱动层将服务端返回的完整结果先缓冲到内存中再做结构化解析——哪怕你业务层写了1000条/块的分块处理逻辑,底层已经在持续堆积原始响应数据。换MySQL驱动可正常跑完的原因是,主流Node.js MySQL驱动默认实现了MySQL协议层的逐行流式解析,不会全量缓冲结果集,和数据库本身性能无关。
读取25M条数据正常的原因也很简单:2个数值列的25M条记录总大小(含解析临时对象)未触及Node.js默认堆内存阈值(通常1.4G~2G),驱动攒完全量数据仍未触发OOM;100M条数据读取到7M条阶段时,缓冲的原始数据+解析产生的临时对象总大小刚好撞堆满内存阈值,GC无法回收被驱动持有的缓冲对象,直接抛出heap out of memory错误。
分步排查流程
第一步:验证驱动层缓冲问题
直接用Node.js原生http模块手写查询请求,手动监听response对象的data事件逐块接收数据,不经过任何第三方驱动封装,跑相同的100M条查询:- 如果该场景下内存稳定无持续上涨,可100%定位为第三方驱动默认配置问题
- 注意查询时不要使用
FORMAT JSON返回格式,该格式要求拿到完整结果才能完成JSON解析,天然不支持流式,替换为FORMAT JSONEachRow或FORMAT RowBinary逐行格式;同时在查询参数中添加wait_end_of_query=0、buffer_size=1000,避免服务端攒批返回 - 检查驱动是否默认开启了gzip压缩但使用全量解压逻辑:很多旧驱动请求时带了
Accept-Encoding: gzip头,但会等完整压缩包接收完成后再一次性解压,会直接导致内存暴涨
第二步:排查业务层隐性内存持有
启动Node.js进程时添加--inspect --expose-gc --trace-gc参数:- 用Chrome DevTools连接调试端口,在内存上涨阶段拍堆快照:如果占内存最高的对象是
Buffer类型、原始响应字符串,可确认是驱动层缓冲问题;如果是你业务逻辑中定义的数组、对象占比最高,检查分块处理逻辑是否存在将已处理记录持续写入全局变量、闭包变量未释放的问题(比如为了统计误将所有记录push到全局数组) - 观察GC日志:如果每次Full GC后老年代内存仍持续线性上涨无回落,说明存在代码层面的内存泄漏;如果内存是阶梯式突增、到阈值直接崩溃,基本是大缓冲一次性读入导致的
- 用Chrome DevTools连接调试端口,在内存上涨阶段拍堆快照:如果占内存最高的对象是
第三步:校验服务端配置
检查查询语句和服务端配置是否存在强制攒批的设置:- 确认查询未设置过大的
max_block_size参数,避免服务端单批返回数据量过大 - 确认
result_overflow_mode参数为默认值,不会强制服务端缓存全量结果 - 关闭查询的
use_cursor等非官方兼容参数,这类参数很多旧驱动实现有问题,会触发全量结果缓存
- 确认查询未设置过大的
快速修复验证
直接替换为官方维护的@clickhouse/client驱动,该驱动默认开启HTTP流式响应、逐行解析,使用和之前完全一致的1000条分块逻辑跑100M条查询,如果可正常跑完,即可坐实旧驱动的实现缺陷。
内容的提问来源于stack exchange,提问作者Kudlicka Tomas
相关产品推荐
相关产品推荐

