# 大规模NodeJS项目架构与优化

# NodeJS异步IO原理浅析及优化方案

# 异步IO的是与非

异步IO的好处:

  • 前端通过异步IO可以消除UI堵塞。
  • 假设请求资源A的时间为M,请求资源B的时间为N.那么同步的 请求耗时为M+N.如果采用异步方式占用时间为Max(M,N)。
  • 随着业务的复杂,会引入分布式系统,时间会线性的增加, M+N+...和Max(M,N…),这会放大同步和异步之间的差异。
  • I/O是昂贵的,分布式I/O是更昂贵的。
  • NodeJS 适用于IO密集型不适用CPU密集型
  • 并不是所有都用异步任务好,遵循一个公式: s= (Ws+Wp)/(Ws+Wp/p) Ws 表示同步任务,Wp 表示异步任务,p 表示处理器的数量。

一些底层的知识: 1.CPU时钟周期:1/cpu主频 -> 1s/3.1 GHz 2.一波复杂的数学公式 3.操作系统对计算机进⾏行行了了抽象,将所有输⼊入输出设备抽 象为⽂文件。内核在进⾏行行⽂文件I/O操作时,通过⽂文件描述符进 ⾏行行管理理。应⽤用程序如果需要进⾏行行IO需要打开⽂文件描述符, 在进⾏行行⽂文件和数据的读写。异步IO不不带数据直接返回,要 获取数据还需要通过⽂文件描述符再次读取。

# Node对异步IO的实现

完美的异步IO应该是应该是应⽤用程序发起⾮非阻塞 调⽤用,⽆无需通过遍历或者事件幻想等⽅方式轮询。

图;

  • 应用程序先将 JS 代码经 V8 转换为机器码。
  • 通过 Node.js Bindings 层,向操作系统 Libuv 的事件队列中添加一个任务。
  • Libuv 将事件推送到线程池中执行。
  • 线程池执行完事件,返回数据给 Libuv。
  • Libuv 将返回结果通过 Node.js Bindings 返回给 V8。
  • V8 再将结果返回给应用程序。

Libuv 实现了 Node.js 中的 Eventloop ,主要有以下几个阶段: 图;

  • imers:执行 setTimeout 和 setInterval 中到期的 callback。
  • pending callbacks:上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。
  • idle, prepare:仅内部使用。
  • poll:最为重要的阶段,执行 I/O callback,在适当的条件下会阻塞在这个阶段。
  • check:执行 setImmediate 的 callback。
  • close callbacks:执行 close 事件的 callback,例如 socket.on("close",func)。

# 几个特殊的API

  1. SetTimeout和SetInterval 线程池不参与
  2. process.nextTick() 实现类似 SetTimeout(function(){},0);每次调用放入队列中, 在下一轮循环中取出。
  3. setImmediate();比process.nextTick()优先级低
  4. Node如何实现一个Sleep?

几个特殊的API

setTimeout(function () {    
    console.log(1); 
}, 0); 
setImmediate(function () {    
    console.log(2); 
}); 
process.nextTick(() => {    
    console.log(3); 
}); 
new Promise((resovle,reject)=>{    
    console.log(4);    
    resovle(4); 
}).then(function(){    
    console.log(5); 
}); 
console.log(6); 
// 4 6 3 5 1 2 
// 先执行同步,promise里的按同步执行所以先输出4 6
// 执行完同步后执行nextTick 所以输出3
// 接下来执行promise.then里的内容 输出5
// setTimeout 设置为 0ms 时,setTimeout 先执行 所以输出1 2, 如果setTimeout 设置了延迟时间的话,则先执行 setImmediate
// setTimeout setImmediate process.nextTick 不参与eventloop 都有自己的tracker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

实现一个一个sleep

async function test() {
    console.log('Hello')
    await sleep(1000)
    console.log('world!')
    }
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
    } test()
1
2
3
4
5
6
7
8

