关于node.js:说说Nodejs高并发的原理

写在后面

咱们先来看几个常见的说法

  • nodejs是单线程 + 非阻塞I/O模型
  • nodejs适宜高并发
  • nodejs适宜I/O密集型利用,不适宜CPU密集型利用

在具体分析这几个说法是不是、为什么之前,咱们先来做一些筹备工作

从头聊起

一个常见web利用会做哪些事件

  • 运算(执行业务逻辑、数学运算、函数调用等。次要工作在CPU进行)
  • I/O(如读写文件、读写数据库、读写网络申请等。次要工作在各种I/O设施,如磁盘、网卡等)

一个典型的传统web利用实现

  • 多过程,一个申请fork一个(子)过程 + 阻塞I/O(即blocking I/O或BIO)
  • 多线程,一个申请创立一个线程 + 阻塞I/O

多过程web利用示例伪代码

listenFd = new Socket(); // 创立监听socket
Bind(listenFd, 80); // 绑定端口
Listen(listenFd);   // 开始监听

for ( ; ; ) {
    // 接管客户端申请,通过新的socket建设连贯
    connFd = Accept(listenFd);
    // fork子过程
    if ((pid = Fork()) === 0) {
        // 子过程中
        // BIO读取网络申请数据,阻塞,产生过程调度
        request = connFd.read();
        // BIO读取本地文件,阻塞,产生过程调度
        content = ReadFile('test.txt');
        // 将文件内容写入响应
        Response.write(content);
    }
}

多线程利用实际上和多过程相似,只不过将一个申请调配一个过程换成了一个申请调配一个线程。线程比照过程更轻量,在系统资源占用上更少,上下文切换(ps:所谓上下文切换,略微解释一下:单核心CPU的状况下同一时间只能执行一个过程或线程中的工作,而为了宏观上的并行,则须要在多个过程或线程之间按工夫片来回切换以保障各进、线程都有机会被执行)的开销也更小;同时线程间更容易共享内存,便于开发

上文中提到了web利用的两个外围要点,一个是进(线)程模型,一个是I/O模型。那阻塞I/O到底是什么?又有哪些其余的I/O模型呢?别着急,首先咱们看一下什么是阻塞

什么是阻塞?什么是阻塞I/O?

简而言之,阻塞是指函数调用返回之前,当后退(线)程会被挂起,进入期待状态,在这个状态下,当后退(线)程暂停运行,引起CPU的进(线)程调度。函数只有在外部工作全副执行实现后才会返回给调用者

所以阻塞I/O是,应用程序通过API调用I/O操作后,当后退(线)程将会进入期待状态,代码无奈持续往下执行,这时CPU能够进行进(线)程调度,即切换到其余可执行的进(线)程继续执行,当后退(线)程在底层I/O申请解决完后才会返回并能够继续执行

多进(线)程 + 阻塞I/O模型有什么问题?

在理解了什么是阻塞和阻塞I/O后,咱们来剖析一下传统web利用多进(线)程 + 阻塞I/O模型有什么弊病。

因为一个申请须要调配一个进(线)程,这样的零碎在并发量大时须要保护大量进(线)程,且须要进行大量的上下文切换,这都须要大量的CPU、内存等系统资源撑持,所以在高并发申请进来时CPU和内存开销会急剧回升,可能会迅速拖垮整个零碎导致服务不可用

nodejs利用实现

接下来咱们看看nodejs利用是如何实现的。

  • 事件驱动,单线程(主线程)
  • 非阻塞I/O
    在官网上能够看到,nodejs最次要的两大特点,一个是单线程事件驱动,一个是“非阻塞”I/O模型。单线程 + 事件驱动比拟好了解,前端同学应该都很相熟js的单线程和事件循环这套机制了,那咱们次要来钻研一下这个“非阻塞I/O”是怎么一回事。首先来看一段nodejs服务端利用常见的代码,
const net = require('net');
const server = net.createServer();
const fs = require('fs');

server.listen(80);  // 监听端口
// 监听事件建设连贯
server.on('connection', (socket) => {
    // 监听事件读取申请数据
    socket.on('data', (data) => {
    // 异步读取本地文件
    fs.readFile('test.txt', (err, data) => {
            // 将读取的内容写入响应
            socket.write(data);
            socket.end();
        })
    });
});


能够看到在nodejs中,咱们能够以异步的形式去进行I/O操作,通过API调用I/O操作后会马上返回,紧接着就能够继续执行其余代码逻辑,那为什么nodejs中的I/O是“非阻塞”的呢?答复这个问题之前咱们再做一些筹备工作,参考nodejs进阶视频解说:进入学习

read操作根本步骤

首先看下一个read操作须要经验哪些步骤

  • 用户程序调用I/O操作API,外部收回零碎调用,过程从用户态转到内核态
  • 零碎收回I/O申请,期待数据筹备好(如网络I/O,期待数据从网络中达到socket;期待零碎从磁盘上读取数据等)
  • 数据筹备好后,复制到内核缓冲区
  • 从内核空间复制到用户空间,用户程序拿到数据

