Node.js精进(9)——性能监控(上)
市面上成熟的 Node.js 性能监控系统,监控的指标有很多。
以开源的 Easy-Monitor 为例,在系统监控一栏中,指标包括内存、CPU、GC、进程、磁盘等。
这些系统能全方位的监控着应用的一举一动,并且可以提供安全提醒、在线分析、导出真实状态等服务。
本专题分为上下两个篇章,会简单分析下在 Node.js 环境中的几个资源瓶颈,包括CPU 、内存和进程奔溃,并且会给出相应的监控方法。
本系列所有的示例源码都已上传至Github,点击此处获取。
一、CPU
在 Linux 系统中,可以通过 top 命令看到当前的 CPU 资源利用率、内存使用等信息,并且可按特定指标排序,类似于 Windows 的任务管理器。
在 Node.js 中,提供了两个方法可以读取和计算出 CPU 负载和 CPU 使用率两个指标。
这两个指标在一定程度上都可以反映一台计算机的繁忙程度。
1)CPU 负载
CPU 负载是指在一段时间内等待或占用 CPU 的进程数,进程是操作系统中资源分配的最小单位。
平均负载(Load Average)就是那些进程数除以时间得到的平均数。
假设一台计算机只有一个 CPU 并且是一核,将 CPU 比作一座只有一条单向车道的桥,车比作进程。
- 当平均负载为 0 时,桥上没有车。
- 当平均负载为 0.5 时,桥上一半路段有车。
- 当平均负载为 1 时,桥上所有路段都有车,虽然大桥已满,但不会堵车。
- 当平均负载为 2 时,大桥已满,并且还多了一样多的车在桥外排队等待。
如果 CPU 每分钟可以处理 100 个进程,那么当平均负载是 2 时,还有 100 个进程在排队等待中。
现在的芯片厂商往往会让 1 个 CPU 包含多个核,并且还能将 1 个核虚拟成 2 个逻辑 CPU,CPU 负载建议的计算方式是:
(CPU个数 * 核数 * 2 * 0.8)或者(CPU个数 * 核数 * 2 * 0.7)
不建议 CPU 长期满负荷工作。对于平均负载的量化,会采用三个时间标准:1 分钟,5 分钟和 15 分钟。
1 分钟的时间比较短,有时候峰值突然升高,有可能是暂时现象。
5 分钟和 15 分钟是较为合适的评判指标,当这两个时间段内的平均负载都大于 1,那就表明问题持续存在。
这是一个危险的信号,CPU 上等待的进程在增多,若不及时清理,就会越堵越长,影响程序的正常运行。
在 os 模块中,提供了 loadavg() 方法,可以得到一个包含 1、5 和 15 分钟的平均负载的数组。
const os = require("os");
os.loadavg(); // [ 1.9951171875, 1.951171875, 1.93359375 ]
注意,平均负载是 Unix 特有的概念,在 Windows 上,返回值始终为 [0, 0, 0]。
2)CPU 使用率
CPU 使用率是指程序在运行期间占用 CPU 的百分比,也就是说量化 CPU 的占用情况,计算方式如下:
CPU使用率 = (1 - CPU空闲时间 / CPU总时间) * 100
CPU 使用率高,并不意味着 CPU 负载也高,例如当前任务很少,其中有一个需要大量的计算(CPU 密集型场景),那么使用率会很高,但负载很低。
CPU 负载高,并不意味着 CPU 使用率也高,例如当前任务很多,在任务执行过程中因为等待 I/O 使得 CPU 非常空闲(I/O 密集型场景),那么使用率就会变低,但负载很高。
在 os 模块中,提供了 cpus() 方法,可得到以每个逻辑 CPU 内核信息组成的对象数组,如下所示。
[
{
model: 'Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz',
speed: 2300,
times: { user: 27207990, nice: 0, sys: 17891890, idle: 179286370, irq: 0 }
},
{
model: 'Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz',
speed: 2300,
times: { user: 294240, nice: 0, sys: 352550, idle: 223732290, irq: 0 }
},
]
其中 times 属性是一些时间信息,其中 nice 值仅适用于 POSIX 平台。在 Windows 中,所有处理器的 nice 值始终为 0。
- user:CPU 在用户模式下花费的毫秒数。
- nice:CPU 在良好模式下花费的毫秒数。
- sys:CPU 在系统模式下花费的毫秒数。
- idle:CPU 在空闲模式下花费的毫秒数。
- irq:CPU 在中断请求模式下花费的毫秒数。
下面用一个示例计算 CPU 使用率,遍历 CPU 信息数组后,将各个时间依次累加,然后返回总时间和空闲时间,最后套用公式计算。
function getCPUInfo() {
const cpus = os.cpus();
let user = 0, nice = 0, sys = 0, idle = 0, irq = 0, total = 0;
// 遍历 CPU
for (const cpu in cpus) {
const times = cpus[cpu].times;
user += times.user;
nice += times.nice;
sys += times.sys;
idle += times.idle;
irq += times.irq;
}
total += user + nice + sys + idle + irq;
return {
idle,
total,
};
}
const cpu = getCPUInfo();
// CPU 使用率
const usage = (1 - cpu.idle / cpu.total) * 100;
3)v8-profiler
Node.js 是基于 V8 引擎运行的,而 V8 引擎内部实现了一个 CPU Profiler,并且开放了相关 API,v8-profiler 就是一个基于这些 API 收集一些运行时数据(例如 CPU 和内存)的库。
不过在安装时,会报错,因此需要换一个包:v8-profiler-next,基于 v8-profiler,兼容 Node.js V4 以上的所有版本。
../src/cpu_profiler.cc:6:9: error: no member named 'Handle' in namespace 'v8'; did you mean 'v8::CodeEventHandler::Handle'?
在下面的示例中,是一段需要消耗 CPU 计算的加密代码。
const crypto = require('crypto');
const password = 'test'
const salt = crypto.randomBytes(128).toString('base64')
crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex')
在下面的示例中,会在 1 分钟后导出一份 CPU 分析文件,运行后会在当前目录生成 cpuprofile 后缀的文件。
const fs = require('fs');
const v8Profiler = require('v8-profiler-next');
const title = 'test';
// 兼容 vscode 中的 cpuprofile 解析
v8Profiler.setGenerateType(1);
v8Profiler.startProfiling(title, true);
// 1分钟后运行
setTimeout(() => {
const profile = v8Profiler.stopProfiling(title);
// 导出CPU分析文件
profile.export(function (error, result) {
fs.writeFileSync(`${title}.cpuprofile`, result);
profile.delete();
});
}, 60 * 1000);
点击 Chrome DevTools 工具栏右侧的更多按钮,选择 More tools -> JavaScript Profiler 进入到 CPU 的分析页面。

