在本文中,我將分享我在 Node.js 中追蹤和修復高記憶體使用率的方法。
最近我收到了一張標題為「修復庫 x 中的記憶體洩漏問題」的票證。該描述包括一個 Datadog 儀表板,其中顯示了十幾個遭受高內存使用率並最終因 OOM(內存不足)錯誤而崩潰的服務,並且它們都有共同的 x 庫。
我最近才接觸到程式碼庫(不到 2 週),這使得這項任務具有挑戰性,也值得分享。
我開始使用兩個訊息:
有一個所有服務都使用的庫導致記憶體使用率很高,它涉及到redis(redis包含在庫的名稱中)。
受影響的服務清單。
以下是連結到票證的儀表板:
服務在 Kubernetes 上執行,很明顯,服務會隨著時間的推移累積內存,直到達到內存限制、崩潰(回收內存)並重新啟動。
在本節中,我將分享我如何處理手邊的任務,找出高記憶體使用率的罪魁禍首,然後修復它。
由於我對程式碼庫相當陌生,我首先想了解程式碼、相關函式庫的作用以及應該如何使用它,希望透過這個過程可以更容易地辨識問題。不幸的是,沒有適當的文件,但透過閱讀程式碼和搜尋服務如何利用該庫,我能夠理解它的要點。它是一個圍繞 redis 流的庫,並為事件生成和消費提供方便的介面。花了一天半的時間閱讀程式碼,由於程式碼結構和複雜性(許多我不熟悉的類別繼承和rxjs ),我無法掌握所有細節以及資料如何流動。
因此,我決定暫停閱讀,並嘗試在觀察程式碼執行情況並收集遙測資料的同時發現問題。
由於沒有可用的分析資料(例如連續分析)可以幫助我進一步調查,因此我決定在本地複製該問題並嘗試捕獲記憶體配置檔案。
我發現了幾種在 Node.js 中捕獲記憶體設定檔的方法:
由於不知道該去哪裡尋找,我決定執行我認為是庫中最「資料密集」的部分,即 redis 流生產者和消費者。我建立了兩個簡單的服務,它們可以產生和使用來自 redis 流的資料,然後我繼續捕獲記憶體配置文件並比較一段時間內的結果。不幸的是,在對服務產生負載並比較配置文件幾個小時後,我無法發現這兩個服務中任何一個服務的記憶體消耗有任何差異,一切看起來都很正常。該庫公開了許多不同的介面以及與 Redis 流交互的方式。我清楚地意識到,複製這個問題比我預期的要複雜得多,尤其是我對實際服務的特定領域知識有限。
那麼問題是,如何找到合適的時機和條件來捕獲記憶體洩漏?
如前所述,捕獲記憶體設定檔的最簡單、最方便的方法是對受影響的實際服務進行連續分析,但我沒有這個選項。我開始研究如何至少利用我們的臨時服務(它們面臨著同樣高的記憶體消耗),這將使我無需額外的努力即可捕獲所需的資料。
我開始尋找一種將 Chrome DevTools 連接到其中一個正在執行的 Pod 並隨著時間的推移捕獲堆快照的方法。我知道記憶體洩漏發生在暫存階段,因此,如果我能夠捕獲該資料,我希望能夠至少發現一些熱點。令我驚訝的是,有一種方法可以做到這一點。
執行此操作的過程
SIGUSR1
訊號來啟用 pod 上的 Node.js 偵錯器。kubectl exec -it <nodejs-pod-name> -- kill -SIGUSR1 <node-process-id>
有關 Node.js 訊號的更多訊息,請參閱Signal Events
如果成功,您應該會看到來自服務的日誌:
Debugger listening on ws://127.0.0.1:9229/....
For help, see: https://nodejs.org/en/docs/inspector
kubectl port-forward <nodejs-pod-name> 9229
chrome://inspect/
,您應該在目標清單中看到您的 Node.js 進程:如果沒有,請確保您的目標發現設定正確設定
現在您可以開始捕捉逾時快照(時間段取決於記憶體洩漏發生所需的時間)並進行比較。 Chrome DevTools 提供了一種非常方便的方法來做到這一點。
您可以在記錄堆快照中找到有關內存快照和 Chrome 開發工具的更多訊息
建立快照時,主執行緒中的所有其他工作都會停止。根據堆內容,甚至可能需要一分多鐘的時間。快照內建在記憶體中,因此它可以使堆大小加倍,從而導致填滿整個內存,然後使應用程式崩潰。
如果您要在生產中取得堆疊快照,請確保從中取得快照的進程可以崩潰,而不會影響應用程式的可用性。
回到我的例子,選擇兩個快照進行比較並按增量排序,我得到了您在下面看到的內容。
我們可以看到最大的正增量發生在string
建構函數上,這意味著該服務在兩個快照之間建立了許多字串,但它們仍在使用中。現在的問題是它們是在哪裡建立的以及誰在引用它們。幸運的是,捕獲的快照包含此資訊以及稱為Retainers
資訊。
在深入研究快照和永不縮小的字串列表時,我注意到一種類似於 id 的字串模式。單擊它們,我可以看到引用它們的鏈物件 - 又稱Retainers
。這是一個名為sentEvents
的陣列,其類別名稱是我可以從庫程式碼中辨識出來的。哎呀,我們找到了罪魁禍首,一個不斷增長的 ID 列表,到目前為止我認為這些 ID 從未被發布過。我加班拍攝了一堆快照,這是唯一一個不斷重新出現為具有較大正增量的熱點的地方。
有了這些訊息,我不需要嘗試完全理解程式碼,而是需要關注陣列的用途、何時填充和何時清除。在一個地方,程式碼將專案pushing
送到陣列,而在另一個地方,程式碼將專案popping
,這縮小了修復的範圍。
可以安全地假設陣列在應該清空的時候沒有被清空。跳過程式碼的細節,基本上發生的事情是這樣的:
該庫公開了用於消費、生成事件或生成和消費事件的介面。
當它既消耗又產生事件時,它需要追蹤進程本身產生的事件,以便跳過它們而不重新消耗它們。 sentEvents
在生成時被填充,並在嘗試使用時被清除,它會跳過訊息。
你看得出來這是怎麼回事嗎? ?當服務僅使用該庫來產生事件時, sentEvents
仍會填入所有事件,但沒有用於清除它的程式碼路徑(使用者)。
我修補了程式碼以僅追蹤生產者、消費者模式上的事件並部署到登台。即使存在暫存負載,很明顯該補丁也有助於減少高記憶體使用率,並且沒有引入任何回歸。
當修補程式部署到生產環境時,記憶體使用量大幅減少,服務可靠性得到提高(不再出現 OOM)。
一個不錯的副作用是處理相同流量所需的 Pod 數量減少了 50%。
對我來說,這是一個很好的學習機會,可以追蹤 Node.js 中的記憶體問題並進一步熟悉可用的工具。
我認為最好不要詳細討論每個工具的細節,因為這值得單獨發表一篇文章,但我希望這對於任何有興趣了解更多有關此主題或面臨類似問題的人來說是一個很好的起點。
原文出處:https://dev.to/gkampitakis/tracking-down-high-memory-usage-in-nodejs-2lbn