本节会重点分析内存和进程奔溃,并且会给出相应的监控方法。

  本系列所有的示例源码都已上传至Github,点击此处获取。

一、内存

  虽然在 Node.js 中并不需要手动的对内存进行分配和销毁,但是在开发中因为程序编写问题也会发生内存泄漏的情况。

  所以还是有必要了解一些 Node.js 开放的内存操作和常见的内存泄漏场景。

1)内存指标

  Node.js 项目在启动后(例如 node index.js),会创建一个服务进程。进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。

  程序在运行时会被分配一些内存空间,这个空间称为常驻内存(Resident Set),V8 会将内存分为几个段(也叫存储空间):

  • 代码(Code):存储可执行的代码。
  • 栈(Stack):存储原始类型的值(例如整数、布尔值等),以及对象的引用地址(指针)。
  • 堆(Heap):存储引用类型的值,例如对象、字符串和闭包。

  在下图中描绘了各个段,以及之间的关系。

  

  Node.js 提供了 process.memoryUsage() 方法,用于读取一个描述 Node.js 进程的内存使用量对象,所有属性值都以字节为单位。

  • rss:resident set size (常驻内存大小)的缩写,表示进程使用了多少内存(RAM中的物理内存),包括所有 C++ 和 JavaScript 对象和代码。
  • heapTotal:堆的总大小,包括不能分配的内存,例如在垃圾回收之前对象之间的内存碎片。
  • heapUsed:堆的使用量,已分配的内存,即堆中所有对象的总大小。
  • external:使用到的系统链接库所占用的内存,包含 C++ 模块的内存使用量。
  • arrayBuffers:为所有 Buffer 分配的内存,它被包含在 external 中。当 Node.js 被用作嵌入式库时,此值可能为 0,在这种情况下可能不会追溯 ArrayBuffer 的分配。

  下面的例子演示了本机的进程内存使用情况,默认都是字节,为了便于阅读,已将输出结果换算成 MB。

// 换算成 MB
function format (bytes) {
return (bytes / 1024 / 1024).toFixed(2) + 'MB';
};
// 进程的内存使用
const mem = process.memoryUsage(); // 单位 字节
// {
// rss: '20.05MB',
// heapTotal: '3.86MB',
// heapUsed: '3.02MB',
// external: '0.24MB',
// arrayBuffers: '0.01MB'
// }
console.log({
rss: format(mem.rss),
heapTotal: format(mem.heapTotal),
heapUsed: format(mem.heapUsed),
external: format(mem.external),
arrayBuffers: format(mem.arrayBuffers)
});

  在 os 模块中,有两个方法:freemem() 和 totalmem(),分别表示系统的空闲内存和总内存。

  以本机为例,电脑的内存是 16G,因此总内存也是这个数,而系统的空闲内存会动态变化。

const os = require('os');
// 系统的空闲内存
const freemem = os.freemem();
format(freemem); // 178.58MB
// 系统所有的内存
const totalmem = os.totalmem();
format(totalmem); // 16384.00MB = 16G

2)内存泄漏

  内存泄漏(memory leak)是计算机科学中的一种资源泄漏,主因是程序的内存管理失当,因而失去对一段已分配内存的控制。

  程序继续占用已不再使用的内存空间,或是存储器所存储对象无法透过执行代码而访问,令内存资源空耗。下面会罗列几种内存泄漏的场景:

  第一种是全局变量,它不会被自动回收,而是会常驻在内存中,因为它总能被垃圾回收器访问到。

  第二种是闭包(closure),当一个函数能够访问和操作另一个函数作用域中的变量时,就会构成一个闭包,即使另一个函数已经执行结束,但其变量仍然会被存储在内存中。

  如果引用闭包的函数是一个全局变量或某个可以从根元素追溯到的对象,那么就不会被回收,以后不再使用的话,就会造成内存泄漏。

  第三种是事件监听,如果对某个目标重复注册同一个事件,并且没有移除,那么就会造成内存泄漏,之前记录过一次这类内存泄漏的排查

  第四种是缓存,当缓存中的对象属性越来越多时,长期存活的概率就越大,垃圾回收器也不会清理,部分不需要的对象就会造成内存泄漏。

