通过开源项目学技术 — memeye内存监控

542 阅读6分钟

这是我参与「掘金日新计划 · 4 月更文挑战」的第 5 天,点击查看活动详情

说明

原仓库已经五年多没有更新了,可能也根本没有更新的必要,但还是将原仓库地址放在这里 Memeye

因为原仓库比较久远,所以笔者打算重写一套node内存监控的系统,以Memeye原仓库为蓝本,起名ShowMemor,欢迎👏🏻共建🏹。

memeye上手

memeye上手非常简单,我们直接来看:

const http = require('http')
const memeye = require('memeye')

memeye()

let s = '';
for (let i=0;i<1024 * 10;i++){
	s += 'a';
}

const str = s;
const bufStr = Buffer.from(s);

const server = http.createServer((req,res) => {
	if (req.url === '/buffer'){
		res.end(bufStr)
	}else if(req.url === '/string'){
		res.end(str)
	}
})

server.listen(3000);

wrk压测

wrk -t12 -c400 -d30s http://192.168.20.186:3000/string

压测结果

string2.PNG

buffer.PNG

我们可以看可视化的效果还是很不错的,接下来就来分析一下它的原理。

memeye架构

image.png

图上分为三部分,左侧就是性能数据的获取部分,右侧是客户端可视化的部分,而中间是进行IPC通信获取实时数据,并通过事件监听的方式触发数据推送,通过websocket发送数据到客户端。

很精简,但却有很多值得我们学习的地方。很多时候并不是我们写不出来什么库或者框架,而是我们不知道底层给我们提供了什么功能,就例如这个内存监控的实现,首先采集数据所用到的node原生提供的库就是我们平时不会用到的。因为业务不会用到,所以🙅‍♂️去看,所以不知道,所以连node提供给我们了哪些能力都不知道,何谈创新?

node子进程及进程间IPC通信

我们来看一下关于node开启子进程,以及如何进行IPC通信。

父进程 parent.js

const child = require('child_process');
const childProcess = child.fork(__dirname + '/child.js');
// 默认去找路径下的 index.js
// const childProcess = child.fork(__dirname);

// 向子进程发送数据
childProcess.send({'name':'flten'});

// 监听子进程发送过来的数据
childProcess.on("message",function (message) {
    console.log('parent received message',message)
})

子进程 child.js

// 监听父进程发送过来的数据
process.on("message",function(message){
    console.log('child received message',message)
})

// 子进程向父进程发送数据
process.send({'age':24})

image.png

其实比较简单,开启进程使用的是child_process,而进程间通信,涉及两者之间,父进程可以向子进程发送数据,也可以监听子进程发来的数据,子进程亦是如此,直接看代码更为直观。

socketio双向通信

主要是服务器主动向客户端发送

服务端

const express = require('express');

const app = express();
app.use(express.static(__dirname));

app.listen(8000);

const ws = require('ws').Server;
const server = new ws({port:8888});

server.on('connection',function(socket){
	console.log('服务端建立连接成功')
	socket.on('message',function(message){
		console.log('监听到客户端传来的消息',message)
		socket.send('服务端向客户端发送的消息')
	})
})

客户端

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>
	<div id="clock"></div>
<script type="text/javascript">
	const socket = new WebSocket('ws://localhost:8888');
	socket.onopen = function(){
		console.log('监听到服务打开')
		socket.send('客户端向服务端发送消息')
	}
	socket.onmessage = function(event){
		console.log('客户端监听到服务端传来的消息',event.data)
	}

</script>
</body>
</html>

我们用express开启了一个后端服务,而在客户端直接使用webstorm为静态文件打开一个静态服务。服务端引入ws,客户端调用原生的WebSocket构造函数,根据所提供的监听事件就可以进行通信了。

是不是很简单?只是我们可能遇不到双向通信的业务,所以不会去关注node有ws,使用那么久javascript,却也不知道有个WebSocket构造函数。

node流与管道

定时器轮询

如何获取实时数据呢?其实最简单的一种方式,就是使用轮询,简单粗暴但却非常有效。

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

setInterval(() => {
    let v8Stat = {
        heap: v8.getHeapStatistics(),
        heapSpace: v8.getHeapSpaceStatistics(),
    }
    let osStat = {
        freeMem: os.freemem(),
        totalMem: os.totalmem(),
        cpus: os.cpus(),
    }
    console.log(v8Stat,osStat)
}, 1000);