接下来咱们看一下操作系统中有哪些I/O模型

几种I/O模型

阻塞式I/O


非阻塞式I/O


I/O多路复用(过程可同时监听多个I/O设施就绪)


信号驱动I/O


异步I/O


那么nodejs里到底应用了哪种I/O模型呢?是上图中的“非阻塞I/O”吗?别着急,先接着往下看,咱们来理解下nodejs的体系结构

nodejs体系结构,线程、I/O模型剖析

最下面一层是就是咱们编写nodejs利用代码时能够应用的API库,上面一层则是用来买通nodejs和它所依赖的底层库的一个中间层,比方实现让js代码能够调用底层的c代码库。来到最上面一层,能够看到前端同学相熟的V8,还有其余一些底层依赖。留神,这里有一个叫libuv的库,它是干什么的呢?从图中也能看出,libuv帮忙nodejs实现了底层的线程池、异步I/O等性能。libuv实际上是一个跨平台的c语言库,它在windows、linux等不同平台下会调用不同的实现。我这里次要剖析linux下libuv的实现,因为咱们的利用大部分时候还是运行在linux环境下的,且平台间的差异性并不会影响咱们对nodejs原理的剖析和了解。好了,对于nodejs在linux下的I/O模型来说,libuv实际上提供了两种不同场景下的不同实现,解决网络I/O次要由epoll函数实现(其实就是I/O多路复用,在后面的图中应用的是select函数来实现I/O多路复用,而epoll能够了解为select函数的升级版,这个临时不做具体分析),而解决文件I/O则由多线程(线程池) + 阻塞I/O模仿异步I/O实现


上面是一段我写的nodejs底层实现的伪代码帮忙大家了解

listenFd = new Socket();    // 创立监听socket
Bind(listenFd, 80); // 绑定端口
Listen(listenFd);   // 开始监听

for ( ; ; ) {
    // 阻塞在epoll函数上,期待网络数据筹备好
    // epoll可同时监听listenFd以及多个客户端连贯上是否有数据准备就绪
    // clients示意以后所有客户端连贯,curFd示意epoll函数最终拿到的一个就绪的连贯
    curFd = Epoll(listenFd, clients);

    if (curFd === listenFd) {
        // 监听套接字收到新的客户端连贯,创立套接字
        int connFd = Accept(listenFd);
        // 将新建的连贯增加到epoll监听的list
        clients.push(connFd);
    }

    else {
        // 某个客户端连贯数据就绪,读取申请数据
        request = curFd.read();
        // 这里拿到申请数据后能够收回data事件进入nodejs的事件循环
        ...
    }
}

// 读取本地文件时,libuv用多线程(线程池) + BIO模仿异步I/O
ThreadPool.run((callback) => {
    // 在线程里用BIO读取文件
    String content = Read('text.txt');  
    // 收回事件调用nodejs提供的callback
});

通过I/O多路复用 + 多线程模仿的异步I/O配合事件循环机制,nodejs就实现了单线程解决并发申请并且不会阻塞。所以回到之前所说的“非阻塞I/O”模型,实际上nodejs并没有间接应用通常定义上的非阻塞I/O模型,而是I/O多路复用模型 + 多线程BIO。我认为“非阻塞I/O”其实更多是对nodejs编程人员来说的一种形容,从编码方式和代码执行程序上来讲,nodejs的I/O调用确实是“非阻塞”的

总结

至此咱们应该能够理解到,nodejs的I/O模型其实次要是由I/O多路复用和多线程下的阻塞I/O两种形式一起组成的,而应答高并发申请时发挥作用的次要就是I/O多路复用。好了,那最初咱们来总结一下nodejs线程模型和I/O模型比照传统web利用多进(线)程 + 阻塞I/O模型的劣势和劣势

  • nodejs利用单线程模型省去了系统维护和切换多进(线)程的开销,同时多路复用的I/O模型能够让nodejs的单线程不会阻塞在某一个连贯上。在高并发场景下,nodejs利用只须要创立和治理多个客户端连贯对应的socket描述符而不须要创立对应的过程或线程,零碎开销上大大减少,所以能同时解决更多的客户端连贯
  • nodejs并不能晋升底层真正I/O操作的效率。如果底层I/O成为零碎的性能瓶颈,nodejs仍然无奈解决,即nodejs能够接管高并发申请,但如果须要解决大量慢I/O操作(比方读写磁盘),仍可能造成系统资源过载。所以高并发并不能简略的通过单线程 + 非阻塞I/O模型来解决
  • CPU密集型利用可能会让nodejs的单线程模型成为性能瓶颈
  • nodejs适宜高并发解决大量业务逻辑或快I/O(比方读写内存)

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理