将分析文件 Load 进来,首先看到的是 Heavy 视图的分析结果,在图中选中的下拉框中还可以选择 Chart 和 tree。
前者能显示火焰图,按时间顺序排列;后者能显示调用结构的总体状况,从调用堆栈的顶端开始,即从最初调用的位置开始。

在 Heavy 视图中,会按照对应用的性能影响程度从高到低排列,这其中有 3 个指标:
- Self Time:完成当前函数调用所用的时间,仅包括函数本身的语句,不包括它调用的任何子函数。
- Total Time:完成此函数的当前调用以及它调用的任何子函数所花费的总时间。
- Function:函数名及其全路径,可展开查看子函数。
切换到 Tree 视图,逐层打开,就可以看到 pbkdf2Sync() 函数占据了 CPU 的大部分时间。

上图中的 (program) 只计算了 native code 的时间,不包含执行脚本代码的时间(即没有在 JavaScript 的堆栈上),idle 也是 native 在执行 (program) 的一种。
二、垃圾回收器
Node.js 是一个基于 V8 引擎的 JavaScript 运行时环境,而 Node.js 中的垃圾回收器(GC)其实就是 V8 的垃圾回收器。
这么多年来,V8 的垃圾回收器(Garbage Collector,简写GC)从一个全停顿(Stop-The-World),慢慢演变成了一个更加并行,并发和增量的垃圾回收器。
本节内容参考了 V8 团队分享的文章:Trash talk: the Orinoco garbage collector。
1)代际假说
在垃圾回收中有一个重要术语:代际假说(The Generational Hypothesis),这个假说不仅仅适用于 JavaScript,同样适用于大多数的动态语言,Java、Python 等。
代际假说表明很多对象在内存中存在的时间很短,即从垃圾回收的角度来看,很多对象在分配内存空间后,很快就变得不可访问。
2)两种垃圾回收器
在 V8 中,会将堆分为两块不同的区域:新生代(Young Generation)和老生代(Old Generation)。
新生代中存放的是生存时间短的对象,大小在 1~ 8M之间;老生代中存放的生存时间久的对象。
对于这两块区域,V8 会使用两个不同的垃圾回收器:
- 副垃圾回收器(Scavenger)主要负责新生代的垃圾回收。如果经过垃圾回收后,对象还存活的话,就会从新生代移动到老生代。
- 主垃圾回收器(Full Mark-Compact)主要负责老生代的垃圾回收。
无论哪种垃圾回收器,都会有一套共同的工作流程,定期去做些任务:
- 标记活动对象和非活动对象,前者是还在使用的对象,后者是可以进行垃圾回收的对象。
- 回收或者重用被非活动对象占据的内存,就是在标记完成后,统一清理那些被标记为可回收的对象。
- 整理内存碎片(不连续的内存空间),这一步是可选的,因为有的垃圾回收器不会产生内存碎片。
3)副垃圾回收器
V8 为新生代采用 Scavenge 算法,会将内存空间划分成两个区域:对象区域(From-Space)和空闲区域(To-Space)。
副垃圾回收器在清理新生代时,会先将所有的活动对象移动(evacuate)到连续的一块空闲内存中(这样能避免内存碎片)。
然后将两块内存空间互换,即把 To-Space 变成 From-Space。

