最近在研读书籍 深入浅出nodejs , 随手写下的一些笔记, 和大家分享~ 如有错误,欢迎指正~
Node的出现,打破了前后端的壁垒,借助事件驱动和V8的高性能,成为服务端的佼佼者。接下来会展开描述Web应用在后端实现中的细节和原理
Cookie的一些设置:
Cookie的性能影响: 由于Cookie的实现机制,一旦服务器向客户端发送了设置Cookie的意图,除非Cookie过期,否则客户端每次发送请求都会发送这些Cookie到服务器端,一旦设置的Cookie过多,将会导致报文头较大。大多数的Cookie并不需要每次都用上,因为这会造成带宽的部分浪费 最好不要把Cookie设置在根节点下,否则几乎所有子路径下的请求都会带上这个Cookie。 为静态组件使用不同的域名(静态文件几乎不关心状态,Cookie对它而言几乎无用)(但是可能会增加DNS查询)
头部报文中的内容已经能够满足大多数的业务逻辑操作了,但是单纯的头部报文无法携带大量的数据,在业务中,我们往往需要接受一些数据,比如表单提交,文件提交,JSON上传,XML上传等。
Node中的http模块只对HTTP报文的头部进行了解析,然后触发request事件。如果请求中还带有部分内容(如POST具有报文和内容),内容部分需要用户自行接收和解析。通过报头的Transfer-Encoding或Content-Length即可判断请求中是否带有内容。在HTTP_Parser解析报文头部接受以后,报文内容会通过data事件触发,我们只需要以流的方式处理即可。(将接收到的buffer列表转化为一个Buffer对象后,再转换为没有乱码的字符串,挂载在req上)
表单数据,Content-type为application/x-www-urlencoded, 报文体内容跟查询字符串相同
JSON文件, application/json(再用JSON.parse解析)
xml文件,applicaton/xml (使用第三方库解析)
附件上传, multipart/form-data, 还有一个boundary分界符。
引入中间件来简化和隔离基础设施(例如查询字符串的解析,cookie的解析等)和业务逻辑之间的细节。让开发者关注在业务的开发上,以达到提高开发效率的目的。中间件的行为比较类似过滤器的行为,就是在进入具体业务逻辑之前,先让过滤器处理。每个中间件处理相对简单的逻辑,最终汇成强大的基础框架。
中间件的上下文也是请求对象和响应对象:req和res。由于Node异步的原因,我们需要提供一种机制,在当前中间件处理完成后通知下一个中间件执行。采用尾触发的方式实现。
const middleware = function(req, res, next) {
//TODO
next()
}
中间件异常处理:由于异步方法的异常不能直接捕获,中间件异步产生的异常需要自己抛出来( next(err)的方式 )。可以添加异常处理的中间件,多加一个参数(err, req, res, next)
中间件与性能:
Node选型V8,意味着他的模型和浏览器相似。JS运行在单进程的单线程上,好处是:程序状态单一,在没有线程的情况下没有锁、线程同步问题,操作系统在调度时也因为较少上下文的切换,可以很好的提高CPU的使用率。但是单进程单线程并非完美结构,如今CPU基本都是多核的,一个Node进程只能利用一个核,这将抛出Node实际应用的第一个问题:如何处分利用多核CPU服务器。另外,由于Node执行在单线程长,一旦异常没有被捕获,将会引起整个进程的奔溃。这就带来了第二个问题:如何保证进程的健壮性和稳定性。(严格意义上说,Node不是真正的单线程架构,存在一定的I/O线程存在)
石器时代: 执行模型是同步的,一次只为一个请求服务,所有请求按次序等待服务。假设每次响应服务耗用N秒,QPS(query per second)为1/N.
青铜时代:复制进程。为了解决同步架构的并发问题,简单的改进是通过进程的复制同时服务更多的请求和用户。每个请求都需要一个进程来服务,代价比较昂贵,相同状态会存在很多份,比较浪费。假设进程上限为M,QPS为 M/N
白银时代:多线程。为了解决多进程的浪费问题,多线程被引入,让一个线程服务一个请求。线程相对进程开销要小得多,并且线程之间可以共享数据。但是多线程的每个线程还是需要自己独立的堆栈,所以也只是比多进程略好。另外由于一个CPU核心在一个时刻只能做一件事,操作系统只能将CPU切片,让线程可以较为均匀地使用资源,但是切换线程也需要切换线程上下文。所以在大量并发时,还是无法做到强大的伸缩性。假设线程资源占用为进程的1/L,QPS为 M*L/N
黄金时代: 事件驱动。Apache是采用多线程/多进程模型,当并发数增长到上万时,内存消耗的问题就是暴露出来。为了解决高并发的问题,基于事件驱动的模型出现了,像Node和Nginx均是基于事件驱动的方式实现的,采用单线程避免了不必要的内存开销和上下文切换。问题就是:CPU利用率以及健壮性。
面对单进程单线程对于多核CPU利用不足的问题,就是启动多进程即可。理想状态下每个进程各自利用一个CPU,以此实现多核CPU的利用,Node提供了child_process模块,并且提供了child_process.fork()供我们实现进程的复制。
Master-Worker模式。进程分为两种:主进程和工作进程。主进程不服务具体的业务处理,而是负责调度或者管理工作进程。工作进程负责具体的业务处理。但是fork进程代价还是很昂贵的。我们只是为了充分利用多核CPU,不是为解决并发问题。
在Master-Worker模式中,要实现主进程管理和调度工作进程的功能,需要主进程和工作进程之间的通信。线程之间通过send方法发送数据,message事件实时监听收到的数据。通过fork创建子进程之后,为了实现父子进程之间的通信,父进程与子进程之间将会创建IPC(Inter-Process Communication)通道,父子进程之间才能通过message和send传递消息。
父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正创建子进程,并通过环境变量告诉子进程这个IPC通道的文件描述符。子进程在启动的过程中,根据文件描述符去连接这个已经存在的IPC通道,从而完成父子进程之间的连接。是一个双向通信,在系统内核中就完成了进程间通信,不需要进过实际的网络层。(只有启动的子进程是Node进程,子进程才会去连接IPC通道)
句柄传递: 句柄是一种可以用来表示资源的引用,它的内部包含了指向对象的文件描述符。比如句柄可以用来表示一个服务端socket对象,一个客户端socket对象,一个UDP套接字,一个管道等。发送句柄意味着,在主进程收到socket请求后,将这个socket直接发送给工作进程(或者说直接将一个TCP服务器发送给子进程)(并不意味着可以发送任意对象,只是消息传递,不是真正地传递对象,其中涉及的底层细节不赘述)。
端口共同监听: 依靠句柄传递实现了多个进程可以监听相同的端口而不引起异常。(对于sned发送的句柄还原出来的服务而言,他们的文件描述符是相同的,所以监听相同端口不会报错,但是文件描述符同一时间只能被某个进程使用,这些进程服务是抢占式的)
在多进程之间监听相同的端口,使得用户请求能够分散到多个进程上进行处理,者带来的好处是可以将CPU资源利用起来。但是不能让一个CPU忙不过来也不能让别的CPU闲着,这种保证多个处理单元工作量公平的策略叫做负载均衡。Node默认提供的机制是采用操作系统抢占式策略。一般而言这种抢占式对大家公平的,各个进程可以根据自己的繁忙度来进行抢占。但是对于Node而言,需要分清的是他的繁忙是由CPU、I/O两部分构成的,影响抢占的是CPU的繁忙度。对于不同业务可能存在I/O繁忙,而CPU空闲的情况,从而形成负载不均衡的情况。为此Node在V0.11中提供了一种新的策略使得负载均衡更加合理,这种新策略叫做Round-Robin,又叫轮叫调度。轮叫调度的工作方式是由主进程接受连接,将其依次分发给工作进程。
child_process模块比较偏向底层,后来Node提供了cluster模块,用以解决多核CPU的利用率问题,同时也提供了较完善的API,用以处理进程的健壮性问题。
cluster.isMaster
#如果该进程是主进程,则为 true。 这是由 process.env.NODE_UNIQUE_ID 决定的。 如果 process.env.NODE_UNIQUE_ID 未定义,则 isMaster 为 true。
cluster.isWorker
#如果该进程不是主进程,则为 true(与 cluster.isMaster 相反)。
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生工作进程。
for (let i = 0; i < numCPUs/2; i++) {
cluster.fork();
}
cluster.on('online', ()=> {
console.log('一个子进程出现了');
})
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是 HTTP 服务器。
http.createServer((req, res) => {
console.log(`${process.pid}工作`)
res.writeHead(200);
res.end('你好世界\n');
}).listen(8888);
console.log(`工作进程 ${process.pid} 已启动`);
}
事实上cluster就是child_process和net模块的组合应用。cluster启动时,如同我们之前说的,他会在内部启动TCP服务器,在cluster.fork()子进程时,将这个TCP服务器端socket的文件描述符发送给工作进程。如果进程时fork复制出来的,那么它的环境变量里就存在NODE_UNIQUE_ID, 如果工作进程中存在listen()监听端口的调用,它就将拿到该文件描述符,将端口重用,从而实现多个子进程共享端口。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。