3)heapdump

  想要定位内存泄漏,可以使用快照工具(例如 heapdump、v8-profiler 等)导出内存快照,使用 DevTools 查看内存快照。

  在下面的示例中,会在全局缓存之前和之后导出一份内存快照。

const heapdump = require('heapdump');
// 内存泄漏前的快照
heapdump.writeSnapshot('prev.heapsnapshot');
// 全局缓存
const cached = [];
for(let i = 0; i < 10; i++)
cached.push(new Array(1000000));
// 内存泄漏后的快照
heapdump.writeSnapshot('next.heapsnapshot');

  得到文件后,打开 Chrome DevTools,选择 Memory =》Profiles =》Load 加载内存快照。

  默认是 Summary 视图,显示按构造函数名称分组的对象,如下图所示。

  

  视图中的字段包括:

  • Contructor:使用构造函数创建的对象,其中 (closure) 表示闭包。后面增加 * number 表示构造函数创建的实例个数。
  • Distance:到 GC 根元素的距离,距离越大,引用越深。
  • Shallow Size:对象自身的大小,即在 V8 堆上分配的大小,不包括它引用的对象。
  • Retained Size:对象自身的大小和它引用的对象的大小,即可以释放的内存大小。

  切换到 Comparison 视图,选择比较的内存快照(next.heapsnapshot),检查两者的数据差异和内存变化,如下图所示。

  

  如果 Delta 一直增长,那么需要特别注意,有可能发生了内存泄漏,视图中的所有字段说明如下所列:

  • # New:新建的对象个数。
  • # Deleted:删除的对象个数。
  • # Delta:发生变化的对象个数,净增对象个数。
  • Alloc.Size:已经分配的使用中的内存。
  • Freed Size:为新对象释放的内存。
  • Size Delta:可用内存总量的变化,上图中的数字是负数,说明可用内存变少了。
  • Containment 视图提供了一种从根元素作为入口的对象结构鸟瞰图,如下图所示。

  

  打开 GC roots =》 Isolate =》 Array 可以看到在代码中插入给 cached 数组的 10 个元素。

  

  要想能快速定位线上的内存泄漏,需要很多次的实践,知道字段含义仅仅是第一步。

  还需要在这么多信息中,定位到问题代码所在的位置,这才是监控地最终目的。

二、Core Dump

  Core Dump(核心转储)是操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写入一个磁盘文件中。

  在这个文件中包含内存分配信息 、堆栈指针等关键信息,对于诊断和分析程序异常非常重要,因为可以还原真实的案发现场。

1)lldb

  本机是 Mac OS,默认自带了 lldb 命令,先用此命令来加载和分析 Core Dump 文件。

  首先要在终端放开 Core Dump 文件的大小限制,这样才能成功生成,命令如下。

ulimit -c unlimited

  但是一开始怎么样都生成不了,查了 Mac 官方文档stackoverflow 等各种网络资料都无济于事。

  后面自己才不经意的发现,这个命名只有在当前终端才有效,换个终端或 Tab 页都将无效,白白浪费了 3 个小时。

  然后创建 error.js 文件,里面就写一段会报错的代码,例如读取 undefined 的属性。

const test = { };
setTimeout(() => {
console.log(test.obj.name);
}, 1000);

  接着在终端输入启动的命令,但是需要带上参数 --abort-on-uncaught-exception。

node --abort-on-uncaught-exception error.js

  代码运行完成后,Mac OS 就会在 /cores 目录中生成一个 core.[pid] 的文件,pid 就是当前进程的编号,通过 process.pid 也能读取到。

  在本地生成了一个 core.5889 文件,足足有 1.8G,怪不得不能随便生成,硬盘吃不消。最后输入 lldb 命令加载和分析文件。

lldb -c core.5889

  在加载成功成功后,会有一段提示。在最后一行需要手动输入 bt(backtrace)查看堆栈信息。

  

  上述是 C++ 的堆栈,可以看到 uv_run 开启事件循环,然后运行 uv__run_timers 阶段,接着就发生了错误,底层的错误内容看不大懂。

