返回

NodeJS 进程内存占用分析与调试全攻略

Linux

NodeJS 进程内存占用分析与调试

最近遇到一个棘手的问题:NodeJS 程序内存占用过高。 我用 process.memoryUsage() 获取到的内存信息如下:

RSS       Total     Used      External
248.5 MB  47.54 MB  33.61 MB  5.06 MB

我知道 RSS (Resident Set Size) 是进程占用的总内存,包括堆、栈和其他一些结构。 堆内存里是各种动态对象,其他还有代码段、栈空间等等。

我比较清楚堆内存里有什么,因为程序是我自己写的。我想弄明白的是,除了堆内存,那剩下的 200MB 左右被什么占用了。

为啥这么关心这个问题呢? 因为上面这组数据是从生产环境拿到的。 同一个程序,在本地跑的时候,除堆以外的内存占用大概只有 40MB。 所以,我怀疑线上环境是不是有什么猫腻。

想在不影响进程运行的前提下,看看能不能查出, 或者大概知道那些内存是被谁占用了。 最好是能用一些操作系统的工具。

问题原因分析

NodeJS 程序的内存占用,除了程序本身的数据,还可能受到以下因素影响:

  1. NodeJS 自身的开销: NodeJS 运行时本身也需要占用内存, 比如 V8 引擎、内置模块、事件循环等。

  2. 依赖的模块: 项目中引入的第三方模块,尤其是那些包含原生模块 (C/C++ addons) 的,可能会有额外的内存开销,且这部分开销不容易直接观察到。

  3. JIT 编译优化: V8 引擎为了提高执行效率,会对 JavaScript 代码进行即时编译 (JIT)。JIT 编译产生的优化代码和缓存等也会占用内存。

  4. 操作系统级别的内存碎片: 内存频繁地分配和释放,可能导致操作系统层面的内存碎片,虽然实际使用的内存不多,但进程占用的虚拟内存 (RSS) 看起来会很大。

  5. 外部资源 : 如果Node进程和外部有大量交互,比如文件句柄,或者网络句柄,这些也是占用内存的。

  6. 长时间运行的进程 :Node进程随着时间推移,申请内存会增加。

解决方案

下面我会介绍几种方法,用来分析 NodeJS 进程的内存使用情况。

1. 使用 process.memoryUsage() 获取详细信息

虽然我们已经使用了 process.memoryUsage(),但它可以提供更详细的信息:

setInterval(() => {
  const mem = process.memoryUsage();
  console.log(mem);
  // {
  //   rss: 49353192,          // 常驻集大小(Resident Set Size)
  //   heapTotal: 19992480,    // V8 引擎堆内存的总大小
  //   heapUsed: 6593824,      // 已经使用的堆内存大小
  //   external: 1280694,     // V8 引擎管理的、绑定到 Javascript 对象的 C++ 对象的内存使用量
  //   arrayBuffers: 9898      //  为 ArrayBuffers 和 SharedArrayBuffers 分配的内存量,也包含在 external 值中
  // }
}, 5000);

这个增强的日志输出每5秒一次内存使用量数据。你可以长时间观察 externalarrayBuffers 的变化,来协助定位那些不在v8堆内的内存部分。如果某个参数持续升高,往往代表程序有潜在问题。

2. 利用 pmap (Linux) 查看进程内存映射

pmap 是 Linux 下的一个命令行工具,它可以显示进程的内存映射。 通过 pmap,我们可以看到进程加载了哪些库,以及每个内存区域的大小。

  1. 获取 NodeJS 进程的 PID:

    ps aux | grep node
    
  2. 使用 pmap 查看内存映射 (以 PID 为 12345 为例):

    pmap -x 12345
    

如果想看更详细的信息,用 -XX 参数:

```bash
pmap -XX 12345
```

pmap -x的输出中,主要关注以下几列:

*   **Address:**  内存区域的起始地址。
*   **Kbytes:**  内存区域的大小 (KB)。
*   **RSS:**  实际驻留在物理内存中的大小 (KB)。
*   **Mapping:**  映射的文件名 (如果有的话),或者 [heap]、[stack] 等。

通过观察 pmap 的输出,你可以:

  • 查看加载的动态链接库 (.so): 如果发现某个陌生的库占用了很多内存,可以考虑是不是某个第三方模块引入的。
  • 查看 [heap] 和 [stack] 的大小: 这有助于你了解堆和栈的相对大小。
  • 查看一些大的、匿名的内存区域: 这部分内存通常不好直接判断用途,但至少提供了一些线索。