接着为了新生代的内存空间不被耗尽,对于两次垃圾回收后还活动的对象,会把它们移动到老生代,而不是 To-Space。

最后是更新引用已移动的原始对象的指针。上述几步都是交错进行,而不是在不同阶段执行。
4)主垃圾回收器
主垃圾回收器负责老生代的清理,而在老生代中,除了新生代中晋升的对象之外,还有一些大的对象也会被分配到此处。
主垃圾回收器采用了 Mark-Sweep(标记清除)和 Mark-Compact(标记整理)两种算法,其中涉及三个阶段:标记(marking),清除(sweeping)和整理(compacting)。
(1)在标记阶段,会从一组根元素开始,递归遍历这组根元素。其中根元素包括执行堆栈和全局对象,浏览器环境下的全局对象是 window,Node.js 环境下是 global。
在这个遍历过程中,会追溯每一个指向 JavaScript 对象的指针,将其标记为可访问,同时追溯对象中每一个属性的指针。
这个过程会一直持续至找到并标记运行时可到达的所有对象,而那些追溯不到的就是垃圾数据。
(2)在清除阶段,会将非活动对象占用的内存空间添加到一个叫空闲列表的数据结构中。
空闲列表中的内存块由大小来区分,这是为了方便以后需要分配内存时,可以快速的找到大小合适的内存空间并分配给新的对象。
下图描绘了在将垃圾数据回收前后,内存占用的情况。

可以看出,在执行清除算法后,会产生大量不连续的内存碎片。
(3)在整理阶段,会让所有活动的对象都向一端移动,然后直接清理掉端边界以外的内存,如下图所示。

5)垃圾回收机制
在本节开头提到了并行(parallel)、增量(incremental)和并发(concurrent)三种垃圾回收机制。
(1)并行是指主线程和协助线程同时执行同样的工作,这仍然是一种全停顿。
但垃圾回收所耗费的时间等于总时间除以参与的线程数量(加上一些同步开销)。

(2)增量是指主线程间歇性的去做少量的垃圾回收,而不是花一整段时间去执行。
虽然没有减少主线程暂停的时间,但 JavaScript 的执行都能得到及时的响应。

(3)并发是指主线程一直执行 JavaScript,而辅助线程在后台执行垃圾回收,这种实现起来最难,需要处理很多复杂的场景。
例如 JavaScript 堆上的任何东西都可以随时更改,使之前所做的工作无效。 况且现在有读/写竞争,辅助线程和主线程有可能同时在更改同一个对象。

V8 在新生代垃圾回收中会使用并行清理,每个协助线程会将所有的活动对象都移动到 To-Space。
主垃圾回收器主要使用并发标记,当堆的动态分配接近最高阈值时,会启动并发标记任务。
V8 会利用主线程上的空闲时间主动的去执行垃圾回收,在 Chrome 中,大约有 16.6 毫秒的时间去渲染动画的每一帧。
如果动画提前完成,那么就能在下一帧之前的空闲时间去触发垃圾回收。