2)llnode

  这个 llnode 其实是 lldb 的一个插件,能还原 JavaScript 堆栈帧、对象、源代码等可读信息,类似于 Source Map 的功能。

  直接运行安装命令 npm install llnode 会报错,如下所示。

Reading lldb version...
xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance
Error: Command failed: xcodebuild -version
xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance

  查看官方文档,在 Mac OS 中,需要安装 LLDB 及其库或者直接安装 Xcode 并使用它附带的 LLDB,前者的命令如下。

brew install --with-lldb --with-toolchain llvm

  但是这条命令会报下面的错误,于是将 --with-lldb 参数去除。

Error: invalid option: --with-lldb

  再运行一次,持续了一个小时,才下载 32%,最后又是报错。

Error: invalid option: --with-toolchain

  无奈就想到去安装 Xcode,但是集成软件太大,要 10G多,于是选择 Command Line Tools (macOS 10.14) for Xcode 10.1,下载了 20 多分钟。

  安装完成后,还是无法下载 llnode 包,只得去下载 Xcode 10.1,又是 20 多分钟,Xcode_10.1.xip 是一个压缩包,需要解压。

  解压安装完成后,将当前目录的 Xcode 移动到应用程序目录,运行下面命令。

sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

  重新下载 llnode 包,这次终于不报错了,开始出现下面的提示。

Looking for llvm-config...
⠹ [0/1] Installing llnode@*No llvm-config found Reading lldb version...
⠼ [0/1] Installing llnode@*Deduced lldb version from Xcode version: Xcode 10.1 -> lldb 3.9
Installing llnode for lldb, lldb version 3.9 Looking for headers for lldb 3.9...
Could not find the headers, will download them later Looking for shared libraries for lldb 3.9...
Could not find the shared libraries
llnode will be linked to the LLDB shared framework from the Xcode installation

  因为没有全局安装 llnode,所以加载命令要加 npx,core.5889 加了绝对路径。

npx llnode -c /cores/core.5889

  下图是成功加载后的图,运行 v8 bt 命令后,并没有得到预期的堆栈信息。

  

  过程非常曲折,最后还是很遗憾没有成功解析,不知道是 lldb 的问题还是生成的文件问题,亦或是 Node 版本的问题。

  如果不想这么麻烦的解析,还可以直接使用成熟的 Node.js 性能平台,也有 Coredump 文件分析,并且做了深度定制,能更清晰地看到错误源码。

参考资料:

Node.js 环境性能监控探究

Nodejs: MemoryUsage()返回的rss,heapTotal,heapUsed,external的含义和区别

What do the return values of node.js process.memoryUsage() stand for?

如何分析 Node.js 中的内存泄漏  Node.js 应用故障排查手册

Record heap snapshots

前端内存泄露浅析  Node常用dump分析

Chrome Memory Tab: Learn to Find JavaScript Memory Leaks

Node 案发现场揭秘 —— Coredump 还原线上异常

Node.js调试之llnode篇  Node调试指南-uncaughtException

coredump lldb常用命令与调试技巧

node常用dump分析 Node调试指南-内存篇

Explore Node.js core dumps using the llnode plugin for lldb

v8 source list always fails w/ error: USAGE: v8 source list

