本文首發於個人我的博客: kmknkk.xin
不足之處歡迎斧正!
在解釋node爲何可以作到高併發以前,不妨先了解一下node的其餘幾個特性:node
咱們先來明確一個概念,即:node是單線程
的,這一點與JavaScript在瀏覽器中的特性相同,而且在node中JavaScript主線程與其餘線程(例如I/O線程)是沒法共享狀態的。nginx
單線程的好處就是:redis
固然單線程也有許多壞處:sql
不過在今天看來,這些壞處都已經再也不是問題或者獲得了適當的解決:數據庫
(1) 建立進程 or 細分實例後端
關於第一個問題,最直白解決方案就是使用child_process核心模塊
或者cluster
:child_process 和 net 組合應用。咱們能夠經過在一臺多核服務器上建立多個進程(一般使用fork
操做)來充分利用每一個核心,不過要處理好進程間通訊問題。另外一個方案是,咱們能夠將物理機器劃分爲多臺單核的虛擬機,並經過pm2等工具,管理多臺虛擬機造成一個集羣架構,高效運行所需服務,至於每臺機器間的通訊(狀態同步)我這裏先按下不表,在下文的
Node分佈式架構
中再作詳細說明。瀏覽器
(2) 時間片輪轉安全
關於第二點,我跟小夥伴討論事後認爲能夠經過時間片輪轉方式,在單線程上模擬多線程,適當減小應用阻塞的感受(雖然這種方法不會真的像多線程那樣節約時間)
(3) 負載均衡、壞點監控/隔離服務器
至於第三點,我跟小夥伴們也討論過,認爲主要的痛點就在於node不一樣於JAVA,它所實現的邏輯是以異步爲主的。這就致使了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隊列不爲空:
(2)若是poll隊列爲空:
當進入到poll階段,而且調用了timers的話,會發生下面的狀況:
優先級:
根據上面的圖,咱們不可貴出:
Next Tick Queue > MicroTask Queue
那麼setTimeout、setInterval和setImmediate誰快呢?
答案是:不肯定
單單從執行圖上看,若是二者都是在mian module裏定義的,那麼:setTimeout、setInterval > setImmediate
可是有兩個條件制約了這一結論:
因此當 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。
簡單介紹完了大致的架構組成,接下來咱們來細看每一個部分的細節:
流量接入層所作的就是對全部接受的流量進行處理,提供瞭如下服務:
超時檢測
集羣健康檢查/隔離壞點服務器
失敗重試機制
鏈接池/會話保持機制
當轉發到各個產品線後就到了負載層工做的時候了:將請求根據狀況轉發到各地機房
固然,這個平臺並不止轉發這一個功能,你能夠把它理解爲一個大型的私有云系統,提供如下服務:
這一層主要的工做是:
(1)編寫可靠的 Node 代碼,爲需求提供後端服務
(2)編寫高性能查詢語句,與 Redis、Mysql 交互,提升查詢效率
(3)經過 Redis 同步集羣裏各個 Node 服務的狀態
(4)經過硬件管理平臺,管理/監控物理機器的狀態、管理IP地址等
(固然這部分我只是粗淺地列列條目,仍是須要時間來積累、深刻理解)
這一層主要的工做是:
(1)建立 Mysql 並設計相關頁、表;創建必要的索引、外鍵,提高查詢便利性
(2)部署 redis 並向 Node 層提供相應接口
雖然 Node 的單線程特性給其提供的服務帶來了許多問題,但只要咱們積極面對這些問題,用合理的方法(如使用 child_process 等模塊或構建分佈式集羣)去解決他們,發揮 Node 的各類優點,就能夠享受到它所帶來的好處!
待更新: