Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

13 - Node #13

Open
Linjiayu6 opened this issue Jul 16, 2020 · 7 comments
Open

13 - Node #13

Linjiayu6 opened this issue Jul 16, 2020 · 7 comments

Comments

@Linjiayu6
Copy link
Owner

Linjiayu6 commented Jul 16, 2020

1 - 进程 & 线程

【1. 概念】
【进程】 process 资源分配和调度任务。

【单线程】进程(黑盒)是线程thread(工人)容器, 单线程是一个工人干活。
异步任务是走队列的, 同步任务过多占用主进程, 可能会导致线程阻塞, 就是只有一个人干活干太长时间了,其他任务block。

【进程通信】多进程用fork模式, 每个都是隔离独立空间(多造出来几个黑盒)。只能建立IPC通信, 进程间才能共享。
可以利用CPU核数, 创建多个进程。


【2. 模型】
【Node是单线程模型】基于事件驱动/非阻塞模型, 适合高并发场景 高I/O类型。
   原因: 利用事件循环机制,不是启动每个线程为每个请求服务,资源占用较少。
   避免线程创建和切换产生的资源开销。不适合密集计算类型, 会CPU飙升为服务挂掉情况。

   Node处理计算密集型问题: 长时间运行,占用资源,CPU时间片不会释放,后续I/O无法发起。

【Java是个多线程模型】利于密集计算类型。不适合高并发类型。
   原因在于多线程是每来一个请求都要创建线程, 执行期间需要切换线程带来的开销。


【3. 多进程 + 单线程模式 / 解决什么问题?】
- Node是单进程 + 单线程模式, 但是可以利用多核CPU, 多开辟几个进程空间, 就是多雇佣几个工人干活。
=> 扩展 多进程 + 单线程模式
- 多进程不是为了解决高并发, 主要解决的是 Node CPU利用率不足问题, 利用机器多核特性。

对于浏览器来说,为什么设计JS是单线程?

浏览器有 UI线程(渲染引擎是多线程) + JS单线程
UI线程和JS线程互斥,因为JS 会影响UI的,只能是JS空闲,UI才会更新。

假设JS是多线程,那么DOM操作也是多线程,DOM不知道听谁的了,都是leader很混乱。
不如只有一个worker 去调度,异步放到队列里。等待执行。

浏览器对异步机制 有两个队列 micro queue 和 macro queue
只有同步 > micro queue > macro queue 只有当前队列空了,才能走下一步

2 - Common.js Node模块化经历了什么?

Node模块分为: 核心模块 和 文件模块。
核心模块: fs http math ....
文件模块:
const a = require('/src/a.js')
1. 路径分析 '/src/a.js'
2. 文件定位
3. 编译执行

- 核心模块 (例如http, fs)  是直接被写到内存中,这样会二次加载更快。
- 文件模块 (RD写的代码),  运行时动态加载,按照路径分析,定位,再编译执行。
@Linjiayu6
Copy link
Owner Author

Linjiayu6 commented Jul 16, 2020

3 - 异步 I/O

I/O 概念

I/O分为 阻塞I/O 和 非阻塞I/O。
- 阻塞: [完整IO操作] 完成获取数据的过程,但是必须阻塞在那里,等待其返回。
- 非阻塞: [不完成操作] 不带数据直接返回,[轮训确认是否完成] 要获取数据,需要轮训再次读取。
这两种都不是完美的方案。 第一种浪费资源,导致CPU无法处理其他事情,第二种轮训会重复调用IO操作,CPU进入状态判断环节,也是对资源的浪费。

理想情况下 非阻塞I/O

1. A 发I/O请求 给B处理
2. B收到请求后, 立刻返回(无数据, 就是说明我在处理中)
3. waiting......
4. B处理后,返回数据给A

这种好处是 A不需要轮训一遍遍确认了。而是B完成就直接通知A

浏览器处理 事件循环 模型?

Node事件循环 处理非阻塞I/O机制 (和浏览器不一样了哦)

- 事件是基于循环处理的,类似while,是个典型的订阅/发布 或 生产者 / 消费者 模式。
- 每执行一次循环体 都是一次 tick
- 每次都去循环是否还有事件,有的话执行回调,没有则退出

A()
B()
C()
事件观察Observer 说 有A需要执行 => 
A去执行 => A是否有回调 => 有则执行回调 => 通知 事件观察 Observer 我执行完了 结果给你 => end
直到没有可执行的事件,线程结束。

image

