node 相关内容总结

架构

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 的稳定性

      1. 父进程监听子进程心跳,3次没心跳,杀掉子进程;
      1. 子进程监控内存占用过大,主动退出;
      1. 父进程监听子进程退出事件,并延迟创建子进程;
      1. 子进程捕获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 会在父进程与子进程间,建立一个通信管道,用于进程之间的通信
    1. child_process.spawn() child_process.spwanSync()

      • spawn() 会异步的衍生子进程,spawnSync() 方法则以同步的方式提供同样的功能 ,但会阻塞事件循环、知道衍生的子进程退出或者终止
  • send() 用于向新进程发送消息,新进程中通过监听 message 事件,来获取消息

worker threads