摘要:而在單線程環境下,繞不過錯誤就意味著導致應用退出,重啟恢復的間隙會導致服務中斷,這是我們不愿意看到的。這也是支持高并發的重要原因之一實際上不光是操作,的絕大多數操作都是以這種異步的方式進行的。
本文首發于我的個人博客: kmknkk.xinNode特性:高并發
不足之處歡迎斧正!
在解釋node為什么能夠做到高并發之前,不妨先了解一下node的其他幾個特性:
單線程我們先來明確一個概念,即:node是單線程的,這一點與JavaScript在瀏覽器中的特性相同,并且在node中JavaScript主線程與其他線程(例如I/O線程)是無法共享狀態的。
單線程的好處就是:
無需像多線程那樣去關注線程之間的狀態同步問題
沒有線程切換所帶來的開銷
沒有死鎖存在
當然單線程也有許多壞處:
無法充分利用多核CPU
大量計算占用CPU會導致應用阻塞(即不適用CPU密集型)
錯誤會引起整個應用的退出
不過在今天看來,這些壞處都已經不再是問題或者得到了適當的解決:
(1) 創建進程 or 細分實例
關于第一個問題,最直白解決方案就是使用child_process核心模塊或者cluster:child_process 和 net 組合應用。我們可以通過在一臺多核服務器上創建多個進程(通常使用fork操作)來充分利用每個核心,不過要處理好進程間通信問題。另一個方案是,我們可以將物理機器劃分為多臺單核的虛擬機,并通過pm2等工具,管理多臺虛擬機形成一個集群架構,高效運行所需服務,至于每臺機器間的通信(狀態同步)我這里先按下不表,在下文的Node分布式架構中再做詳細說明。
(2) 時間片輪轉
關于第二點,我跟小伙伴討論過后認為可以通過時間片輪轉方式,在單線程上模擬多線程,適當減少應用阻塞的感覺(雖然這種方法不會真的像多線程那樣節約時間)
(3) 負載均衡、壞點監控/隔離
至于第三點,我跟小伙伴們也討論過,認為主要的痛點就在于node不同于JAVA,它所實現的邏輯是以異步為主的。異步I/O這就導致了node無法像JAVA一樣方便地使用 try/catch 來來捕獲并繞過錯誤,因為無法確定異步任務會何時傳回異常。而在單線程環境下,繞不過錯誤就意味著導致應用退出,重啟恢復的間隙會導致服務中斷,這是我們不愿意看到的。
當然,在服務器資源豐富的當下,我們可以通過 pm2 或 nginx 這些工具,動態的判斷服務狀態。在服務出錯時隔離壞點服務器,將請求轉發到正常服務器上,并重啟壞點服務器以繼續提供服務。這也是Node分布式架構的一部分。
你可能會問,既然node是單線程的,事件全部在一個線程上處理,那不是應該效率很低、與高并發相悖嗎?
恰恰相反,node的性能很高。原因之一就是node具有異步I/O特性,每當有I/O請求發生時,node會提供給該請求一個I/O線程。然后node就不管這個I/O的操作過程了,而是繼續執行主線程上的事件,只需要在該請求返回回調時在處理即可。也就是node省去了許多等待請求的時間。
這也是node支持高并發的重要原因之一
實際上不光是I/O操作,node的絕大多數操作都是以這種異步的方式進行的。它就像是一個組織者,無需事必躬親,只需要告訴成員們如何正確的進行操作并接受反饋、處理關鍵步驟,就能使得整個團隊高效運行。
事務驅動你可能又要問了,node怎么知道請求返回了回調,又應該何時去處理這些回調呢?
答案就是node的另一特性:事務驅動,即主線程通過event loop事件循環觸發的方式來運行程序
這是node支持高并發的另一重要原因
圖解node環境下的Event loop:
┌───────────────────────┐ ┌─>│ timers │<————— 執行 setTimeout()、setInterval() 的回調 │ └──────────┬────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回調 │ ┌──────────┴────────────┐ │ │ I/O callbacks │<————— 執行幾乎所有的回調,除了 close callbacks 以及 timers 調度的回調和 setImmediate() 調度的回調 │ └──────────┬────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回調 │ ┌──────────┴────────────┐ │ │ idle, prepare │<————— 內部調用,可忽略 │ └──────────┬────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回調 | | ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ - (retrieve new I/O events; node will block here when appropriate) │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ | | | | | └───────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回調 | ┌──────────┴────────────┐ │ │ check │<————— setImmediate() 的回調將會在這個階段執行 │ └──────────┬────────────┘ | |<-- 執行所有 Next Tick Queue 以及 MicroTask Queue 的回調 │ ┌──────────┴────────────┐ └──┤ close callbacks │<————— socket.on("close", ...) └───────────────────────┘
poll階段:
當進入到poll階段,并且沒有timers被調用的時候,會發生下面的情況:
(1)如果poll隊列不為空:
Event Loop 將同步的執行poll queue里的callback(新的I/O事件),直到queue為空或者執行的callback到達上線。
(2)如果poll隊列為空:
如果腳本調用了setImmediate(), Event Loop將會結束poll階段并且進入到check階段執行setImmediate()的回調。
如果腳本沒有setImmediate()調用,Event Loop將會等待回調(新的I/O事件)被添加到隊列中,然后立即執行它們。
當進入到poll階段,并且調用了timers的話,會發生下面的情況:
一旦poll queue是空的話,Event Loop會檢查是否timers, 如果有1個或多個timers時間已經到達,Event Loop將會回到timer階段并執行那些timer的callback(即進入到下一次tick)。
優先級:
根據上面的圖,我們不難得出:
Next Tick Queue > MicroTask Queue
那么setTimeout、setInterval和setImmediate誰快呢?
答案是:不確定
單單從執行圖上看,如果兩者都是在mian module里定義的,那么:setTimeout、setInterval > setImmediate
但是有兩個條件制約了這一結論:
event loop初始化需要一定時間
setTimeout有最小毫秒數(一般認為最少1ms)
所以當 event loop準備時間 > setTimeout毫秒數時,進入timers檢查時已有setTimeout的任務,故timeout先輸出。反之則immediate先輸出。
如果是在poll階段定義的setTimeout和setImmediate,那么immediate先于timeout輸出。原因是在poll階段,會先進入check階段再進入timers階段。例如:
const fs = require("fs"); fs.readFile("./test.txt", "utf8", (err, data) => { setTimeout( () => { console.log("setTimeout"); }, 0); setImmediate( () => { console.log("setImmediate"); }) }) /** * * console: * > setImmediate * > setTimeout * **/
多說一句:
由于timer需要從紅黑樹中取出定時器來判斷時間是否到了,時間復雜度為O(lg(n)),故如果想立即異步執行一個事件,最好不要用 setTimeout(func, 0)。而是使用 process.nextTick() 來完成。
我了解到的Node集群架構主要分為以下幾個模塊:
Nginx(負載均衡、調度) -> Node集群 -> Redis(同步狀態)
按我的理解整理了一副圖:
當然,這應該是比較理想狀態下的架構方式。因為雖然 Redis 的讀/寫相當快,但這是因為其將數據存儲在內存池里,在內存上進行相關操作。
這對于服務器的內存負荷是相當高的,所以通常我們還是會在架構中加入 Mysql,如下圖:
先解釋一下這幅圖:
當用戶數據到來時,將數據先寫入 Mysql,Node 需要數據時再去 Redis 讀取,若沒有找到再去 Mysql 里面查詢想要的數據,并寫入 Redis,下次使用時就可以直接去 Redis 里面查詢了。
加入 Mysql 相較于只在 Redis 里讀/寫的好處有:
(1)避免了短期內無用的數據寫入 Redis,占用內存,減輕 Redis 負擔
(2)在后期需要對數據進行特定查詢、分析的時候(比如分析運營活動用戶漲幅),SQL關系查詢能提供很大的幫助
當然在應對短時間大流量寫入的時候,我們也可以直接將數據寫入 Redis,以達到快速存儲數據、增加服務器應對流量能力的目的,等流量下去了再多帶帶將數據寫入 Mysql。
簡單介紹完了大體的架構組成,接下來我們來細看每個部分的細節:
流量接入層流量接入層所做的就是對所有接受的流量進行處理,提供了以下服務:
流量緩沖
分流和轉發
超時檢測
與用戶建立連接超時
讀取用戶body超時
連接后端超時
讀后端響應頭超時
寫響應超時
與用戶長連接超時
集群健康檢查/隔離壞點服務器
隔離壞點服務器并嘗試修復/重啟,直到該服務器恢復正常
失敗重試機制
在請求轉發到某集群某機器上,返回失敗后,將該請求轉發到該集群的別的機器,或者跨集群的機器上進行重試
連接池/會話保持機制
對于延遲敏感用戶使用連接池機制,減少建立連接的時間
安全防護
數據分析
當轉發到各個產品線后就到了負載層工作的時候了:將請求根據情況轉發到各地機房
當然,這個平臺并不止轉發這一個功能,你可以把它理解為一個大型的私有云系統,提供以下服務:
文件上傳/服務線上部署
線上配置修改
設置定時任務
線上系統監控/日志打印服務
線上實例管理
鏡像中心
等等...
Node集群層這一層主要的工作是:
(1)編寫可靠的 Node 代碼,為需求提供后端服務
(2)編寫高性能查詢語句,與 Redis、Mysql 交互,提高查詢效率
(3)通過 Redis 同步集群里各個 Node 服務的狀態
(4)通過硬件管理平臺,管理/監控物理機器的狀態、管理IP地址等
(當然這部分我只是粗淺地列列條目,還是需要時間來積累、深入理解)
數據庫層這一層主要的工作是:
(1)創建 Mysql 并設計相關頁、表;建立必要的索引、外鍵,提升查詢便利性
(2)部署 redis 并向 Node 層提供相應接口
總結雖然 Node 的單線程特性給其提供的服務帶來了許多問題,但只要我們積極面對這些問題,用合理的方法(如使用 child_process 等模塊或構建分布式集群)去解決他們,發揮 Node 的各種優勢,就可以享受到它所帶來的好處!
待更新:
Redis相關特性
sql查詢性能指標 & 優化策略
Node內存監控 & 內存泄露排查/處理
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96491.html
摘要:持久化到中反向代理的負載均衡基于的集群搭建如何實現從中訂閱消息轉發到客戶端的擴展是阻塞式,使用訂閱發布模式時,會導致整個進程進入阻塞。緩存是用于解決高并發場景下系統的性能及穩定性問題的銀彈。 showImg(https://segmentfault.com/img/bVYE6k?w=900&h=385); Redis 是由意大利程序員 Salvatore Sanfilippo(昵稱:a...
摘要:以上是對云計算數據中心架構的一些剖析。因此,云計算數據中心的架構也會隨著社會的進步不斷調整和優化。 云計算,應當高度貼合網絡未來更高層次的發展趨勢,著力于提高網絡數據處理和存儲能力,致力于低碳高效的利用基礎資源。具體而言,應著重從高端服務器、高密度低成本服務器、海量存儲設備和高性能計算設備等基礎設施領域提高云計算數據中心的數據處理能力。云計算要求基礎設施具有良好的彈性、擴展性、自動化、數據...
閱讀 1381·2023-04-25 16:45
閱讀 1923·2021-11-17 09:33
閱讀 2312·2021-09-27 14:04
閱讀 918·2019-08-30 15:44
閱讀 2636·2019-08-30 14:24
閱讀 3417·2019-08-30 13:59
閱讀 1695·2019-08-29 17:00
閱讀 894·2019-08-29 15:33