setTimeout, setInterval / process.nextTick / setImmediate

  • timers 队列 [setTimeOut, setInterval]
    microqueue = [process.nextTick]
  • poll 队列
    microqueue = [process.nextTick]
  • check 队列 [setImmediate]
    microqueue = [process.nextTick]
poll 阶段是执行的, 分配回调去哪个队列。
process.nextTick:  事件循环当前阶段结束前执行。
process.nextTick 是属于 micro queue 每个阶段完成,都会执行micro queue。

setImmediate: 只有check阶段,当poll空了会去检查 是否有 setImmediate 任务,才会去转为check执行。

image
image

@Linjiayu6
Copy link
Owner Author

Linjiayu6 commented Jul 16, 2020

4 - 多进程创建 - child_process.fork() 模式 实现进程复制

  • 这种情况会占用资源,阻塞进程
const http = require('http')
const port = 8080
const url = '127.0.0.1'

// 长循环
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i
  }
  return sum
}

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    return res.end('ok')
  } else {
    // 服务端要处理计算类型, 同步且占用着资源, 这段时间服务器是不会对处理其他请求的。
    // 怎么去解决呢? 独立开个进程做这个事情
    longComputation()
    return res.end('after long computation, yes ok.....')
  }
})

server.listen(port, url, () => console.log(`server started at http://${url}:${port}`))

如何单开个进程处理?

  • child_process fork 方式
  • process.on process.send child.on child.send ....
// main.js 主进程
const http = require('http')
const port = 8080
const url = '127.0.0.1'
const { fork } = require('child_process');

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    return res.end('ok')
  } else {
    const compute = fork('child.js') // 子进程去执行计算, 此时主进程事件循环不会被阻塞
    compute.send('start')
    console.log('1. 子进程开始处理')

    compute.on('message', sum => { // 子进程监听message事件, 并处理后续
      console.log('3. 最后处理结果返回')
      res.end(`Sum is ${sum}`) 
    }) 
  }
})
server.listen(port, url, () => console.log(`server started at http://${url}:${port}`))
// child.js 子进程
/**
 * 该进程做计算的事情
 */
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 10000; i++) {
    sum += i
  }
  return sum
}

// 全局注册事件 process是 EventEmitter实例
process.on('message', msg => {
  var sum = longComputation()
  console.log('2. 子进程处理后 传给父进程', sum)
  process.send(sum) // 当完成肠循环后, 发送信息给父进程
})

@Linjiayu6
Copy link
Owner Author

Linjiayu6 commented Jul 16, 2020

5 - cluster 利用负载均衡 / 子进程集群

创建进程 / 父子进程通信

1. 进程创建 ?  child_process.fork / cluster.fork
2. 父子间进程通信 ?  IPC 发布订阅模式 (cluster 集成EventEmitter), 通过message / send() 传递
cluster
- 主进程管理 多个子进程模式
- 每个 worker 进程通过使用 child_process.fork() 函数,基于 IPC(Inter-Process Communication,进程间通信)
实现与 master 进程间通信
// 主(父)进程 master 管理 多个子进程 worker, 实现集群功能
var cluster = require('cluster')
var http = require('http')
var numCPUs = require('os').cpus().length // 获取CPU的个数

// 主进程
if (cluster.isMaster) {
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork()
  }
  // cluster 是个EventEmitter
  // cluster.workers 里面所有的子进程信息
  for (let workerId in cluster.workers) {
    // 子进程: 订阅者模式
    cluster.workers[workerId].on('message', data => {
      console.log('每个子进程绑定事件回调: ', data.msg) // 输出 'process.send 从父传给子'
    })
  }
  return
}

http.createServer(function(req, res) {
  res.writeHead(200);
  res.end("hello world\n")
  // 父进程: 发布信息给子
  process.send({ msg: 'process.send 从父传给子' })
}).listen(8000)

cluster & fork 区别?

cluster 和 fork 都可以创建主进程
但是 fork 没有负载均衡,cluster 内置负载均衡功能。
cluster.isMaster 用来判断是否为主进程? master进程主要负责接受和请求,在下发给子进程 worker 处理。

image

负载均衡

背景: 用户请求分散到 多个进程上进行处理。就是不能让一个工人太忙也不能太闲,要雨露均沾,工作量公平策略叫负载均衡。
Round-Robin策略: 轮叫调度,有主进程分配分发。N个任务,每次是 第i =( i + 1 ) mod N 来发送连接。

@Linjiayu6
Copy link
Owner Author

