Node.js HTTP 模块的内存泄露问题

开发 前端
setupConnectionsTracking 是追踪连接超时,回到我们的测试例子中可以发现,我们并没有执行 listen,也就是说,Server 对象并不会处理连接,那么也就没有连接需要追踪,所以修复方式就是把调用 setupConnectionsTracking 的时机延迟到 listen 成功时,修复代码大致如下。

很久没有逛社区了,晚上回来看了一下最近的情况,突然看到一个内存泄露问题,作为一个 APM 开发者,自然想分析其中的原因。

问题

下面介绍一下具体的问题。看一下 demo。

const http = require('http')

async function main () {
  let i = 0
  while (true) {
    if (i % 100 === 0) {
      global.gc()
    }

    if (i % 10000 === 0) {
      console.log(process.memoryUsage().heapUsed)
    }

    http.createServer((req, res) => {})
    i++
  }
}

main()

Node.js v20.3.1 下执行上面代码(node --expose-gc demo.js)输出如下。

2681120
11409488
19632792
28038016
36438104

可以看到内存不断在增长。下面来分析这个问题。

分析

const http = require('http');
const v8 = require('v8');

for (i = 0; i < 1000; i++) {
    http.createServer((req, res) => {});
}
v8.writeHeapSnapshot('memory-leaky.heapsnapshot');

采集的快照如下。

图片图片

可以看到,Server 对象没有被释放。看一下是谁引用了它。

图片图片

是定时器引用了 Server 对象,我们看一下定时器对象又是被谁引用了。

图片图片

有一个关键的变量 connectionsCheckingInterval,到 Node.js 源码里看一下,最终发现是 Server 初始化时创建的。

function Server(options, requestListener) {
  setupConnectionsTracking(this);
}

function setupConnectionsTracking(server) {
  server[kConnectionsCheckingInterval] = setInterval(checkConnections.bind(server), server.connectionsCheckingInterval).unref();
}

可以看到 checkConnections.bind 返回的匿名函数持有了 Server,而匿名函数又被 setInterval 持有了,所以导致 Server 对象无法释放。

修复

那么如何修复这个问题呢?修复这个问题,首先需要了解 setupConnectionsTracking 是做什么的,逻辑如下。

function checkConnections() {
  if (this.headersTimeout === 0 && this.requestTimeout === 0) {
    return;
  }

  const expired = this[kConnections].expired(this.headersTimeout, this.requestTimeout);

  for (let i = 0; i < expired.length; i++) {
    const socket = expired[i].socket;

    if (socket) {
      onRequestTimeout(socket);
    }
  }
}

可以看到,setupConnectionsTracking 是追踪连接超时,回到我们的测试例子中可以发现,我们并没有执行 listen,也就是说,Server 对象并不会处理连接,那么也就没有连接需要追踪,所以修复方式就是把调用 setupConnectionsTracking 的时机延迟到 listen 成功时,修复代码大致如下。

function Server(options, requestListener) {
  this.on('listening', () => {
    setupConnectionsTracking(this);
  });
}

修改源码重新编译后测试结果如下。

3653552
4002680
3753400
3762976
3773088

可以看到内存已经不会增长了,采集快照也可以看到不会再存在大量 Server 对象。

总结

这个例子虽然看起来有点不常见,用法也很怪异,但是从侧面说明了虽然 JS 自带 GC,但是因为逻辑 / 引用关系复杂,还是很容易出现内存泄露问题,所以写代码时还是需要注意,具体的 issue 可以参考 https://github.com/nodejs/node/issues/48604。

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2021-09-26 05:06:04

Node.js模块机制

2020-04-15 15:48:03

Node.jsstream前端

2019-12-17 11:40:44

Node.js模块前端

2017-03-20 13:43:51

Node.js内存泄漏

2017-03-19 16:40:28

漏洞Node.js内存泄漏

2022-01-02 06:55:08

Node.js ObjectWrapAddon

2020-01-03 16:04:10

Node.js内存泄漏

2021-10-03 15:02:50

HTTPNodejs

2014-09-12 10:35:09

Node.jsHTTP 206

2011-12-09 11:16:48

Node.js

2022-06-23 06:34:56

Node.js子线程

2011-09-08 14:07:28

Node.js

2013-11-01 09:34:56

Node.js技术

2015-03-10 10:59:18

Node.js开发指南基础介绍

2023-06-20 19:35:00

Node.js工具

2022-03-02 09:01:30

Node.js v1HTTPNode.j

2017-04-24 08:31:26

Node.jsExpress.jsHTTP

2021-01-26 08:07:44

Node.js模块 Async

2020-08-31 15:00:17

Node.jsrequire前端

2020-05-29 15:33:28

Node.js框架JavaScript
点赞
收藏

51CTO技术栈公众号