其中的node的v8模块我相信很少被我们到,但这就是核心呀,因为它就是重要的数据来源。而通过node原生提供的os模块我们就可以获取到cpu数、空闲内存、总内存等系统的情况。但这些都只是node原生提供给我们的能力而已,获取到了这些其实已经解决核心。

事件监听

内存数据收集

那么获取的数据是什么样子呢?我们可以来看一下。

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

let v8Stat = {
    heap: v8.getHeapStatistics(),
    heapSpace: v8.getHeapSpaceStatistics(), // V8 堆空间的统计信息
}
let osStat = {
    freeMem: os.freemem(),
    totalMem: os.totalmem(),
    cpus: os.cpus(),
}

console.log('v8Stat',v8Stat);
console.log('osStat',osStat);

结果


v8Stat {
  heap: {
    total_heap_size: 4468736,
    total_heap_size_executable: 524288,
    total_physical_size: 4468736,
    total_available_size: 4342017560,
    used_heap_size: 2885536,
    heap_size_limit: 4345298944,
    malloced_memory: 8192,
    peak_malloced_memory: 123168,
    does_zap_garbage: 0,
    number_of_native_contexts: 1,//native_context 的值是当前活动的顶层上下文的数量。 随着时间的推移,此数字的增加表示内存泄漏。
    number_of_detached_contexts: 0 //detached_context 的值是已分离但尚未回收垃圾的上下文数。 该数字不为零表示潜在的内存泄漏。
  },
  heapSpace: [
    {
      space_name: 'read_only_space',
      space_size: 151552,
      space_used_size: 150392,
      space_available_size: 0,
      physical_space_size: 151552
    },
    {
      space_name: 'new_space',
      space_size: 2097152,
      space_used_size: 915672,
      space_available_size: 131752,
      physical_space_size: 2097152
    },
    {
      space_name: 'old_space',
      space_size: 1458176,
      space_used_size: 1348792,
      space_available_size: 184,
      physical_space_size: 1458176
    },
    {
      space_name: 'code_space',
      space_size: 360448,
      space_used_size: 87456,
      space_available_size: 0,
      physical_space_size: 360448
    },
    {
      space_name: 'map_space',
      space_size: 528384,
      space_used_size: 267768,
      space_available_size: 0,
      physical_space_size: 528384
    },
    {
      space_name: 'large_object_space',
      space_size: 135168,
      space_used_size: 131112,
      space_available_size: 0,
      physical_space_size: 135168
    },
    {
      space_name: 'code_large_object_space',
      space_size: 0,
      space_used_size: 0,
      space_available_size: 0,
      physical_space_size: 0
    },
    {
      space_name: 'new_large_object_space',
      space_size: 0,
      space_used_size: 0,
      space_available_size: 1047424,
      physical_space_size: 0
    }
  ]
}
osStat {
  freeMem: 6661525504,
  totalMem: 17067167744,
  cpus: [
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    },
    {
      model: 'Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz',
      speed: 3192,
      times: [Object]
    }
  ]
}

收集子进程内存信息

我们的主进程需要和客户端进行双向通信,所以可以将信息采集的工作交给子进程去做。

const child = require('child_process');
const childProcess = child.fork(__dirname + '/child.js');

// 收集子进程内存信息
let processStat = process.memoryUsage();
console.log(processStat);

结果

{
  rss: 26828800,
  heapTotal: 4993024,
  heapUsed: 3163440,
  external: 1122743,
  arrayBuffers: 10059
}

colors.js 控制台输入日志样式美化

这个不那么重要,但是很有趣,并且很可能你知道了以后就会去使用。让你的打印信息五彩缤纷,能够识别不同的信息类型。

const colors = require('colors/safe');

colors.setTheme({
    info: 'green',
    warn: 'yellow',
    debug: 'blue',
    error: 'red'
});

function Logger(prefix = '') {
    this.prefix = prefix;
}

Logger.prototype.error = function (msg) {
    console.log(this.prefix + colors['error'](msg))
}

Logger.prototype.warn = function (msg) {
    console.log(this.prefix + colors['warn'](msg))
}

Logger.prototype.info = function (msg) {
    console.log(this.prefix + colors['info'](msg))
}

Logger.prototype.debug = function (msg) {
    console.log(this.prefix + colors['debug'](msg))
}

const log = new Logger('flten:');
log.error('error');
log.warn('warn');
log.info('info');
log.debug('debug');

结果

image.png

持续优化