关于node的前言
JavaScript运行在浏览器的沙盒中,他始终会受限于浏览器的中间层提供的能力。Node技术的出现给前端工作打开了新的局面。毫无疑问,现代的前端工程化已经离不开Node的应用了,而Node本身的设计是用作服务端语言,越来越多的前端团队,不再只将node局限于工程化应用,也开始去负责BFF层,比如SSR架构、数据适配、数据拼接裁剪、后端应用等。当作为服务端应用的时候,服务的【稳定】与【安全】是最重要的指标,提起性能指标
“首屏加载时长”、“可交互时长”等,这些每一个前端er都了解的h5性能指标。node用作服务端,有哪些性能指标是值得我们注意的呢?
影响node服务的因素
node用作服务端,相比较前端工程而言,我们需要关注的不仅仅是node自身的特性之外,还有依赖的服务器资源;如果依赖的服务器性能不好,node服务的性能必然也受到影响;
因此,我们去思考node服务的性能问题时候,需要从两个大的方面去思考:一是node运行时会出现的问题,二是服务器资源的性能
- CPU
- 内存
- 磁盘I/O
- 网络I/O
本文主要分析内存指标,以及内存泄漏的隐患排查
内存的限制
node的储存分为堆和栈,栈中存储基本数据类型,堆中存放引用类型:对象与变量; 对于node存储来说,堆内存是整个内存的主要占用。我们所关注的内存指标就是指堆内存的占用指标
一般的后端语言几乎没有内存限制的问题。但是node是基于V8引擎,在对象分配上遵从V8的方式,node通过Javascript使用内存是有限制的,在64位系统最大内存为1.4G左右,32位系统为0.7G左右; 之所以有内存的限制,一方面V8的设计之初是用于浏览器使用,这个限制的值对于一般网页来说是足够的。更深层的原因是V8的垃圾回收机制;
当我们声明一个变量并赋值的时候,就会存放在V8申请的堆内存中,当堆内存不够会继续申请内存,知道达到内存的限制。如果超出限制,那么就会出现内存泄漏的现象,出现卡顿等现象;这里的内存指标是最方便去量化的,通过node提供process.memoryUsage()
即可查看与了解
- rss:进程占用的内存总量。
- heapTotal:堆内存申请的总量。
- heapUsed:实际堆内存使用量。
\
垃圾回收
V8垃圾回收的基础是先将内存进行分代; 在V8中按照对象存活的时间将内存的进行分代。 存活时间短(可立即回收的变量)的放入新生代,常驻内存(全局变量、无法立即去回收的变量)放入老生代;
v8内存空间 = 新生代占用内存空间 + 老生代占用内存空间;
node也提供了扩宽内存的方法,在启动node的时候,可以通过传递--max-old-space-size 和 --max-old-space-size来调整内存的大小,这两个对应扩充的值就是上面提到的老生代内存与新生代的内存值,这个调整一旦启动,就不可更改,除非再次启动。在V8内存受限制的时候,可以按照这个值进行放宽
node --max-old-space-size=1800 server.js // 1800Mb
即使有调整,我们也不能全部都是用V8申请的内存,这源于v8的回收策略
目前的Node使用的是scavenge算法,它基于复制的方式实现垃圾回收,这在一定的程度是有对内存资源的浪费存在; 代码开发过程中,也可以按需使用global.gc()
去主动触发垃圾回收,如果主动触发之后,查询heapUsed
并没有下降,可以考虑是内存泄漏的存在了
内存泄漏的分析
通过上述,如果堆内存达到了堆内存的指标,无法再为新的变量/对象进行申请新内存的时候就是出现内存泄漏的现象了。老生代的常驻内存是不会被V8回收的,即使是手动出发。代码中常见的几项不会被垃圾立即回收,积累过多会造成内存泄漏隐患:
- 全局变量引用
- 闭包作用域内变量
- 模块的缓存
一个内存泄漏的简单案例, 仅供学习内存分析: 每次请求的时候会通过request传过来的的信息去数据库取数据,对读取的数据做了一层缓存,简单的示例代码如下:
const { json } = require('express');
const express = require('express');
const { v4: uuid } = require('uuid');
const app = express();
function getDataBase() {
const cache = {}
return function(key) {
if (cache[key]) return cache[key]
let data = new Array(10000).fill('cache')
cache[key] = data
return data
}
}
const dataBase = getDataBase()
app.get('/memoryUsage', (req, res, next) => {
let uid = uuid()
let data = dataBase(uid)
res.json({
msg: '内存数据',
data: JSON.stringify({
data
})
})
})
const port = 3100
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
通过wrk压测工具压测,你会发现请求数量一多,请求就挂掉了。
这个时候就到了分析node内存的时候了,以下是 Chrome-Memory分析
node.js调试工具: Chrome调试
- 启动Chrome开发者工具: 启动node服务的时候传递
--inspect
node --inspect server.js
- 打开chrome, 地址栏输入
chrome://inspect
, 界面如下图,点击Target 种的node服务,在弹出的弹框中选择Allocation sampling
, 点击start按钮即开始记录
- 如果是单个的请求,看不出什么问题。所有的问题都是在请求量增长的情况下,使用压测工具向node服务发出并发请求,我这里使用的是wrk工具,先模拟高并发请求
wrk -t12 -c1000 -d30s http://localhost:3100/memoryUsage
发送之前的内存情况:
发送之后10S左右, 内存就到了700多,选择了stop
结束录制后,会有录制报告如下图,可以看出99%的内存占用全在缓存的代码中,由此可以分析出来内存隐患的代码
更多的node调试,可以参阅:https://www.ruanyifeng.com/bl...
内存监控的实际应用
一个完善node服务的性能指标不仅仅只有内存这一项,还包括cpu利用率、相应时间、状态码监控等。这些线上的服务,社区内也有很多好用的工具,推荐一个非常简单好用的监控工具express-status-monitor
, 这个工具引入到工程后,通过默认路由(/status)或者指定的路由既可以访问各项指标的实时监控
const statusMonitor = require('express-status-monitor')({
title: 'XXX服务实时监控',
spans: [
{
interval: 1, // Every 15 seconds
retention: 100 // 在内存中保留60个数据点
}
]
});
app.use(statusMonitor)