Node.js精进(10)——性能监控(下)的更多相关文章

  1. Node.js v0.10.31API手冊-事件

    Node.js v0.10.31API手冊-文件夹 Events(事件) Node里面的很多对象都会分发事件:一个net.Server对象会在每次有新连接时分发一个事件, 一个fs.readStrea ...

  2. Node.js v0.10.31API手冊-控制台

    Node.js v0.10.31API手冊-文件夹 控制台 Object 用于向 stdout 和 stderr 打印字符.类似于大部分 Web 浏览器提供的 console 对象函数,在这里则是输出 ...

  3. Node.js v0.10.31API手工-DNS

    原版的API品种,这是从以前的翻译和翻译风格不同 Node.js v0.10.31API手冊-文件夹 DNS 使用 require('dns') 引入此模块. dns 模块中的全部方法都使用了 C-A ...

  4. Node.js精进(9)——性能监控(上)

    市面上成熟的 Node.js 性能监控系统,监控的指标有很多. 以开源的 Easy-Monitor 为例,在系统监控一栏中,指标包括内存.CPU.GC.进程.磁盘等. 这些系统能全方位的监控着应用的一 ...

  5. Node.js精进(4)——事件触发器

    Events 是 Node.js 中最重要的核心模块之一,很多模块都是依赖其创建的,例如上一节分析的流,文件.网络等模块. 比较知名的 Express.KOA 等框架在其内部也使用了 Events 模 ...

  6. Node.js在Windows与Linux下的安装

    一.Windows配置 (1)官网(http://nodejs.org)选择Node.js的Windows系统(32位和64位)最新版本. (2)下载完成后,执行MSI的安装文件. (3)安装完成,查 ...

  7. Node.js精进(2)——异步编程

    虽然 Node.js 是单线程的,但是在融合了libuv后,使其有能力非常简单地就构建出高性能和可扩展的网络应用程序. 下图是 Node.js 的简单架构图,基于 V8 和 libuv,其中 Node ...

  8. Node.js精进(5)——HTTP

    HTTP(HyperText Transfer Protocol)即超文本传输协议,是一种获取网络资源(例如图像.HTML文档)的应用层协议,它是互联网数据通信的基础,由请求和响应构成. 在 Node ...

  9. Node.js精进(6)——文件

    文件系统是一种用于向用户提供底层数据访问的机制,同时也是一套实现了数据的存储.分级组织.访问和获取等操作的抽象数据类型. Node.js 中的fs模块就是对文件系统的封装,整合了一套标准 POSIX ...

随机推荐

  1. ucore lab3 虚拟内存管理 学习笔记

    做个总结,这节说是讲虚拟内存管理,大部分的时间都在搞SWAP机制和服务于此机制的一些个算法.难度又降了一截. 不过现在我的电脑都16G内存了,能用完一半的情景都极少见了,可能到用到退休都不见得用的上S ...

  2. ClickHouse 对付单表上亿条记录分组查询秒出, OLAP应用秒杀其他数据库

    1.  启动并下载一个clickhouse-server, By default, starting above server instance will be run as default user ...

  3. SQL多表多字段比对方法

    目录 表-表比较 整体思路 找出不同字段的明细 T1/T2两表ID相同的部分,是否存在不同NAME 两表的交集与差集:判断两表某些字段是否相同 两表的交集与差集:找出T2表独有的id 字段-字段比较 ...

  4. K8S 使用Kubeadm搭建单个Master节点的Kubernetes(K8S)~本文仅用于测试学习

    01.集群规划 系统版本:CentOS Linux release 7.6.1810 (Core) 软件版本:kubeadm.kubernetes-1.15.docker-ce-18.09 硬件要求: ...

  5. K8S 使用Minikube搭建Kubernetes(K8S)~单机运行Kubernetes~适用于快速学习

    在一台主机上运行起来的Kubernetes,仅适用于学习!~~~ 系统版本:CentOS Linux release 7.6.1810 (Core) 软件版本:Docker-ce-18.06.0.Ku ...

  6. pandas:数据迭代、函数应用

    1.数据迭代 1.1 迭代行 (1)df.iterrows() for index, row in df[0:5].iterrows(): #需要两个变量承接数据 print(row) print(& ...

  7. python基础学习6

    Python的基础学习6 内容概要 while + else 死循环.while的嵌套 for循环基本使用 range关键字 for循环补充.爬虫 基本数据类型及内置方法 内容详情 while + e ...

  8. Node.js精进(1)——模块化

    模块化是一种将软件功能抽离成独立.可交互的软件设计技术,能促进大型应用程序和系统的构建. Node.js内置了两种模块系统,分别是默认的CommonJS模块和浏览器所支持的ECMAScript模块. ...

  9. 获取在线ip

    /** * 获取在线IP * @return String */ function getOnlineIp($format=0) { global $S_GLOBAL; if(empty($S_GLO ...

  10. 表达式的动态解析和计算,Flee用起来真香

    前言 在很多项目中经常会出现需要动态解析表达式和计算的场景,比如一些自动审核规则,或者是一些变量的值通过维护的公式在运行过程中动态算出:由于场景需求,都需要比较灵活的配置对应的表达式,然后希望在需要的 ...