之前一篇 文章只是讲述了node
的其中较浅层的事件机制。但是 当我们在启动一个 Node
服务的时候我们”做“了什么?
众所周知,Node
的强项是I/O
密集型,为事件驱动型,当某个I/O
执行完毕时,将以事件的形式通知执行 I/O 操作的线程,线程执行这个事件的回调函数。而弱项在于处理CPU
密集型的任务,比如在内存中使用一个大的数据集进行复杂计算,它会阻塞掉其他进程的任务。同样的,当你在发起一个有 CPU 密集型任务的远程接口请求时,也同样会阻塞掉其他需要被执行的请求。
为了解决高并发的问题,我们所熟知的很多服务器多是多线程的模式,优点在于: 线程相对进程开销较小,且线程之间可以共享数据,并且利用线程池可以减少创建和销毁线程的开销。 缺点在于:过多的线程会导致消耗大量的时间在切换上下文中,所以大并发量时,还是无法做到强大的伸缩性。
Node.js
底层是C++
(V8
也是C++
写的)。底层代码中,近半数都用于事件队列、回调函数队列的构建。用事件驱动来完成服务器的任务调度,犹如"针尖上的舞蹈",用一个线程,担负起了处理非常多的任务的使命。
当开启一个 Node 服务时,发生了什么?
我们将一段简单的代码用于开启一个服务器:
1 | var http = require("http"); |
通过一个简单的开启一个 Node
服务器来看一下全局 global
对象:
实际上,就是一个 process
对象,pid
为 33334
, 即一个进程。
Process 是一个全局对象,无需 require 直接使用,给我们提供了当前进程中的相关信息。官方文档提供了详细的说明。
- process.env: 环境变量,例如通过 process.env.NODE_ENV 获取不同环境项目配置信息
- process.nextTick:这个在谈及 EventLoop 时经常为会提到
- process.pid:获取当前进程id
- process.ppid:当前进程对应的父进程(子进程的所有资源都继承父进程,得到父进程资源的副本,但是两个是单独的进程,继承了以后二者就没有什么关联了,子进程单独运行,因此不同于进程和线程的关系)
- process.cwd():获取当前进程工作目录,
- process.platform:获取当前进程运行的操作系统平台
- process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值
- 进程事件: process.on(‘uncaughtException’,cb) 捕获异常信息、 process.on(‘exit’,cb)进程推出监听
- 三个标准流: process.stdout 标准输出、 process.stdin 标准输入、 process.stderr 标准错误输出 (node 的
console.log
实际上就是对stdout
,stderr
的封装) - process.title 指定进程名称,有的时候需要给进程指定一个名称
除了线程,当开启了一个Node服务时,实际上开启了:
- One process: 一个 全局的
process
对象。 a process is a global object that can be accessed anywhere and has information about what’s being executed at a time. - One thread: 单线程 (单线程就是一个进程只开一个线程)
- One event loop:一个事件循环, 这是 Node 最核心的一点,使得
Node
可以异步执行,以及拥有非阻塞 IO。 - One JS Engine Instance:一个 JS 引擎实例。
- One Node.js Instance: 一个
NodeJs
应用实例。
关于Node单线程的误区
虽说Node是单线程,但是实际上查看实际程序运行时的树形图:
那么,为什么一个服务,Node
启动了不止一个进程呢?
实际上:
Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的。
- 主线程:编译,执行代码。
- 编译、优化线程: 在主线程执行的时候,可以优化代码。
- 分析线程: 记录分析线程运行的时间,为
Cranksharft
优化代码执行提供依据。 - 垃圾回收的几个线程。
因此,所谓的Node
单线程是指Javascript
的执行是单线程的,但JavaScript的宿主环境,无论是Node
还是浏览器都是多线程的,因为libuv
中类似线程池的概念,libuv
会通过操作类似线程池的实现来模拟不同操作系统的异步调用。这对开发者来说是不可见的。
libuv是一个跨平台IO库,它结合了UNIX下的libev和windows下的IOCP的特性,最早由Node的作者开发,专门为 Node 提供多平台下的异步IO支持。Libuv本身由C++语言实现,Node中的非阻塞IO以及事件循环的底层机制都是由libuv实现的。
Node的异步调用实际上是由Libuv来支持的。而读文件等的实际操作是由Libuv来完成。
Node多进程
我们知道单线程会带来弊端:
- 无法利用多核CPU
- 错误会引起整个应用退出
针对以上两个比较棘手的问题的解决方案:
对于1:
- 一些管理工具比如pm2. forver等都可以实现创建多进程解决多核CPU的利用率问题。
-在V0.8之前,实现多进程可以使用child_process
. - 在v0.8之后,可以使用
cluster
模块。通过主从模式,创建多个工作进程。
对于2:
- Nginx反向代理,负载均衡,开多个进程,绑定多个端口。
- 一些管理工具比如
pm2
(实际上也是使用cluster
),forver
等都可以实现进程监控,错误自动重启等。 - 开多个进程监听同一个窗口,使用Node提供的
cluster
模块。
Node的作用域及Global对象相关
我们知道,node 是遵循 CommonJs
规范,node以文件为模块,其中module和exports用于输出模块,require用于引用模块。而全局变量global
,则应该是模块中都能访问得到的。
Global Objects全局对象These objects are available in all modules. Some of these objects aren’t actually in the global scope but in the module scope - this will be noted.(这些对象在所有的模块中都可用。实际上有些对象并不在全局作用域范围中,但是在它的模块作用域中------这些会标识出来的。)
全局对象这个概念我想大家应该不会感到陌生,在浏览器中,最高级别的作用域是Global Scope ,这意味着如果你在Global Scope中使用 “var” 定义一个变量,这个变量将会被定义成Global Scope。
比如:
1 | var a = 1; |
In Node this is different. The top-level scope is not the global scope;var somethinginside a Node module will be local to that module.
(但是在NodeJS里是不一样的,最高级别的Scope不是Global Scope,在一个Module里用 “var” 定义个变量,这个变量只是在这个Module的Scope里。)
1 | var name = 'var-name'; |
说到 node 的作用域相关,就不得不在这里mark一下 exports
和 module.exports
的区别。
module.exports 初始值为一个空对象 {}.
exports 是指向的 module.exports 的引用。
require() 返回的是 module.exports 而不是 exports。
例子:
1 | //utils.js |
可以看出,require 引用的是 module.exports
的内容。
参考: