架构
javasript 应用端
V8引擎
node api
os 系统
-进程同步
解决办法一:callback
- 这种方法虽然能快速的解决,但暴露的问题也很明显,一是代码维护不方面,二是代码的深层嵌套看起来很不舒服。这种方法并不可取。
解决方法二:递归调用
- 先将多个函数组成一个数组。再可以利用递归函数的特性,使程序按照一定的顺序执行。
解决方法三:调用类库
- 随着nodejs的发展,响应的类库也越来越多。Step和async 就是其中不错的。
进程
操作系统挂载运行程序的单元
拥有独立的资源-内存等
child_process
并行一定并发,并发未必并行。
协程
协程可以理解为特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行
有一点必须明确的是,一个线程的多个协程的运行是串行的
线程
运算调度的单元
进程内的线程共享相关资源
worker_threads
洋葱模型
处理顺序从左到右,左边接收一个request,右边输出返回response
一般的中间件都会执行两次,调用next之前为第一次,调用next时把控制传递给下游的下一个中间件。当下游不再有中间件或者没有执行next函数时,就将依次恢复上游中间件的行为,让上游中间件执行next之后的代码
单线程、异步、 非阻塞 IO
单线程
硬件上:创建线程和线程上下文切换有时间开销。
软件上:多线程编程模型的死锁、状态同步等问题让开发者头疼
非阻塞 IO
- 非阻塞I/O通过轮询实现的
10.5.0之前
- 具有多个占用大量 CPU 的函数,将会导致服务器吞吐量的显着下降。在最坏的情况下,服务器将会失去响应,并且无法将任务委派给工作池。
10.5.0以后
- worker_threads 模块使多线程变得简单
RPC 调用 —两个服务端之前的通讯 —相关协议
与ajax 不同
不一定用DNS作为寻网服务,因为都是内网
- 寻址/负载均衡,使用特有的服务进行寻址
应用层协议一般不用http — 用二进制协议
二进制协议
更小的数据体积
更快的编码效率
基于TCP udp 协议
TCP 通讯
单工通讯-只能单向通
半双工通讯,同时只有一端发送
全双工通讯
buffer
alloc
from
protocol-buffer
net
node的事件循环
主线程 运行 v8 与js的
多个子线程通过事件循环被调度
- 做完后在回掉给主线程
原理
Node.js的I/O 处理完之后会有一个回调事件,这个事件会放在一个事件处理队列里头,在进程启动时node会创建一个类似于While(true)的循环,它的每一次轮询都会去查看是否有事件需要处理,是否有事件关联的回调函数需要处理,如果有就处理,然后加入下一个轮询,如果没有就退出进程,这就是所谓的“事件驱动”
node中事件循环的实现是依靠的libuv引擎
这里主要说明的是 node11 前后的差异,因为 node11 之后一些特性已经向浏览器看齐了,总的变化一句话来说就是,如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行对应的微任务队列,一起来看看吧~
- node11.x 之前,其事件循环的规则就如上文所述:先取出完一整个宏任务队列中全部任务,然后执行一个微任务队列。
node 红任务按着 执行阶段 执行的 微任务也是nexttick 优先执行
macro-task 大概包括:
setTimeout
setInterval
setImmediate
script(整体代码)
I/O 操作等。
micro-task 大概包括:
process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
new Promise().then(回调)等
浏览器事件循环
除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行
宏任务
script(整体代码)
setTimeout
setInterval
setImmediate
I/O
UI render
微任务
process.nextTick
- (与普通微任务有区别,在微任务队列执行之前执行)
Promise
Async/Await(实际就是promise)
MutationObserver(html5新特性)
执行阶段
输入数据阶段(incoming data)->轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器检测阶段(timers)->I/O事件回调阶段(I/O callbacks)->闲置阶段(idle, prepare)->轮询阶段…
定时器检测阶段(timers):本阶段执行 timer 的回调,即 setTimeout、setInterval 里面的回调函数。并且是由 poll 阶段控制的,同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。
I/O事件回调阶段(I/O callbacks):执行延迟到下一个循环迭代的 I/O 回调,即上一轮循环中未被执行的一些I/O回调。
闲置阶段(idle, prepare):仅系统内部使用。
轮询阶段(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到 timers 阶段。
如果没有定时器, 会去看回调函数队列。
如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
如果 poll 队列为空时,会有两件事发生
如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去,一段时间后自动进入 check 阶段。
检查阶段(check):setImmediate() 回调函数在这里执行
关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on(‘close’, …)。
性能优化
测试
ab 压力测试
webbench
找性能瓶颈
top
- cpu 内存使用情况
iostat
- 硬盘
后端服务器
性能分析
chrome devtool
- inspect
node 自带的profile
js优化
减少不必要的计算
空间换时间
垃圾回收
新生代
- 容量小,回收快
老圣代
- 容量大,回收慢
减少内存使用
buffer 内存分配策略
大于8kb 小于 8kb 情况
多次创建new char【】 数组 浪费,所以大于8kb的就会创建一个大数组,如果遇见小8kb的时候 切一小块给他,如果不够用了在申请一个,如果被销毁会被释放空间。这样减少内存分配的消耗
动静分离
静态内容
- 基本不会动,也不会因为参数不同改变 —手段cdn分发,http缓存
动态内容
- 因参数变动 —–手段 用大量的源站机器承载,结合反向代理(ngnix)进行负载均衡(找到负载最小的机器相应请求)
反向代理与缓存服务
location
proxy_pass
proxy_cache
redis 缓存
cluster模块 多进程
cluster.isMaster 主进程
- cluster.fork() 创建子进程
进程守护
node 的稳定性
- 父进程监听子进程心跳,3次没心跳,杀掉子进程;
- 子进程监控内存占用过大,主动退出;
- 父进程监听子进程退出事件,并延迟创建子进程;
- 子进程捕获uncautht异常,并主动退出;
pm2
forever
防止出现单点故障,提供主从备份服务器。
node 内置模块
文件模块
路径模块
事件模块
http模块
buffer
Buffer 被引入用以帮助开发者处理二进制数据
Buffer 与流紧密相连。 当流处理器接收数据的速度快于其消化的速度时,则会将数据放入 buffer 中。
流 模块
一种以高效的方式处理读/写文件、网络通信、或任何类型的端到端的信息交换
内存效率: 无需加载大量的数据到内存中即可进行处理。
时间效率: 流式处理数据- 当获得数据之后即可立即开始处理数据,这样所需的时间更少,而不必等到整个数据有效负载可用才开始。
负载均衡
用来在多个计算机、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的
负载均衡(Load Balance)是建立在网络协议分层上的
解决互联网架构中的高并发和高可用的问题
多进程模型
Cluster 模块
集成了child_process.fork方法创建node子进程的方式
集成了根据多核CPU创建子进程后,自动控制负载均衡的方式。
Master-Worker模式
Cluster 模块允许设立一个主进程和若干个 worker 进程,由主进程监控和协调 worker 进程的运行,worker 之间采用进程间通信交换信息,cluster 模块内置一个负载均衡器,协调各个进程之间的负载。这是典型的分布式架构中用于并行处理业务的模式,具备较好的可伸缩性和稳定性。主进程不负责具体的业务处理,而是负责调度或管理工作进程,他是趋向于稳定为。工作进程负责具体的业务处理。
父进程在实际创建子进程之前,会创建 IPC通道并监听它,然后才 真正的创建出 子进程,这个过程中也会通过环境变量(NODECHANNELFD)告诉子进程这个IPC通道的文件描述符
1、主进程和子进程 主进程和子进程通过
IPC
通信子进程与子进程
一对多,可以通过父进程进行分发
一对一,可以通过ipc通信
子进程在启动的过程中,根据文件描述符去连接这个已存在的IPC通道,从而完成父子进程之间的连接。
Node.js进程通信原理
该负载均衡使用了 Round-robin算法(也被称之为循环算法)
node 监听同一个80 端口
一开始依然是 master 进程监听 80,当收到用户请求之后,master 并不是直接把这些数据扔给 worker,而是在 80 端口接收到数据后,生成对应的 socket,再把该 socket 对应的文件描述符通过管道传给 worker,
在之后 master 停止监听 80port,因为已经把文件描述符给了 worker,之后 worker 直接监听这个套接字即可
为啥这个时候不会端口冲突??
第一个是,Node 对每个端口监听设置了SO_REUSEADRR,标示可以允许这个端口被多个进程监听。
第二个点是,用这个的前提是每个监听这个端口的进程,监听的文件描述符要相同。
为什么通过 master 传给 worker 就可以了呢?
- 因为 master 在与 worker 通信的时候,每个子进程收到的文件描述符都是一样的(通过 master 传入,不理解的参见上面双工通信的讲解),这个时候就是所有子进程监听相同的 socket 文件描述符,就可以实现多个进程监听同一个端口的目标啦~。
child_process
child_process.fork()函数来进行进程的复制。
- fork() 用于直接创建一个子进程会返回一个 childProcess 对象,与spawn方法不同的是,fork 会在父进程与子进程间,建立一个通信管道,用于进程之间的通信
child_process.spawn() child_process.spwanSync()
- spawn() 会异步的衍生子进程,spawnSync() 方法则以同步的方式提供同样的功能 ,但会阻塞事件循环、知道衍生的子进程退出或者终止
send() 用于向新进程发送消息,新进程中通过监听 message 事件,来获取消息