你的Node.js服務(wù)又崩了,日志里堆滿"JavaScript heap out of memory"。你加了--max-old-space-size,重啟,祈禱。三天后,同樣位置,同樣報錯。
這不是運氣問題。是你從未被教過內(nèi)存到底怎么工作。
物理內(nèi)存到V8堆:一段被跳過的旅程
內(nèi)存條上的DRAM芯片是起點。操作系統(tǒng)把物理地址翻譯成虛擬地址,每個進程以為自己獨占整片空間。Node.js進程啟動時,向操作系統(tǒng)申請一塊地址池,這叫虛擬內(nèi)存(Virtual Memory)。
虛擬內(nèi)存分三塊:代碼段(你的JS和依賴)、棧(函數(shù)調(diào)用的臨時數(shù)據(jù))、堆(動態(tài)分配的對象)。堆是V8的地盤,但V8的堆只是進程堆的一部分。你的Buffer、TypedArray、甚至fs.readFile的緩存,都可能落在堆外。
V8堆內(nèi)部更細:新生代(New Space)用Scavenge算法快速清理短命對象,老生代(Old Space)用Mark-Sweep-Compact對付長壽對象。Orinoco垃圾回收器(2018年后默認)允許并發(fā)標(biāo)記,但壓縮階段仍需暫停——這就是你看到的卡頓。
新生代默認32MB(64位系統(tǒng)),老生代從0開始增長,直到觸及--max-old-space-size或系統(tǒng)極限。很多人以為設(shè)成4096就安全了,但物理內(nèi)存只有8GB的機器上,Node進程可能被OOM Killer直接干掉,連遺書都不留。
libuv與線程池:單線程的"外包團隊"
Node.js確實只有一個執(zhí)行JS的線程,叫主線程(Main Thread)。但libuv——那個綁定V8和操作系統(tǒng)的中介——偷偷養(yǎng)了線程池。
默認4個線程,藏在你看不見的地方。fs.readFile、dns.lookup、crypto.pbkdf2,這些"異步"操作其實是把臟活丟給線程池,主線程繼續(xù)跑事件循環(huán)(Event Loop)。線程干完活,把結(jié)果塞回任務(wù)隊列,等主線程輪詢到再執(zhí)行回調(diào)。
線程池大小可調(diào):UV_THREADPOOL_SIZE=128 node app.js。但別急著改。線程切換有成本——上下文切換(Context Switch)要保存寄存器、刷新緩存、讓出CPU。線程太多,OS調(diào)度器疲于奔命,你的"優(yōu)化"變成負優(yōu)化。
CPU調(diào)度器用CFS(完全公平調(diào)度器)決定誰跑。每個線程有vruntime,跑越久優(yōu)先級越低。Node主線程如果一直忙,vruntime暴漲,可能被搶占了——你的回調(diào)延遲就是這么來的。
事件循環(huán)的六個階段:回調(diào)到底何時執(zhí)行
timers階段檢查setTimeout/setInterval,I/O callbacks處理系統(tǒng)錯誤,idle/prepare內(nèi)部用,poll階段等I/O事件,check階段跑setImmediate,close callbacks收尾。一輪結(jié)束,下一輪開始。
poll階段最微妙。如果隊列空了,有setImmediate就跳到check,沒有就等I/O。你的數(shù)據(jù)庫查詢回調(diào)卡在這,因為前一個同步計算占滿了主線程。事件循環(huán)不是魔法,是排隊系統(tǒng)——隊首的任務(wù)堵死后面所有人。
process.nextTick在階段間隙插隊,Promise微任務(wù)(Microtask)在宏任務(wù)后清空。濫用nextTick會餓死I/O,微任務(wù)遞歸會棧溢出。這些不是"最佳實踐",是機制決定的必然。
Cluster與Worker Threads:兩種并行路線
Cluster模塊 fork 多個進程,各帶獨立V8實例。端口共享靠操作系統(tǒng),內(nèi)存不共享,崩潰隔離。但進程間通信(IPC)走序列化/反序列化,大對象傳遞是災(zāi)難。
Worker Threads(Node 10.5+)共享ArrayBuffer,不共享JS對象。每個Worker有獨立事件循環(huán)和V8隔離堆,但能在同一個進程地址空間操作內(nèi)存。適合CPU密集型:圖像處理、復(fù)雜計算。IO密集型別用,libuv線程池已經(jīng)夠用了。
SharedArrayBuffer + Atomics 能實現(xiàn)真正的鎖機制,但內(nèi)存模型復(fù)雜到讓人頭禿。99%的場景,隊列+Worker池更簡單。
診斷工具:從猜想到證據(jù)
--prof 生成V8日志,--prof-process 解析火焰圖。但采樣開銷不小,生產(chǎn)環(huán)境慎用。
clinic.js 封裝了doctor(事件循環(huán)診斷)、bubbleprof(異步流可視化)、heap-profiler(內(nèi)存快照)。heap-profiler抓到的快照用Chrome DevTools打開,能看到對象引用鏈——你的內(nèi)存泄漏通常藏在閉包或事件監(jiān)聽器里。
Linux上perf + 0x 能看內(nèi)核態(tài)時間。如果sys時間高,可能是線程競爭或系統(tǒng)調(diào)用太頻繁。strace -c 統(tǒng)計syscall次數(shù),有時候瓶頸在 unexpected 的地方。
你的服務(wù)現(xiàn)在跑在什么階段?poll空轉(zhuǎn)還是mark-sweep卡頓?下次崩潰前,先確認是堆內(nèi)存、外部內(nèi)存,還是RSS被系統(tǒng)限制。數(shù)據(jù)不會撒謊,但你的假設(shè)會。
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.