front to back
- 业务上 : front 面向展示 交互 ; back 功能 服务 数据一致性等等
- 环境 : front browser webview 单机; back 集群 高并发
- 思想差异 : front 快速开发 快速渲染 视觉效果 等等 ; back 服务稳定,性能,内存泄漏等等
V8 内存的简介
首先通过memoryUsage可以查看node进程的使用情况,具体介绍如下
process.memoryUsage(); 查看node进程内存使用情况 单位是字节
{ rss: 20725760, (resident set size)进程的常驻内存
heapTotal: 7376896, 已申请到的堆内存
heapUsed: 3881280, 当前使用的堆内存
external: 8772 , c++的内存使用,受v8管理
}
针对上面api使用写了个相关例子
let showMem=function () {
var mem=process.memoryUsage();
var format=function (bytes) {
return (bytes/1024/1024).toFixed(2)+'MB';
}
console.log('Process :heapTotal '+format(mem.heapTotal)+' heapUsed '+ format(mem.heapUsed)+' rss '+format(mem.rss));
console.log("------------------------------------")
}
var useMem=function () {
var size=200*1024*1024; //每次构建200MB对象
var buffer=new Buffer(size);
for(var i=0;i<size;i++){
buffer[i]=0;
}
return buffer;
}
var totoal=[];
for(var j=0;j<15;j++){
showMem();
totoal.push(useMem());
}
showMem();
heapTotal和heapUsed变化极小,唯一变化rss值,且该值远超V8内存分配限制(说明:V8的内存限制:64位系统约为1.4GB、32位系统约为0.7GB (这个规定是node源码 中限制的),具体测试可以将上述实例中buffer改成array即可)
说明:node 内存由通过v8进行分配的部分(新生到和老生代)(只有这部分才会受v8垃圾回收的限制)和node进行自行分配的部分(例如buffer)
额外补充
- --max-old-space-size 命令就是设置老生代内存空间的最大值
- --max-new-space-size 命令则可以设置新生代内存空间的大小
这两个参数只能在node启动时进行设置
下面从gc层面谈论下v8内存,这个可以说很多,我的云笔记中关于java和js的内存使用,分配,gc等整理了好几个系列,下面我用自己的话简单总结下。
关于内存分区有以下两个大类:
- new space(特征:对象存活时间短) 又分为from和to两个区域,采用复制算法,空间换时间。如果存活多次,转移到老生代中。
- 老生代中(对象存活时间长),采用mark-sweep(标记清除)和mark-compact(标记整理),缺点容易形成内存碎片,导致无法分配大对象。v8主要使用mark-sweep,在内存空间不够时,才使用标记整理。老生代又可以细分Old Space(新生代中gc活过2次晋升到这个空间)、Large Object Space(大对象,一般指超过1MB,初始时直接分配到此),Map Space(“隐藏类”的指针,便于快速访问对象成员)、Code Space(机器码,存储在可执行内存中)
关于gc,总的来说在js中不同对象存活时间不同,没有一种算法适应所有场景,内存垃圾进行分代,针对不同分代使用相应高效算法,有些gc算法和java是一样的,复制(空间换时间,new Space),标记清除和标记整理(old space)等等。
补充存活标记依据
- 全局变量或者有由全局变量出发,可以访问到的对象
- 正在执行的函数中的局部对象,包括这些局部对象可以访问到的对象。
- 闭包中引用的对象
关于内存,偏底层语言(例如c)和js以及java不一样
#c代码
#include <stdio.h>
void init()
{
int arr[5]={};
for(int i=0;i<5;i++){
arr[i]=i;
}
}
void test(){
int arr[5];
for(int i =0;i<5;i++){
printf("%d\n",arr[i]);
}
}
int main(int argc,char const *argv[]){
init();
test();
return 0;
}
// 0 1 2 3 4
# java代码
public class Test {
public static void main(String[] args){
init();
test();
}
public static void init(){
int[] arr=new int[]{0,1,2,3,4};
// for(int i=0;i<5;i++){
// System.out.println(arr[i]);
// }
}
public static void test(){
int[] arr=new int[5];
for(int i=0;i<5;i++){
System.out.println(arr[i]);
}
}
}
// 0 0 0 0 0
上述例子说明c语言堆栈执行过程中,函数执行完堆栈释放,程序员如果不关注释放过程,出现一些问题 ,
而js和java就不会,因为内存自动分配,垃圾自动回收,不用考虑脏数据擦除。
内存泄漏
首先什么是内存泄漏?
对象不再被应用程序使用,但是垃圾回收器却不能移除它们,因为它们正在被引用。
node进程运行中garbage没被回收肯定属于内存泄漏,关于这个定义引起讨论是在node进程退出或崩溃后的情况。我的理解是如果有部分内存比如共享内存(用于进程间通信),如果没被释放,这依然属于内存泄漏,内存泄漏不仅仅只进程层面。
内存泄漏原因有好几种,ppt有的,我不在列举,我在分享过程中有同学提出个疑问,关于exports使用中为什么会导致泄漏,印象比较深刻,可能当时讲的不清楚,下面写个具体例子详细阐述下。
//A.js
var leakAry=[];
exports.leak=function(){
leakAry.push('leak '+" gcy "+new Date());
}
//B.js
var obj=require('./a');
for(var i=0;i<10;i++){
obj.leak();
}
在node当中,为了加速module访问,所有module都会被编译缓存,上述代码导致泄漏的原因就是模块上,被缓存局部变量重复访问,因为没初始化,导致内存占用不断增大。
其次平时如何定位内存泄漏具体问题。
var http=require('http');
var heapdum=require('heapdump');
var leakArray=[];
var leak=function () {
for(var i=0;i<100000;i++){
leakArray.push("leak "+Math.random());
}
};
var i=0;
http.createServer(function (req,res) {
leak(); //泄漏触发位置
i++;
res.writeHead(200,{'Content-Type':'text/plain'});
res.end("hello world gcy"+i);
}).listen(1337);
console.log(process.pid);
console.log("server start gcy");
-------------------------------------------------------------
for ((i=1;i<=10000;i++));
do curl -v --header "Connection: keep-alive" "http://127.0.0.1:1337/"
done
批量100和10000个请求,记录heap dump镜像,通过对比视图查看变化比较大地方,通过分析定位具体泄漏位置,展示效果如下图
可以看到有三处对象明显增长的地方,string、concatenated string以及 array 对象增长。点击查看一下对象的引用情况,可以发现原因是leak执行,leakArray没有初始化,导致其里面字符串没有被清除,从而导致内存泄漏。
关于异步处理
问题:不适合处理复杂的状态机
解决方案:是使用队列结构进行可控的异步并发。
关于这一点理解分享中上有人提出可异议,
我的理解是比如逻辑中有大量的promise待处理,一旦此逻辑比较多,我们没法掌控,宏观上没法知晓具体执行情况,但是通过队列,在高并发请求下,大量的状态机promise通过队列
的管理,我们可以做到可控,哪些被消费了,状态机所处某个过程的比例都可以统计,一旦有这些统计信息,我们就可以进行相应的处理。求证。
弱计算
什么是 IO 密集型? 控制器busy
什么是 CPU 密集型? 运算器busy
关于弱计算的分析可以用node生成profile文件,然后通过chrome进行分析或者webstrome自带的v8 profiling进行分析,可以得到一系列函数执行时间统计和调用堆栈过程时间消耗统计,依据这些信息,我们在做相应的优化。图中显示的是例子test2的结果。
部署
child_process
//普通情况,只是作为用法示例
var fork=require('child_process').fork;
var cpus=require('os').cpus();
for(var i=0;i<cpus.length;i++){
fork('./work.js')
}
-----------------------
//work.js
var http=require('http');
http.createServer(function (req,res) {
res.writeHead(200,{'Content-Type':'text/plain'});
res.end('hello world gcy');
}).listen(Math.round((1+Math.random())*1000),'127.0.0.1');
---------------------------------------------------
---------------------------------------------------
升级版(相比上面外界访问只有一个端口,实际场景应用)
var server= require('net').createServer();
server.listen(1136,function () {
child1.send('server',server);
child2.send('server',server);
// server.close();
});
总结
child_process 模块给node提供了一下几个方法创建子进程
1: spwan(); 启动一个子进程执行命令
2: exec() 启动一个子进程执行命令,与spwan不同的是,他有一个回调函数获知子进程状况
3: fork() 与spwan类似 不同地方在于 在创建子进程的时候只需指定 需要执行的JavaScript文件即可
上面方法很少用了,node当中有cluster模块,简单的几个方法就可以创建集群,其本质基于上面的封装,底层实现原理基于句柄共享。
var index=require('./app');
if (cluster.isMaster)
for (var i = 0, n = os.cpus().length; i < n; i += 1)
cluster.fork();
else
index.app();
-------------------------------------------
//app.js
function app() {
var server = http.createServer(function(req, res) {
res.writeHead(200);
res.end('hello world gcy\n');
console.log(cluster.worker.id);
});
server.listen(8088);
}
exports.app=app;
cluster使用中有三个问题
1、round-robin是当前cluster的默认负载均衡处理模式(除了windows平台),自行设定负载算法
可以在cluster加载之后未调用其它cluster函数之前执行:cluster.schedulingPolicy = cluster.SCHED_NONE。
2、进程监控问题
master进程不会自动管理worker进程的生死,如果worker被外界杀掉了,不会自动重启,
只会给master进程发送‘exit’消息,开发者需要自己做好管理。
3、数据共享问题
各个worker进程之间是独立的,为了让多个worker进程共享数据(譬如用户session),
一般的做法是在Node.js之外使用memcached或者redis。
cluster适用于在单台机器上,如果应用的流量巨大,多机器是必然的。这时,反向代理就派上用场了,我们可以用node来写反向代理的服务(比如用 http-proxy )
,好处是可以保持工程师技术栈的统一,不过生产环境,我们用的更多的还是nginx,部分重要配置如下。
nginx做集群
upstream gcy.com{
server 127.0.0.1:3000 weight=1;
server 127.0.0.1:3001 weight=2;
server 127.0.0.1:3002 weight=6;
}
维护
维护主要做好以下三点
- 日志
- 异常处理
- 第三方依赖管理。
异常处理解释下,有人提出疑问:异常没有被捕获一路冒泡 ,会触发uncaughtException 事件,
如果异常出现之后,没有进行正确的恢复操作可能导致内存泄漏,清理已使用的资源
(文件描述符(清除文件的占用)、句柄(Master传给work的标识)等) 然后 process.exit。
总结
qcon现场听一次,自己分享一次,总结一次,感觉收货蛮大的,有些地方自己加深了理解,分享中front to back开头部分,原先8页太多,本应一笔带过,讲的时候废话太多,原本我只想阐述其中几个关键名词。
分享本身是一个学习,开拓眼界的的过程,切身体会,还是需要自己实践,写具体case。
上述基本上是我讲的过程记录,做个总结,以便回顾。
演示部分链接, demo
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。