# 函数式编程在Node中的应用

  1. 高阶函数:可以将函数作为输入或者返回值,形成一种 后续传递风格的结果接受方式,而非单一的返回值形式。 后续传递风格的程序将函数业务重点从返回值传递到回调 函数中。
app.use(function(){//todo})。
var emitter = new events.EventEmitter;
emitter.on(function(){//……….todo}) 
1
2
3
  1. 偏函数:指定部分参数产生一个新的定制函数的形式就 是偏函数。Node中异步编程非常常见,我们通过哨兵变量 会很容易造成业务的混乱。underscore,after变量

# 常用的Node控制异步API的技术手段

  1. Step、wind(提供等待的异步库)、Bigpipe、Q.js
  2. Async、Await
  3. Promise/Defferred是一种先执行异步调用,延迟传递的处理方式。Promise是 高级接口,事件是低级接口。低级接口可以构建更多复杂的场景,高级接口一 旦定义,不太容易变化,不再有低级接口的灵活性,但对于解决问题非常有效
  4. 由于Node基于V8的原因,目前还不支持协程。协程不是进程或线程,其执行 过程更类似于子例程,或者说不带返回值的函数调用。
  • 一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我 们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受 系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由 当前协程切换到其他协程由当前协程来控制。

# NodeJS内存管理机制及内存优化

# V8垃圾回收机制

  • Node使用JavaScript在服务端操作大内存对象受到了一定的限制(堆 区),64位系统下约为1.4GB,栈区32位操作系统下是0.7G.新生代 64位是32M 32位是16M node —max-new-space-size app.js: -max-old-space-size app.js
  • Process.memoryUsage->rss、heaptTotal、heapUsed
  • V8的垃圾回收策略主要基于分代式垃圾回收机制。在自动垃圾回 收的演变过程中,人们发现没有一种垃圾回收算法能够胜任所有 场景。V8中内存分为新生代和老生代两代。新生代为存活时间较 短对象,老生代中为存活时间较长的对象。
  • 一句话表示:小孩子尽管玩,到处丢东西大人收。

# Scavenge算法

在分代基础上,新生代的对象主要通过Scavenge算法进行 垃圾回收,再具体实现时主要采用Cheney算法。Cheney算 法是一种采用复制的方式实现的垃圾回收算法。它将内存 一分为二,每一个空间称为semispace。这两个semispace中 一个处于使用,一个处于闲置。处于使用的称之为From, 闲置的称之为To.分配对象时先分配到From,当开始进行垃 圾回收时,检查From存活对象赋值到To.非存活被释放。 然后互换位置。再次进行回收,发现被回收过直接晋升, 或者发现To空间已经使用了超过25%。他的缺点是只能使 用堆内存的一半,这是一个典型的空间换时间的办法,但 是新生代声明周期较短,恰恰就适合这个算法。

图;

a 和 b 在内存空间from被声明, a 被 alert 引用,进行垃圾回收时检查到 a被引用(存活),所以 a 和 alert(a) 都被赋值到To b 被释放。然后from和to互换位置,继续。当进行了5次互换后还没有回收的东西放到了老生代,或者To已经使用了超过25%也会放到老生代。

# Mark-Sweep & Mark-compact

V8老生代主要采用Mark-Sweep和Mark-compact,在使 用Scavenge不合适。一个是对象较多需要赋值量太大 而且还是没能解决空间问题。Mark-Sweep是标记清 楚,标记那些死亡的对象,然后清除。但是清除过 后出现内存不连续的情况,所以我们要使用Markcompact,他是基于Mark-Sweep演变而来的,他先将 活着的对象移到一边,移动完成后,直接清理边界 外的内存。当CPU空间不足的时候会非常的高效。 V8后续还引入了延迟处理,增量处理,并计划引入 并行标记处理。

图;

图中上面两条是Mark-Sweep,下面两条是Mark-compact Mark-compact 是 Mark-Sweep 的进阶版 Mark-Sweep先将要回收的变量进行标记(标上了小星星),然后将他们回收,会造成内存不连续。

Mark-compact 则是先将有用的变量一起推到一边,把无用的留下,然后一起清除掉

图;

# 引用计数DEMO(快照查看)

 function Yideng(name) {
            this.name = name;
}
//demo1
let student1 = new Yideng();
let student2 = new Yideng();
setTimeout(function () {
    student1 = null;
},3000);
//demo2
let student1 = new Yideng("zhijia");
let ydSet = new Set();
ydSet.add(student1)
student1 = null;
ydSet = null;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//闭包在堆区
let YidengFactory = function (name) {
    let student = new Yideng(name);
    return function () {
        console.log(student);
    }
}
let p1 = YidengFactory("⽼袁");
p1();
p1 = null;
1
2
3
4
5
6
7
8
9
10

图;

# 常见内存泄露问题

  • Case1:无限制增长的数组
  • Case2:无限制设置属性和值
  • Case3:任何模块内的私有变量和方法均是永驻 内存的 a = null
  • Case4: 大循环,无GC机会

# 内存泄露分析

node-inspector console.log("Server PID", process.pid); top -pid 2322 sudo node --inspect app.js while true; do curl "http://localhost:1337/"; done

# 大规模Node站点结构原理分析

# 经典的MVC框架

图;

# NET多层架构

图;

图;

# JavaWeb多层架构

图;

图;

# 服务器集群管理与Node集群的应用

# 预备上线

  • 前端工程化的搭载动态文件的MAP分析压缩打 包合并至CDN
  • 单测、压测 性能分析工具发现Bug
  • 编写nginx-conf实现负载均衡和反向代理
  • PM2启动应用程序小流量灰度上线,修复BUG
  • 上线前的不眠夜,你见过凌晨5点的北京么?

# 多线程

  • Master进程均为主进程,Fork可以创造主从进程。
  • 通过child_process可以和NET模块组合,可以创建多个线程并监 听统一端口。通过句柄传递完成自动重启、发射自杀信号、 限量重启、负载均衡。
  • Node默认的机制是采用操作系统的抢占式策略。闲着的进程争 抢任务,但是会造成CPU闲置的IO暂时并未闲置。Node后来引 入了Round-Robin机制,也叫轮叫调度。主进程接受任务,在发
  • 每个子进程做好自己的事,然后通过进程间通信来将他们连 接起来。这符合Unix的设计理念,每个进程只做一件事,并做 好。将复杂分解为简单,将简单组合成强大。
var cluster = require('cluster'); 
var http = require('http'); 
var numCPUs = require('os').cpus().length; 
if (cluster.isMaster) {   
    require('os').cpus().forEach(function(){     
        cluster.fork();   
    });   
    cluster.on('exit', function(worker, code, signal) {     
        console.log('worker ' + worker.process.pid + ' died');   
    });   
    cluster.on('listening', function(worker, address) {       
        console.log("A worker with #"+worker.id+" is now connected to " + address.address + ":" + address.port); 
    });
} else {   
    http.createServer(function(req, res) {     
        res.writeHead(200);     
        res.end("hello world\n");     
        console.log('Worker #' + cluster.worker.id + ' make a response');   
    }).listen(8000); 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# PM2

pm2 是一个带有负载均衡功能的Node应用的进程管理器. 当你要把你的独立代码利用全部的服务器上的所有CPU,并保证进程永远都活 着,0秒的重载。

  1. 内建负载均衡(使用Node cluster 集群模块)
  2. 后台运行
  3. 0秒停机重载
  4. 具有Ubuntu和CentOS 的启动脚本
  5. 停止不稳定的进程(避免无限循环)
  6. 控制台检测
  7. 提供 HTTP API
  8. 远程控制和实时的接口API ( Nodejs 模块,允许和PM2进程管理器交互 ) 测试过Nodejs v0.11 v0.10 v0.8版本,兼容CoffeeScript,基于Linux 和MacOS.

图;

图;

# UV过千万的Node站点真身

更新时间: 11/8/2019, 4:51:43 PM