pmap 安全说明

pmap只是读取系统数据,正常使用没有安全隐患.

pmap 进阶
  1. pmap-XX参数输出比较繁杂,可以根据需要使用grep, sortawk 等工具进行过滤、排序和汇总:
pmap -XX <PID> | grep "^[0-9a-f]" | sort -k2 -n -r | head -n 20  #查看内存占用最多的20个映射

这条指令,先输出所有映射表行,然后按照第二列(大小)做数字倒序排序,最终只看最大的20条.
可以调整head -n的值改变输出行数。

  1. 结合watch实时监控
watch -n 5 'pmap -x <PID> | tail -n 1' # 每5秒输出进程内存汇总的最后一行.

3. 利用 gdb (GNU Debugger)

gdb 是一个强大的调试器,不仅可以用来调试 C/C++ 程序,也可以用来调试正在运行的 NodeJS 进程 (虽然不如调试原生代码那么方便)。

  1. 安装 gdb (如果还没有安装):

    # Ubuntu/Debian
    sudo apt-get install gdb
    
    # CentOS/Fedora/RHEL
    sudo yum install gdb
    
  2. 附加到 NodeJS 进程 (以 PID 为 12345 为例):

    sudo gdb -p 12345
    
  3. gdb 中查看内存信息:

    • 查看进程加载的库:

      info sharedlibrary
      
    • 查看内存映射 (类似于 pmap):

      info proc mappings
      
    • 查看特定内存地址的内容:

      x/16xb 0x7fxxxxxxxxxx  # 以十六进制显示从地址 0x7fxxxxxxxxxx 开始的 16 个字节
      
  4. 使用node的inspect功能
    通过向node进程发送信号,我们可以触发NodeJS的debugger,然后检查JS的堆栈信息.

    kill -SIGUSR1 <PID>
    

    这条指令向node进程发送SIGUSR1信号, 此时Node会启动一个debugger并等待客户端连接。然后在另外一个窗口运行:

    node inspect -p <PORT>
    

    其中,端口号可以在第一步node输出的信息里找到。连上debugger后,可以用heapdump等指令,对内存信息做进一步排查。
    注意生产环境的进程尽量避免开启长期debug,以免有安全问题.

gdb 可以提供一些底层的信息,但对于 NodeJS 这种高级语言来说,解读起来比较困难。 它更适合于排查与原生代码 (C/C++ addons) 相关的问题。

GDB安全建议
  • gdb附加到进程通常需要 root 权限。请务必小心操作,避免对线上环境产生影响。
  • 调试完成后,记得用 detach 命令从进程分离。

4. 借助 NodeJS 内置的 inspector 模块

inspector 模块允许我们通过 Chrome DevTools 来调试 NodeJS 程序,就像调试前端代码一样方便。

  1. 启动 NodeJS 进程时开启 inspector:

    node --inspect index.js
    

    或者, 也可以指定调试端口

    node --inspect=9229 index.js
    

    如果想在程序开始运行就进入调试状态:

    node --inspect-brk index.js
    

    --inspect-brk会让程序停在第一行, 等待调试连接。

  2. 打开 Chrome 浏览器, 输入 chrome://inspect

  3. 在 "Remote Target" 下,应该能看到你的 NodeJS 进程,点击 "inspect" 链接。

  4. 在 DevTools 中进行调试:

    • Memory 标签页: 可以进行堆快照 (Heap Snapshot) 的拍摄和分析。 通过对比不同时间点的快照,可以找出内存泄漏的对象。
    • Profiler 标签页: 可以进行 CPU 性能分析,找出消耗 CPU 资源较多的函数。

使用 inspector 模块进行调试的好处是,它对 NodeJS 代码的调试支持非常好,可以直观地查看堆内存中的对象、函数调用栈等。

5.考虑换用 jemalloc

原生的glibc分配器在高负载情况下有性能缺陷。
我们可以尝试切换成jemalloc或者tcmalloc。这些更高级的内存分配器往往能减少碎片,提高性能.

安装:

#以ubuntu为例:
sudo apt install libjemalloc-dev

启动时链接jemalloc:

export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so
node index.js

注意具体的jemalloc路径在不同系统上可能不一样, 自己确认一下.

切换分配器可能有未知风险,建议在线下充分测试后再考虑用于线上.

总结

分析 NodeJS 进程的内存占用是一个复杂的问题,通常需要结合多种工具和方法。本文介绍了几种查看内存信息,debug 内存的方法, 希望可以帮你快速找到NodeJS进程的内存问题点.