存溢出排查與調(diào)優(yōu):從V8堆內(nèi)存到--max-old-space-size配置)
做Node.js開發(fā)久了幾乎每個(gè)人都會(huì)撞上這個(gè)報(bào)錯(cuò)——FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory。我第一次看到這個(gè)紅字是在一個(gè)跑了十幾小時(shí)的數(shù)據(jù)處理腳本上當(dāng)時(shí)第一反應(yīng)是服務(wù)器內(nèi)存不夠第二反應(yīng)是代碼寫崩了結(jié)果查了一圈發(fā)現(xiàn)問(wèn)題根本不是這兩件事。這個(gè)錯(cuò)誤的本質(zhì)是Node.js默認(rèn)的舊空間堆內(nèi)存上限太低而官方給出的--max-old-space-size參數(shù)如果你只是臨時(shí)在命令行里加一次那等于沒(méi)配。這篇就結(jié)合我自己的踩坑過(guò)程把這個(gè)錯(cuò)誤的成因、排查鏈路和真正能一勞永逸的配置方案從頭到尾捋一遍。1. 先搞明白“JavaScript heap out of memory”到底在說(shuō)什么1.1 V8引擎的內(nèi)存模型不是“內(nèi)存不夠”而是“額度不夠”很多人看到out of memory就跑去加服務(wù)器內(nèi)存這其實(shí)是在錯(cuò)誤的方向上使勁。Node.js運(yùn)行JavaScript代碼靠的是V8引擎而V8管理內(nèi)存的方式不是“用多少取多少”而是預(yù)先劃分出幾塊區(qū)域其中**老生代old space**負(fù)責(zé)存放存活時(shí)間較長(zhǎng)的對(duì)象**新生代new space**負(fù)責(zé)存放短命對(duì)象。我們平時(shí)說(shuō)的“堆內(nèi)存不足”絕大多數(shù)情況都指向老生代。V8給老生代設(shè)置了一個(gè)默認(rèn)上限64位系統(tǒng)下大約是1.4GB到2GB之間具體數(shù)值跟Node版本有關(guān)。如果你用node直接跑一個(gè)腳本這個(gè)腳本需要的內(nèi)存超過(guò)這個(gè)閾值V8就會(huì)反復(fù)嘗試?yán)厥栈厥胀炅诉€是不夠最終拋出一個(gè)致命錯(cuò)誤然后直接崩潰。這里有個(gè)關(guān)鍵點(diǎn)這個(gè)上限是V8內(nèi)部的虛擬內(nèi)存額度不是操作系統(tǒng)的物理內(nèi)存上限。也就是說(shuō)哪怕你的服務(wù)器有64GB內(nèi)存Node默認(rèn)也只肯用那1.4GB左右。這解釋了為什么很多人在自己的開發(fā)機(jī)上沒(méi)出過(guò)問(wèn)題一部署到高配服務(wù)器上反而崩了——因?yàn)殚_發(fā)機(jī)的數(shù)據(jù)集小閾值沒(méi)被觸碰服務(wù)器的數(shù)據(jù)集大內(nèi)存額度一下子就撞頂了。1.2 同樣的報(bào)錯(cuò)兩種完全不同的觸發(fā)場(chǎng)景我在排查過(guò)程中發(fā)現(xiàn)遇到這個(gè)錯(cuò)誤的人大致分成兩類處理方式截然不同。第一類是“單次大任務(wù)”型。比如用Node做PDF合并、圖片批量處理、大型JSON解析、Webpack構(gòu)建前端項(xiàng)目。這類場(chǎng)景的特點(diǎn)是某一個(gè)時(shí)刻需要一次性分配大量?jī)?nèi)存內(nèi)存峰值極高但代碼本身沒(méi)有內(nèi)存泄漏。Webpack打包大型項(xiàng)目報(bào)這個(gè)錯(cuò)99%屬于這種。這種場(chǎng)景的解法很直接把堆內(nèi)存上限調(diào)大就行。第二類是“緩慢增長(zhǎng)”型。腳本運(yùn)行初期一切正常跑幾小時(shí)后內(nèi)存占用緩慢上升最終在某次內(nèi)存分配時(shí)崩潰。這種場(chǎng)景通常是代碼里存在內(nèi)存泄漏——全局變量不斷堆積、閉包意外持有大對(duì)象、事件監(jiān)聽器持續(xù)綁定但沒(méi)有移除等等。只調(diào)大內(nèi)存上限是治標(biāo)不治本相當(dāng)于給漏水的水桶加了一個(gè)更大的水槽漏水速度沒(méi)變只是延遲了崩潰時(shí)間。判斷自己屬于哪一類有個(gè)很簡(jiǎn)單的辦法看崩潰前內(nèi)存漲得多快。如果是幾秒內(nèi)瞬間沖頂多半是第一類如果是勻速爬升跑幾小時(shí)才爆那基本是第二類。后面我會(huì)展開講兩類場(chǎng)景的具體處理思路。2. 最直接的臨時(shí)方案--max-old-space-size參數(shù)怎么用才不出錯(cuò)2.1 命令行的正確姿勢(shì)先給新手朋友演示最基礎(chǔ)的臨時(shí)方案。假設(shè)你的程序入口文件是app.js需要把堆內(nèi)存上限調(diào)到4GB命令應(yīng)該這樣寫node --max-old-space-size4096 app.js注意幾個(gè)容易踩的細(xì)節(jié)參數(shù)是在node和腳本文件名之間不是放在文件名后面。寫成node app.js --max-old-space-size4096是不生效的因?yàn)槟惆褏?shù)傳給了程序而不是Node運(yùn)行時(shí)。單位是MB不是GB。4096代表4GB8192代表8GB很多人一上來(lái)就寫--max-old-space-size4結(jié)果堆上限變成了4MB啟動(dòng)即崩潰。64位系統(tǒng)和32位系統(tǒng)的可用上限不同32位老生代上限基本就在1GB上下硬調(diào)大也沒(méi)有意義?,F(xiàn)在開發(fā)環(huán)境基本全是64位但如果你在老的CI鏡像里跑需要注意這個(gè)問(wèn)題。如果是通過(guò)npm腳本運(yùn)行比如npm run dev那就在package.json的scripts里寫好{ scripts: { start: node --max-old-space-size4096 app.js, build: node --max-old-space-size8192 build.js } }這種改法生效最快垮一秒鐘搞定而且只影響這一個(gè)腳本不影響系統(tǒng)上其他的Node進(jìn)程適合拿來(lái)應(yīng)急驗(yàn)證“調(diào)大之后到底能不能跑通”。2.2 系統(tǒng)環(huán)境變量的全局方案如果你跑的Node程序不是你直接啟動(dòng)的而是被某個(gè)進(jìn)程管理器比如PM2拉起來(lái)的或者你希望整個(gè)系統(tǒng)上所有Node進(jìn)程都默認(rèn)擁有更大的堆內(nèi)存那就需要通過(guò)環(huán)境變量NODE_OPTIONS來(lái)全局注入。Linux和macOS在~/.bashrc或~/.zshrc里加上export NODE_OPTIONS--max-old-space-size4096Windows系統(tǒng)在“系統(tǒng)屬性 - 環(huán)境變量 - 新建”里添加用戶變量或系統(tǒng)變量變量名NODE_OPTIONS變量值--max-old-space-size4096。配置完后記得重開終端或者執(zhí)行source ~/.bashrc否則當(dāng)前會(huì)話不會(huì)加載新配置。注意NODE_OPTIONS不僅會(huì)影響老生代大小它可以承載很多V8選項(xiàng)但注意它不支持一些不安全或有副作用的標(biāo)志比如--inspect相關(guān)。另外它會(huì)影響這臺(tái)機(jī)器上所有Node進(jìn)程如果同一臺(tái)機(jī)器上跑著多個(gè)服務(wù)每個(gè)都默認(rèn)4GB內(nèi)存壓力會(huì)明顯增大需要掂量一下再設(shè)。這個(gè)方案雖然能全局生效但嚴(yán)格來(lái)說(shuō)還只是“環(huán)境級(jí)”的換一臺(tái)機(jī)器、換一個(gè)部署環(huán)境配置就沒(méi)了。真正能跟著項(xiàng)目走的、在不同環(huán)境里自動(dòng)生效的是下面要講的永久配置方案。3. 永久配置方案讓配置跟著項(xiàng)目走而不是跟著機(jī)器走3.1 方案一在package.json里固化scripts適合大多數(shù)項(xiàng)目這是我最推薦的做法因?yàn)橹灰?xiàng)目代碼在配置就在。把可能觸發(fā)內(nèi)存峰值的操作全部顯式加參數(shù)。假設(shè)項(xiàng)目里有這樣幾個(gè)常用操作對(duì)應(yīng)的寫法如下{ scripts: { dev: node --max-old-space-size4096 server.js, build: node --max-old-space-size8192 build.js, test: node --max-old-space-size2048 node_modules/.bin/jest } }這里有一個(gè)項(xiàng)目實(shí)踐中容易忽略的坑如果你通過(guò)npx jest跑測(cè)試npx本身會(huì)創(chuàng)建一個(gè)新的Node進(jìn)程而npx jest的寫法不一定能帶上你配置的--max-old-space-size。穩(wěn)妥的做法是像上面這樣直接調(diào)用node_modules/.bin/jest的完整路徑讓參數(shù)明確落在最終執(zhí)行的那個(gè)Node進(jìn)程上。用Webpack打包的讀者可能還會(huì)遇到一個(gè)問(wèn)題Webpack 5有自己的構(gòu)建緩存和多進(jìn)程壓縮機(jī)制光調(diào)大Node堆內(nèi)存不一定能徹底解決OOM。這時(shí)候還需要在webpack.config.js里把performance.hints調(diào)低以及考慮用terser-webpack-plugin的并行壓縮配置來(lái)降低單進(jìn)程內(nèi)存峰值。這個(gè)是給“構(gòu)建類”項(xiàng)目額外提醒一句后面測(cè)試部分我不會(huì)再展開因?yàn)槟侨Q于具體的項(xiàng)目技術(shù)棧。3.2 方案二寫一個(gè).node-opts配置文件配合啟動(dòng)腳本適合部署環(huán)境如果你的項(xiàng)目不是靠package.json啟動(dòng)的或者你們的生產(chǎn)環(huán)境有一套自己的發(fā)布系統(tǒng)我建議把配置收斂到一個(gè)統(tǒng)一的啟動(dòng)腳本里。我自己的做法是在項(xiàng)目根目錄放一個(gè)start.sh#!/bin/bash export NODE_OPTIONS--max-old-space-size6144 exec node server.js $然后給腳本執(zhí)行權(quán)限chmod x start.sh部署系統(tǒng)直接執(zhí)行./start.sh就行。這樣做的好處是發(fā)布系統(tǒng)不用關(guān)心Node內(nèi)存參數(shù)所有運(yùn)行時(shí)調(diào)優(yōu)細(xì)節(jié)都打包在項(xiàng)目代碼里。每個(gè)團(tuán)隊(duì)成員或CI機(jī)器執(zhí)行同樣的命令得到同樣的行為不會(huì)出現(xiàn)“我本地沒(méi)問(wèn)題服務(wù)器上崩了”的情況。Windows環(huán)境沒(méi)有bash可以用start.cmdecho off set NODE_OPTIONS--max-old-space-size6144 node server.js %*兩種腳本一放跨平臺(tái)部署也統(tǒng)一了。3.3 方案三Docker容器里通過(guò)ENV注入適合容器化部署如果你們的服務(wù)是Docker化的那最干凈的方式是在Dockerfile里寫死FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY . . ENV NODE_OPTIONS--max-old-space-size6144 CMD [node, server.js]注意ENV放在CMD之前生效這樣容器啟動(dòng)時(shí)所有Node進(jìn)程都會(huì)帶上這個(gè)參數(shù)。如果你們用docker-compose也可以在compose文件里通過(guò)environment字段設(shè)置效果一樣。這里提醒一個(gè)容器環(huán)境的專項(xiàng)問(wèn)題容器里的Node進(jìn)程要注意--max-old-space-size的數(shù)值不能超過(guò)容器的內(nèi)存上限否則會(huì)被OOM Killer殺掉。比如容器只分配了2GB內(nèi)存但你給Node堆設(shè)了4GBV8還沒(méi)達(dá)到自己的上限容器就先被系統(tǒng)干掉了日志里看到的表現(xiàn)可能是直接Killed而不是“JavaScript heap out of memory”。3.4 方案四PM2進(jìn)程守護(hù)下的Node內(nèi)存配置如果項(xiàng)目是用PM2管理的那就需要在PM2的配置里一并處理。PM2有自己的內(nèi)存重啟機(jī)制跟Node的堆上限是兩回事但經(jīng)常被搞混。ecosystem.config.js里這樣寫module.exports { apps: [ { name: my-app, script: server.js, instances: 1, exec_mode: fork, max_memory_restart: 4G, node_args: --max-old-space-size3584, env: { NODE_OPTIONS: --max-old-space-size3584 } } ] };這個(gè)地方我要多說(shuō)一句初學(xué)者很容易混淆三個(gè)概念max_memory_restart是PM2層面監(jiān)控的進(jìn)程物理內(nèi)存上限超過(guò)了它PM2會(huì)強(qiáng)制重啟進(jìn)程這里的單位是G/M。--max-old-space-size是V8層面的堆內(nèi)存上限單位是MB。NODE_OPTIONS是環(huán)境變量層面的注入方式跟命令行加參效果等價(jià)。如果max_memory_restart: 4G但--max-old-space-size6000PM2會(huì)在V8崩潰之前先把進(jìn)程殺掉。實(shí)際配置時(shí)要讓PM2的監(jiān)控閾值略高于V8的堆上限留出余量給非堆內(nèi)存部分比如原生綁定、緩沖區(qū)分區(qū)等我通常建議堆上限設(shè)為物理內(nèi)存的70%到80%。4. 比改配置更重要的定位“為什么會(huì)內(nèi)存暴漲”4.1 用--trace-gc看垃圾回收日志僅僅調(diào)大堆上限解決了崩潰的“表象”但如果你不搞明白內(nèi)存為什么漲到那個(gè)水平遲早會(huì)在更大的數(shù)據(jù)量面前再崩一次。排查內(nèi)存問(wèn)題的第一個(gè)利器是V8自帶的GC跟蹤node --trace-gc app.js運(yùn)行后控制臺(tái)會(huì)不斷刷出類似這樣的日志[37512:0x559e2acb8000] 1024001 ms: Scavenge 1824.5 (1840.2) - 1200.3 (1840.2) MB, 15.2 / 0.0 ms (average mu 0.733, current mu 0.7) allocation failure [37512:0x559e2acb8000] 1024502 ms: Mark-sweep 1824.5 (1840.2) - 1600.3 (1840.2) MB, 200.2 / 0.0 ms (average mu 0.732, current mu 0.7) allocation failure注意日志里的allocation failure它表明GC是因?yàn)椤皟?nèi)存分配失敗”才被迫觸發(fā)的而不是周期性正?;厥?。如果你的日志里頻繁出現(xiàn)allocation failure說(shuō)明堆內(nèi)存已經(jīng)處在“瀕臨崩潰”的狀態(tài)即使這次沒(méi)崩也離崩潰不遠(yuǎn)了。另一個(gè)值得關(guān)注的指標(biāo)是Mark-sweep標(biāo)記-清除耗時(shí)。如果它從正常的幾十毫秒漲到幾百毫秒甚至幾秒說(shuō)明堆里長(zhǎng)期存活的對(duì)象已經(jīng)多到GC都要耗很久才能遍歷完成這往往意味著有對(duì)象“該回收的沒(méi)被回收”。4.2 用heapdump抓取堆快照定位泄漏點(diǎn)的經(jīng)典手段是生成堆快照然后用Chrome DevTools的Memory面板分析。分兩步。第一步在代碼里埋入快照觸發(fā)點(diǎn)或者用--heapsnapshot-near-heap-limit參數(shù)在堆內(nèi)存逼近上限時(shí)自動(dòng)生成快照node --heapsnapshot-near-heap-limit512 app.js這個(gè)參數(shù)的意思是在堆使用率接近上限512MB時(shí)嘗試生成一份.heapsnapshot文件。第二步把生成的快照文件拖進(jìn)Chrome DevTools的Memory面板用“Comparison”對(duì)照早期快照和崩潰前的快照看哪個(gè)構(gòu)造函數(shù)Constructor的實(shí)例數(shù)量或Retained Size增長(zhǎng)最離譜那基本就是泄漏點(diǎn)所在。我實(shí)際排查過(guò)一個(gè)典型案例一個(gè)定時(shí)抓取網(wǎng)頁(yè)數(shù)據(jù)后入庫(kù)的腳本每次抓取都會(huì)用全局?jǐn)?shù)組暫存結(jié)果但入庫(kù)成功后沒(méi)有array.length 0也不是用array []替換導(dǎo)致歷史數(shù)據(jù)全部殘留在內(nèi)存里。這種問(wèn)題在堆快照里一眼就能看出來(lái)——Array構(gòu)造函數(shù)的Retained Size隨快照增長(zhǎng)出現(xiàn)了大量增長(zhǎng)的“字符串”和“對(duì)象”元素。4.3 癥狀相同的兩類問(wèn)題處理手段完全不同我前文說(shuō)了這個(gè)錯(cuò)誤分兩類。如果你判斷自己屬于“緩慢增長(zhǎng)”型那么單純調(diào)大--max-old-space-size只會(huì)推遲崩潰時(shí)間不會(huì)消除崩潰。正確的做法是在堆快照里找到泄漏源頭把對(duì)應(yīng)的引用關(guān)系打斷。常見的泄漏模式我列在下面你可以對(duì)著排查全局緩存不設(shè)上限。比如用全局Map做緩存只往里寫從不清理最終內(nèi)存被吃光。解法是給緩存加上LRU淘汰機(jī)制。事件監(jiān)聽器不解除。process.on、第三方庫(kù)內(nèi)部的監(jiān)聽器、EventEmitter實(shí)例反復(fù)創(chuàng)建導(dǎo)致監(jiān)聽器數(shù)組無(wú)限膨脹。閉包意外持有大對(duì)象。內(nèi)部函數(shù)被拋出外部長(zhǎng)期引用外層作用域里的巨大數(shù)據(jù)就永遠(yuǎn)無(wú)法被回收。流式處理沒(méi)有監(jiān)聽data的合適消費(fèi)方式。比如把整個(gè)文件讀成字符串再做字符串拼接十幾GB的日志用fs.readFile一把梭內(nèi)存直接爆炸。應(yīng)該改用readline逐行處理或者stream管道。如果屬于“單次大任務(wù)”型比如Webpack構(gòu)建、一次性解析超大的JSON文件那調(diào)大堆內(nèi)存就是正確解法不必過(guò)于擔(dān)心泄漏問(wèn)題。5. 實(shí)測(cè)踩坑我調(diào)大堆內(nèi)存后遇到的三件意料之外的事5.1 調(diào)大堆內(nèi)存反而導(dǎo)致進(jìn)程卡死有一回我把一個(gè)數(shù)據(jù)處理腳本的堆上限從默認(rèn)的1.4GB調(diào)到8GB結(jié)果運(yùn)行到某個(gè)階段后進(jìn)程突然像死了一樣CPU占用掉到0但進(jìn)程不退出、不報(bào)錯(cuò)、什么都不做。一開始我以為是死鎖查了半天才發(fā)現(xiàn)是**GC停頓stop-the-world**時(shí)間過(guò)長(zhǎng)。V8在做全堆垃圾回收時(shí)會(huì)暫停JavaScript執(zhí)行。當(dāng)堆內(nèi)存從1.4GB擴(kuò)大到8GB后單次全量GC需要遍歷的對(duì)象數(shù)量也隨之增加GC停頓時(shí)間從幾十毫秒暴漲到十幾秒。對(duì)于頻繁觸發(fā)GC的場(chǎng)景這種長(zhǎng)停頓造成的“假死”比OOM更難以察覺(jué)因?yàn)槌绦虿皇钦f(shuō)崩就崩而是明明活著卻不干活。解決思路不是把堆調(diào)回小而是在代碼里主動(dòng)規(guī)避“大批量對(duì)象一次性堆積”的模式。比如我那個(gè)腳本原本是循環(huán)里不斷拼接一個(gè)大數(shù)組最后一次性寫庫(kù)改成每處理1000條就寫一次并釋放引用后堆內(nèi)存峰值顯著下降GC頻率和停頓時(shí)間都恢復(fù)正常。5.2NODE_OPTIONS里的參數(shù)居然被“忽略”了有次我在一個(gè)老項(xiàng)目中配置NODE_OPTIONS--max-old-space-size8192結(jié)果啟動(dòng)后通過(guò)process.memoryUsage()看heapTotal峰值還是只有兩三百M(fèi)B跟沒(méi)配一樣。排查半天發(fā)現(xiàn)原來(lái)項(xiàng)目啟動(dòng)時(shí)是通過(guò)node -r ts-node/register app.ts跑的入口是app.ts但啟動(dòng)命令里-r的存在并沒(méi)有問(wèn)題。真正的問(wèn)題出在啟動(dòng)腳本里有一行process.env.NODE_OPTIONS ;是在第三方庫(kù)初始化時(shí)被清掉的。這種事不常見但很坑。如果你遇到配了NODE_OPTIONS卻不生效的情況先在代碼里搜索一下有沒(méi)有對(duì)process.env.NODE_OPTIONS的讀寫操作再確認(rèn)你的啟動(dòng)鏈路里有沒(méi)有中間層進(jìn)程比如nodemon、ts-node、babel-node會(huì)重置環(huán)境變量。另一個(gè)常見原因是用pm2 start --node-args--max-old-space-size4096啟動(dòng)多個(gè)實(shí)例時(shí)參數(shù)只應(yīng)用到了其中一個(gè)進(jìn)程。PM2的--node-args和ecosystem.config.js里的node_args需要注意優(yōu)先級(jí)后者會(huì)覆蓋前者如果你都寫了但結(jié)果不對(duì)檢查優(yōu)先級(jí)是個(gè)好方向。5.3 32位Node帶來(lái)的“假OOM”在某個(gè)老項(xiàng)目中部署鏡像基于32位基礎(chǔ)鏡像構(gòu)建的Node也是32位的。線上頻繁報(bào)OOM本地怎么也復(fù)現(xiàn)不了。后來(lái)查到了——32位Node的V8堆內(nèi)存上限被限制在1GB左右不管你怎么加--max-old-space-size超過(guò)這個(gè)數(shù)也不會(huì)生效。這種情況唯一的解法是更換64位基礎(chǔ)鏡像。如果你發(fā)現(xiàn)自己加了參數(shù)但process.memoryUsage()里的heapTotal始終在1GB上下波動(dòng)先看一眼process.arch是不是x64。這個(gè)細(xì)節(jié)在現(xiàn)在的新項(xiàng)目里幾乎遇不到但在維護(hù)老系統(tǒng)時(shí)很容易被坑到。6. 把內(nèi)存監(jiān)控做成常規(guī)活別等爆了才處理6.1 在應(yīng)用里暴露內(nèi)存指標(biāo)經(jīng)過(guò)了那次線上OOM事故之后我的習(xí)慣是任何要長(zhǎng)期運(yùn)行的Node服務(wù)都加一個(gè)健康檢查接口返回當(dāng)前進(jìn)程的內(nèi)存使用情況。const os require(os); app.get(/health, (req, res) { const mem process.memoryUsage(); res.json({ status: ok, uptime: process.uptime(), memory: { rss: mem.rss, heapTotal: mem.heapTotal, heapUsed: mem.heapUsed, external: mem.external, systemFree: os.freemem(), systemTotal: os.totalmem() } }); });heapUsed / heapTotal這個(gè)比例比絕對(duì)數(shù)值更有參考價(jià)值。如果長(zhǎng)期穩(wěn)定在95%以上說(shuō)明堆內(nèi)存在高位運(yùn)行隨時(shí)可能爆炸需要及時(shí)排查。6.2 用定時(shí)采樣畫出“內(nèi)存曲線”對(duì)于腳本型任務(wù)我會(huì)在腳本里加一個(gè)簡(jiǎn)單的采樣器每30秒記錄一次process.memoryUsage().heapUsed輸出到文件const fs require(fs); setInterval(() { const heapUsed process.memoryUsage().heapUsed; const timestamp new Date().toISOString(); fs.appendFileSync(memory-metrics.log, ${timestamp}, ${heapUsed}\n); }, 30000);跑完任務(wù)后把這份日志丟進(jìn)Excel或任何圖表工具里畫一條曲線。如果曲線是一條接近水平的線說(shuō)明內(nèi)存使用是健康的如果是一條單調(diào)上升的線哪怕上升速度很慢也說(shuō)明有問(wèn)題。這個(gè)方法雖然原始但比任何監(jiān)控系統(tǒng)都更容易堅(jiān)持執(zhí)行因?yàn)椴恍枰~外部署任何東西。6.3 記住這三個(gè)“不要”最后分享幾個(gè)經(jīng)驗(yàn)性總結(jié)都是我實(shí)際操作中的體會(huì)不要不管三七二十一就把--max-old-space-size拉到32GB。堆太大GC停頓長(zhǎng)進(jìn)程看起來(lái)像死了一樣堆太小頻繁GCCPU浪費(fèi)大。我一般從當(dāng)前峰值的1.5倍開始設(shè)運(yùn)行一段時(shí)間觀察GC日志再微調(diào)。不要只依賴NODE_OPTIONS來(lái)統(tǒng)一配置。它雖然方便但不利于團(tuán)隊(duì)協(xié)作因?yàn)閯e人clone代碼后看不到這個(gè)配置存在。我更推薦把參數(shù)直接寫進(jìn)package.json的scripts或者項(xiàng)目自帶啟動(dòng)腳本里讓配置成為項(xiàng)目的一部分。不要忽略Node版本升級(jí)帶來(lái)的默認(rèn)行為變化。不同版本的默認(rèn)最大堆內(nèi)存不完全一樣有的項(xiàng)目升級(jí)Node版本后突然不崩了有的則相反這都正常所以每次升級(jí)Node版本后建議重新壓一遍內(nèi)存相關(guān)的場(chǎng)景。內(nèi)存問(wèn)題的排查節(jié)奏就是先看是“一次性峰值”還是“持續(xù)增長(zhǎng)”再用GC日志和堆快照定位最后才是調(diào)參。把堆上限調(diào)大是手段但不是目的。真正健康的應(yīng)用會(huì)在一個(gè)合理的內(nèi)存水位上平穩(wěn)運(yùn)行而不是靠無(wú)數(shù)次崩潰-調(diào)參-再崩潰來(lái)維持運(yùn)轉(zhuǎn)。希望這篇經(jīng)驗(yàn)?zāi)軒湍闵僮咭恍澛贰?