在《综合性 GC 问题和优化》一文中提到,绝大部分的 GC 引发的问题会表现在 CPU 上,而本质上这类问题却是 GC 引起的内存问题。
一般产生的流程是:先在堆内存不断达到触发 GC 的预设条件,然后不断触发 GC,最后 CPU 飙高。
参考资料:
Difference between 'self' and 'total' in Chrome CPU Profile of JS
Deep understanding of chrome V8 garbage collection mechanism
Node.js精进(9)——性能监控(上)的更多相关文章
- Node.js精进(10)——性能监控(下)
本节会重点分析内存和进程奔溃,并且会给出相应的监控方法. 本系列所有的示例源码都已上传至Github,点击此处获取. 一.内存 虽然在 Node.js 中并不需要手动的对内存进行分配和销毁,但是在开发 ...
- Node.js精进(2)——异步编程
虽然 Node.js 是单线程的,但是在融合了libuv后,使其有能力非常简单地就构建出高性能和可扩展的网络应用程序. 下图是 Node.js 的简单架构图,基于 V8 和 libuv,其中 Node ...
- Node.js精进(4)——事件触发器
Events 是 Node.js 中最重要的核心模块之一,很多模块都是依赖其创建的,例如上一节分析的流,文件.网络等模块. 比较知名的 Express.KOA 等框架在其内部也使用了 Events 模 ...
- Node.js精进(5)——HTTP
HTTP(HyperText Transfer Protocol)即超文本传输协议,是一种获取网络资源(例如图像.HTML文档)的应用层协议,它是互联网数据通信的基础,由请求和响应构成. 在 Node ...
- Node.js精进(6)——文件
文件系统是一种用于向用户提供底层数据访问的机制,同时也是一套实现了数据的存储.分级组织.访问和获取等操作的抽象数据类型. Node.js 中的fs模块就是对文件系统的封装,整合了一套标准 POSIX ...
- Node.js精进(7)——日志
在 Node.js 中,提供了console模块,这是一个简单的调试控制台,其功能类似于浏览器提供的 JavaScript 控制台. 本系列所有的示例源码都已上传至Github,点击此处获取. 一.原 ...
- Node.js精进(11)——Socket.IO
Socket.IO 是一个建立在 WebSocket 协议之上的库,可以在客户端和服务器之间实现低延迟.双向和基于事件的通信. 并且提供额外的保证,例如回退到 HTTP 长轮询.自动重连.数据包缓冲. ...
- CentOS 7部署Node.js+MongoDB:在VPS上从安装到Hello world
写好代码,花钱买了VPS,看着Charges一直上涨却无从下手?记一位新手司机从购买VPS到成功访问的过程 0.购买VPS 首先,选择VPS提供商,部署一个新的服务器(Deploy New Serve ...
- 用node.js写个在Bash上对字符串进行Base64或URL的encode和decode脚本
一:自己这段时间经常要用到Base64编码和URL编码,写个编译型语言有点麻烦干脆就用node.js弄了个,弄好后在/etc/profile里加上alias就能完成工具的配置,先上代码: functi ...
随机推荐
- [报告] Microsoft :Application of deep learning methods in speech enhancement
Application of deep learning methods in speech enhancement 语音增强中的深度学习应用 按: 本文是DNS,AEC,PLC等国际级语音竞赛的主办 ...
- Hadoop(四)C#操作Hbase
Hbase Hbase是一种NoSql模式的数据库,采用了列式存储.而采用了列存储天然具备以下优势: 可只查涉及的列,且列可作为索引,相对高效 针对某一列的聚合及其方便 同一列的数据类型一致,方便压缩 ...
- 2020年DevOps工程师入门指南
DevOps兴起于2010年代,到现在DevOps已经在行业中拥有了一席之地,并在继续发展壮大. 有兴趣成为一名DevOps工程师吗?如果想要成为一名DevOps工程师,需要做到以下五点: 要有开发者 ...
- 10┃音视频直播系统之 WebRTC 中的数据统计和绘制统计图形
一.数据统计 在视频直播中,还有一项比较重要,那就是数据监控 比如开发人员需要知道收了多少包.发了多少包.丢了多少包,以及每路流的流量是多少,才能评估出目前用户使用的音视频产品的服务质量是好还是坏 如 ...
- 【Java8新特性】Optional 类
概述 Optional 类是一个可以为null的容器对象.如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象. Optional 是个容器:它可以保存类型T的值,或者 ...
- 好客租房9-jsx的学习目标
1能够知道什么是jsx 2能够使用jsx创建react元素 3能够在jsx使用javascript表达式 4能够使用jsx的条件渲染和列表渲染 5能够给jsx添加样式 jsx的基本使用 jsx中使用j ...
- redis的Linux系统安装与配置、redis的api使用、高级用法之慢查询、pipline事物
今日内容概要 redis 的linux安装和配置 redis 的api使用 高级用法之慢查询 pipline事务 内容详细 1.redis 的linux安装和配置 # redis 版本选择问题 -最新 ...
- Java 对象实现 Serializable 的原因
java.io.Serializable 是 Java 中的一种标记接口(marker interface).标记接口是一种特殊的接口,java.io.Serializable 接口没有任何方法,也没 ...
- 判断数据类型(typeof&instanceof&toString)
一.数据类型 ES6规范中有7种数据类型,分别是基本类型和引用类型两大类 基本类型(简单类型.原始类型):String.Number.Boolean.Null.Undefined.Symbol 引用类 ...
- 数据库与MySQL的下载使用
目录 数据存储演变史 数据库应用发展史 数据库本质 数据库分类 关系型数据库 非关系型数据库 SQL与NoSQL MySQL简介 版本问题 下载使用 目录结构 基本使用 简单使用 系统服务 修改密码 ...