重慶做網(wǎng)站的網(wǎng)絡(luò)公司域名注冊好了怎么弄網(wǎng)站
I/O密集型進程和CPU密集型進程
聊天應(yīng)用程序、MMO(大型多人在線)游戲、金融交易系統(tǒng)、等實時服務(wù)需要處理大量并發(fā)流量和實時數(shù)據(jù)。
這些服務(wù)是I/O密集型的,因為它們花費大量資源處理輸入輸出操作,例如高吞吐量、低延遲網(wǎng)絡(luò)通信(客戶端與服務(wù)器以及其他應(yīng)用程序組件之間)、實時數(shù)據(jù)庫寫入、文件 I/O、與第三方 API 的通信、流式傳輸實時數(shù)據(jù)等等。
通常,IO 密集型進程的性能取決于服務(wù)器的I/O系統(tǒng),I/O中(如寫數(shù)據(jù)到磁盤)的任何延遲都可能導(dǎo)致系統(tǒng)瓶頸。在I/O密集型進程中,CPU使用率相對較少,它需要等待I/O過程完成才能執(zhí)行某個進程。
而在CPU密集型進程中,性能主要取決于CPU的速度。系統(tǒng)大部分時間都花在執(zhí)行 CPU 中的進程上,而不是與外部組件通信。CPU性能越好,系統(tǒng)性能就越好。
如上所述,實時并發(fā)應(yīng)用程序中的關(guān)鍵進程(例如高吞吐量網(wǎng)絡(luò)操作、數(shù)據(jù)庫寫入、組件間通信等)會由于 IO 操作而引入系統(tǒng)延遲。
為了保證低延遲,不同 的Web 框架利用不同的策略(例如非阻塞 IO、單線程架構(gòu)的異步事件處理、參與者模型、反應(yīng)式編程等)來實現(xiàn)可擴展的實時服務(wù)。
在本文中,將討論 NodeJS的單線程事件循環(huán)模型架構(gòu)來處理大量 IO 密集型進程。
讓我們開始吧。
在深入研究單線程架構(gòu)之前,讓我們先了解一下傳統(tǒng)的基于線程請求的模型存在的問題。
基于線程的同步模型的 IO 瓶頸
在傳統(tǒng)的基于線程的同步模型中,應(yīng)用服務(wù)器利用該模型來處理客戶端請求時,對于 I/O 密集型應(yīng)用服務(wù),會面臨請求吞吐量瓶頸。
以Apache Tomcat服務(wù)器為例,它會維護一個線程池,當它接收到客戶端請求時,該請求會被分配給線程池中的一個工作線程來處理,詳細流程如下:
- 客戶端發(fā)送一個請求到Web服務(wù)器;
- Web服務(wù)器收到請求后,從線程池中選擇一個空閑可用的線程用于處理該請求;
- 此線程讀取客戶端請求,處理客戶端請求,執(zhí)行阻塞的IO操作(如果需要)和準備響應(yīng);
- 此線程將準備好的請求發(fā)送回Web服務(wù)器;
- Web服務(wù)器又將此響應(yīng)發(fā)送到相應(yīng)的服務(wù)器。
服務(wù)器為所有客戶端執(zhí)行以上步驟,為每一個客戶端請求盡量分配一個線程,如果線程池可用線程數(shù)少于并發(fā)請求數(shù)時,則在使用完所有線程之后,剩余的客戶端請求會在隊列中等待。
而在I/O密集型應(yīng)用中,大多數(shù)請求都會執(zhí)行 IO 操作,例如,向數(shù)據(jù)庫發(fā)出查詢。在這種情況下,只要來自服務(wù)器的請求正在等待來自數(shù)據(jù)庫的響應(yīng),該工作線程就會暫時被阻塞。它無法處理對服務(wù)器的其他請求。
因此如果這些線程中有大量的阻塞IO操作(例如:和數(shù)據(jù)庫、文件系統(tǒng)、外部服務(wù)等交互),那么剩余的客戶端將會等待很長的時間。
可用看出在高并發(fā)流量的 I/O 密集型應(yīng)用中,這種線程阻塞行為會導(dǎo)致資源爭用、并發(fā)性降低和性能瓶頸。
解決 IO 瓶頸問題
不同的編程語言和各自生態(tài)系統(tǒng)會采用一些異步方法(單線程事件循環(huán)模型、Actor模型、響應(yīng)式)來解決同步請求阻塞問題。本文主要介紹NodeJS的架構(gòu)和單線程事件循環(huán)模型。
NodeJS 從最基本的設(shè)計出發(fā),目的就在于通過其單線程事件循環(huán)架構(gòu),以最小的開銷高效處理大量并發(fā)請求和異步 IO 操作。作為主線程處理所有客戶端請求,并將所有 IO 操作委托給其它線程,詳細流程如下。
- 客戶端發(fā)送請求到Web服務(wù)器;
- NodeJS的Web服務(wù)器在內(nèi)部維護一個有限的線程池,以便為客戶端請求提供服務(wù);
- NodeJS的Web服務(wù)器接收這些請求并將它們放入隊列(Event Queue)中。 它被稱為“事件隊列”
- NodeJS的Web服務(wù)器內(nèi)部有一個組件,稱為“事件循環(huán)(Event Loop Single Thread)”,從英文名可以看出,事件循環(huán)只使用到了一個線程,使用無限循環(huán)來接收請求并處理它們。它是NodeJS的處理模型的核心
- 事件循環(huán)回去檢查是否有客戶端的請求被放置在事件隊列中。如果沒有,會一直等待事件隊列中存在請求。
- 如果事件隊列中有需要處理的客戶端請求,則會從事件隊列中選擇一個請求。
在事件循環(huán)線程處理客戶端請求時,根據(jù)請求的類型,有不同的處理方式:
- 如果該客戶端請求不需要任何阻塞IO操作,則處理所有內(nèi)容,準備響應(yīng)并將其發(fā)送回客戶端
- 如果該客戶端請求需要一些阻塞IO操作,例如與數(shù)據(jù)庫,文件系統(tǒng),外部服務(wù)交互,就會從從內(nèi)部線程池獲取一個可用的線程并將此客戶端請求分配給該線程,這個內(nèi)部線程池的線程負責接收該請求,處理該請求,執(zhí)行阻塞IO操作,準備響應(yīng)并將其發(fā)送回事件循環(huán),事件循環(huán)依次將響應(yīng)發(fā)送到相應(yīng)的客戶端
以上圖為例,Web服務(wù)器內(nèi)部維護著一個有限的線程池,線程池中線程數(shù)量為m個,NodeJS的Web服務(wù)器接收到Client-1, Client-2, …, Client-n的請求后,將請求放入到事件隊列中NodeJS的事件循環(huán)從隊列中開始拾取這些請求,以Client-1的請求和Client-3為例。
對于Client-1的請求:
- 事件循環(huán)檢查Client-1 Request-1是否確實需要任何阻塞IO操作,或者需要更多時間來執(zhí)行復(fù)雜的計算任務(wù)
- 由于此請求是簡單計算和非阻塞IO任務(wù),因此不需要單獨的線程來處理它
- 事件循環(huán)處理該請求所需要的操作,準備其響應(yīng)Response-1
- 事件循環(huán)發(fā)送Response-1到Client-1
對于Client-3的請求:
- 檢查Client-n Request-n是否需要任何阻塞IO操作或花費更多時間來執(zhí)行復(fù)雜的計算任務(wù)
- 由于此請求有非常復(fù)雜的計算或阻塞IO任務(wù),因此事件循環(huán)不會處理此請求
- 事件循環(huán)從內(nèi)部線程池中獲取線程T-1,并將此Client-n Request-n分配給線程T-1
- 線程T-1讀取并處理Request-n,執(zhí)行必要的阻塞IO或計算任務(wù),最后準備響應(yīng)Response-n
- 線程T-1將此Response-n發(fā)送到事件循環(huán),事件循環(huán)依次將此Response-n發(fā)送到Client-n
舉個生活中的案例,以大排檔點餐為例,大排檔有已經(jīng)做好的熟食,此外還可以根據(jù)顧客的要求現(xiàn)做,服務(wù)員(事件循環(huán))在一個時間段內(nèi)只能處理一個顧客的點餐請求(事件隊列中的任務(wù))。每當一個顧客點完餐,服務(wù)員就會檢查下菜單,如果顧客點的是熟食(沒有阻塞的I/O任務(wù)),服務(wù)員直接把菜端給客戶就行了(直接處理);但是如果顧客點的是現(xiàn)做的食物,那么服務(wù)員就選一個空閑的廚師(內(nèi)部線程池中可用的線程)將顧客的需求轉(zhuǎn)給他,等廚師做好后遞給服務(wù)員,服務(wù)員再端給客戶。
為了加深理解,這里有個簡單的代碼示例:
public class ThreadPoolEventLoop {private final Queue<Runnable> eventQueue = new LinkedBlockingQueue<>(); 創(chuàng)建一個固定大小為3的線程池private final ExecutorService threadPool = Executors.newFixedThreadPool(3); public void startEventLoop() {while (true) {Runnable event;synchronized (eventQueue) {event = eventQueue.poll(); // 從隊列中取出事件}if (event != null) {// 判斷事件是否是 I/O 阻塞任務(wù)if (isIOBound(event)) {threadPool.submit(event); // 使用線程池提交 I/O 阻塞任務(wù)} else {event.run(); // 處理非阻塞任務(wù)}} else {try {Thread.sleep(100); // 如果沒有事件,等待} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}}// 模擬判斷事件是否是 I/O 阻塞任務(wù)的方法private boolean isIOBound(Runnable event) {// 這里可以根據(jù)具體的業(yè)務(wù)邏輯判斷事件是否是 I/O 阻塞任務(wù)// 此處簡單地假設(shè)所有事件都是非阻塞的return false;}public void registerEvent(Runnable event) {synchronized (eventQueue) {eventQueue.offer(event); // 將事件添加到隊列尾部}}public static void main(String[] args) {ThreadPoolEventLoop eventLoop = new ThreadPoolEventLoop();// 注冊幾個簡單的事件,其中一個模擬 I/O 阻塞任務(wù)eventLoop.registerEvent(() -> System.out.println("Event 1 executed"));eventLoop.registerEvent(() -> System.out.println("Event 2 executed"));eventLoop.registerEvent(() -> System.out.println("Event 3 executed"));// 啟動事件循環(huán)eventLoop.startEventLoop();}
}//輸出
Event 1 executed
Event 2 executed
Event 3 executed