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