首頁  >  文章  >  web前端  >  淺析Node高併發的原理

淺析Node高併發的原理

青灯夜游
青灯夜游轉載
2022-10-18 20:53:171268瀏覽

淺析Node高併發的原理

我們先來看幾個常見的說法

  • nodejs是單執行緒非阻塞I/O模型
  • nodejs適合高併發
  • nodejs適合I/O密集型應用,不適合CPU密集型應用 【相關教學推薦:nodejs影片教學

在具體分析這幾個說法是不是、為什麼之前,我們先來做一些準備工作

從頭聊起

一個常見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

淺析Node高併發的原理


非阻塞式I/O

淺析Node高併發的原理


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

淺析Node高併發的原理


信号驱动I/O

淺析Node高併發的原理


异步I/O

淺析Node高併發的原理


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

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

淺析Node高併發的原理

最上面一层是就是我们编写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(例如讀寫記憶體)

更多node相關知識,請造訪:nodejs 教學

以上是淺析Node高併發的原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除