在K8s中調整JVM提高CPU和記憶體利用率 - Anurag

banq發表於2022-12-08

JVM 是有史以來最古老但功能最強大的虛擬機器之一。

每當一個新的 JVM 程式啟動時,所有需要的類都會被ClassLoader的一個例項載入到記憶體中。這個過程分三個步驟進行:

  1. Bootstrap 類載入: “ Bootstrap 類載入器”將 Java 程式碼和基本的 Java 類(如java.lang.Object )載入到記憶體中。這些載入的類位於JRElibrt.jar中。
  2. 擴充套件類載入:ExtClassLoader 負責載入位於java.ext.dirs路徑的所有 JAR 檔案。在非 Maven 或非基於 Gradle 的應用程式中,開發人員手動新增 JAR,所有這些類都在此階段載入。
  3. 應用程式類載入:AppClassLoader 載入位於應用程式類路徑中的所有類。

此初始化過程基於延遲載入方案。
一旦類載入完成,所有重要的類(在程式啟動時使用)都會被推送到JVM 快取(本機程式碼)中——這使得它們在執行時可以更快地訪問。其他類是根據每個請求載入的。
對 Java Web 應用程式發出的第一個請求通常比程式生命週期內的平均響應時間慢得多。這個初始啟動階段通常可以歸因於延遲類載入和即時編譯。
請記住這一點,對於低延遲應用程式,我們需要事先快取所有類——以便在執行時訪問它們時立即可用。

這個調整 JVM 的過程稱為預熱:
JVM 預熱期間的高延遲是一個突出的問題。基於 JVM 的應用程式可提供出色的效能,但在達到最高速度之前需要一些時間來“預熱”。當應用程式啟動時,它通常以降低的效能開始。它可以歸因於諸如實時 (JIT) 編譯之類的東西,它透過收集使用配置檔案資訊來最佳化常用程式碼。這樣做的淨負面影響是,與平均水平相比,在此預熱期間收到的請求將具有非常長的響應時間。在容器化、高吞吐量、頻繁部署和自動縮放的環境中,這個問題可能會加劇

在本文中,我將討論 JVM 預熱問題、我們 Kubernetes 叢集中的高堆記憶體利用率以及我們如何處理這些問題以及我們從中學到的東西。

當遭遇高延遲問題時,我們不清楚這是一個與 JVM 預熱相關的問題時,解決這個問題的最簡單方法是將 pod 增加到穩定狀態下所需數量的 3 倍左右。這無疑解決了我們的問題,但導致了高昂的基礎設施成本。在深入研究這個問題時,我們發現還有其他方法可以解決同樣的問題。

JVM需要更多的CPU,在我們的案例中,在持續幾分鐘的初始預熱階段,它比配置的限制x要多3倍。在預熱之後,JVM可以在其全部潛力下舒適地執行,即使CPU為x。 如果所需的CPU不可用,pod CPU會被節流到可用的資源,這就導致了整個問題。

有一個簡單的方法來驗證這一點。Kubernetes提供了一個每個pod的指標container_cpu_cfs_throttled_seconds_total,它表示--從這個pod開始,CPU被節制限制了多少秒。

由於這種節流限制,預熱需要時間,在此之前,延遲增加,導致請求排隊,因此導致執行緒在等待狀態下的高狀態。

Kubernetes是使用 "請求 "而不是 "限制 "來安排pod
這是我們在除錯問題時遇到的一個非常重要的資訊。
Kubernetes根據配置的資源請求和限制,為pod分配QoS類。

在研究了關於QoS類Kubernetes Burstable QoS後,我們問題的答案似乎很清楚了:

由於Kubernetes使用請求中指定的值來排程pod,它將找到有x個空閒CPU容量的節點來排程這個pod。但由於限制是更高的3倍,如果應用程式在任何時候都需要比x更多的CPU,並且如果該節點上有空閒的CPU容量,應用程式將不會在CPU上被扼殺。如果有的話,它最多可以使用3倍。

這與我們的問題陳述非常吻合。在熱身階段,當JVM需要更多的CPU時,它可以透過突發獲得。一旦JVM被最佳化,它就可以在請求範圍內全速前進。這使得我們可以使用叢集中的備用容量(如果我們沒有備用容量,我們仍然會面臨這個問題,但是8/10的時候,我們往往有那個額外的容量,這是需要限制的)來解決預熱問題,而不需要任何額外的成本。

在減少CPU資源後,是時候研究我們系統的高記憶體使用率了。
當我們在最佳化我們的成本時,我們發現為我們的服務分配了非常高的記憶體資源,因為堆的大小會不斷增加,如果資源不高,Pod就會在記憶體需求沒有得到滿足時陷入CrashLoop。

無效的垃圾回收被認為是我們服務中堆大小高的主要原因,因為堆記憶體曾經逐漸增加,直到主要的GC,導致高記憶體使用,最終需要提供高資源以保持服務穩定。

垃圾收集
垃圾收集(又稱GC)是Java最重要的功能之一。垃圾收集是Java中用來取消分配未使用的記憶體的機制,也就是清除未使用的物件所消耗的空間。為了清除未使用的記憶體,垃圾收集器跟蹤所有仍在使用的物件,並將其餘物件標記為垃圾。基本上,垃圾收集器使用標記和掃除演算法來清除未使用的記憶體。這些是以下型別。

1.序列垃圾收集器
序列垃圾收集器透過保持所有的應用程式執行緒來工作。它是為單執行緒環境設計的。它只使用一個單執行緒進行垃圾收集。它的工作方式是在進行垃圾收集時凍結所有的應用執行緒,這可能不適合伺服器環境。它最適合於簡單的命令列程式。

開啟-XX:+UseSerialGC JVM引數以使用序列垃圾收集器。

2.並行垃圾收集器
並行垃圾收集器也被稱為吞吐量收集器。它是JVM的預設垃圾收集器。與序列垃圾收集器不同,它使用多個執行緒進行垃圾收集。與序列垃圾收集器類似,它在執行垃圾收集時也凍結了所有的應用程式執行緒。

3.CMS垃圾收集器
併發標記掃除(CMS)垃圾收集器使用多個執行緒來掃描堆記憶體,以標記要驅逐的例項,然後掃除標記的例項。CMS垃圾收集器只在以下兩種情況下保留所有的應用執行緒。

  • 同時在保有的生成空間中對引用的物件進行標記。
  • 如果在做垃圾收集時,並行的堆記憶體有變化。

與並行垃圾收集器相比,CMS收集器使用更多的CPU來保證更好的應用吞吐量。如果我們可以分配更多的CPU以獲得更好的效能,那麼CMS垃圾收集器是比並行收集器更優先的選擇。

開啟XX:+USeParNewGC JVM引數以使用CMS垃圾收集器。

4.G1垃圾收集器
G1垃圾收集器用於大型堆記憶體區域。它將堆記憶體分成若干區域,並在這些區域內進行並行收集。G1在回收記憶體後,也會對空閒的堆空間進行壓縮,而CMS垃圾收集器在停止世界(STW)的情況下對記憶體進行壓縮。G1垃圾收集器會根據最多垃圾的情況來確定區域的優先順序。

開啟-XX:+UseG1GC JVM引數以使用G1垃圾收集器。

我們從Java 8預設的GC(Parallel GC)切換到G1 GC,它是為了有效地處理高的Heap大小。這導致了整體的堆大小不變,因此我們可以減少記憶體資源,並在服務中保持同樣的穩定性,其結果是驚人的。

主要經驗

  • 如果節點沒有我們指定的所需限制,我們仍然可能面臨CPU節流,因為pods是根據請求而不是限制來安排的。
  • 我們不應該過分依賴穩定狀態的限制,而應該保持我們的請求足夠高,以滿足應用程式的穩定狀態要求。
  • 在使用G1 GC的同時,我們也應該嘗試使用重複資料刪除,因為大多數網路應用都大量使用字串,所以我認為其優勢會非常明顯。字串重複資料刪除是一項Java功能,可以幫助你節省Java應用程式中重複的字串物件所佔用的記憶體。要使用它,我們需要在JAVA_OPTS中加入以下內容: - XX:+UseStringDeduplication

相關文章