Linjiayu6 commented Jul 16, 2020

6 - cluster 工作原理 TODO

框架: master 进程下HTTP代理服务器 + 多个worker 进程的多个 HTTP代理服务器

clutster模块是 child_process 和 net 模块组合应用。

- 一个cluster 只能管理一组 worker 工作进程;
- net 模块和 socket有关联。

问题: 主master 监听了8080? 那么cluster fork出来的 是不是占用了8080端口?

端口仅由 master 进程中的内部TCP服务器监听一次。

@Linjiayu6
Copy link
Owner Author

7 - Express 和 Koa 的区别

异步流程控制

Express 基于 callback 来组合业务逻辑

Koa1 generator + co / Koa2 async/await 更同步的写法解决问题 语义上好些。
异步流程控制和异常捕获问题

Koa洋葱模型,await next() 控制下一个中间件

错误处理

Express callback来捕获错误,无法try catch,错误只能依赖回调
Koa try catch 来捕获

@Linjiayu6
Copy link
Owner Author

Linjiayu6 commented Jul 23, 2020

8 - Node 知道些什么?

child_process & cluster

【子进程概念】
- child process
- cluster 
  1. 内部是通过child process.fork创建子进程 
  2. 一个master管理多个worker  
      负载均衡 轮询的方式询问worker你忙不忙
      IPC方式进行通信

【多进程间通信】
- pipe 管道方式,类似于血缘关系,将一个进程的输出作为另外一个进程的输入。

【有多个进程,都创建了一个httpserver,  并同时监听一个端口 ? 共享端口?】
- net 模块中,对listen进行了处理。
- 1. master 进程: 正常监听
- 2. worker 进程: 创建 worker server实例, IPC 让master 也 创建个 worker server实例
       当请求来了,master 将请求转发给 worker server实例。

EventEmitter
http 网络

事件循环机制

有6个主要的阶段:
外部输入 => 
poll 轮询阶段 => 
check 检查阶段 => 
close 关闭事件回调阶段 => 
timers 定时器检测 => 
I/O 事件回调 => 
闲置阶段(idle, prepare) =>
poll 轮询阶段 => ...... 反反复复

1.【timers】setTimeout/setInterval 执行回调
2. 【I/O callbacks】除了timers / setImmidate 执行回调
3. 【prepare】
4. 【poll】等待新I/O Node会阻塞在这里
5. 【check】setImmidate 执行回调
6. 【close callbacks】close事件的回调

- setImmediate & setTimeout 相似 调用时机不同
1. setImmediate  - poll 阶段完成时去下个阶段 check 执行
2. setTimeout - poll空闲时候,即到了timers阶段 执行

- process.nextTick 独立于Event Loop 有自己的队列, 且优先于  微队列
- 每个阶段都有 微队列 见缝插针

同步 > process.nextTick > 微队列 (promise.then) > setImmidate(check) > 宏队列 ( timers: setTimeout/setInterval)

总结

浏览端: 同步 > microqueue(async await 后 / promise.then xx) > macroqueue (setTimeout / setInterval)

服务端: 事件循环机制分为6个阶段,主要三个阶段是 poll => check => timers
poll 主要是接收 新的I/O
check 是当poll为空, poll 到 check阶段 会往下走 (setImmidate)
timer 是setTimeout  / setInterval 执行

还涉及两个点: 1. process.next 2. 微队列(promise.then / await 后)
1. process.next 是 独立队列, 优于所有
2. 微队列 是在每个阶段间去执行

process.next -> poll -> micro queue -> check -> micro queue -> timer -> micro queue -> .....

https://blog.fundebug.com/2019/01/15/diffrences-of-browser-and-node-in-event-loop/

@Linjiayu6
Copy link
Owner Author

Linjiayu6 commented Jul 23, 2020

Vue.nextTick 约等于 this.setState

  • Vue NextTick: 数据更新了 DOM渲染 / 变更了,自动执行该函数。即DOM更新后调用。
  • 场景: 改变DOM数据 是基于新DOM的,那就需要针对 新DOM JS操作 放在Vue.nextTick()的回调函数中。
原因是,Vue是异步执行dom更新的,一旦观察到数据变化,
Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。

如果这个watcher被触发多次,只会被推送到队列一次。
这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOm操作。
而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
当你设置 vm.someData = 'new value',DOM 并不会马上更新,
而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。
如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。
为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。
这样回调函数在 DOM 更新完成后就会调用。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant