https://developer.aliyun.com/article/700701
背景
在k8s docker環境中執行Java程序,因爲我們設置了cpu,memory的limit,所以Java程序執行時JVM的參數沒有跟我們設置的參數關聯,導致JVM感知到的cpu和memory是我們k8s的work node上的cpu和memory大小。這樣造成的問題是:當容器中Java程序使用內存超過memory limit時,直接造成Out of Memory錯誤,從而引起容器重啓。JVM很多參數也是很智能的,啓動時內存的分配也會根據cpu和memory進行調整,比如GC相關的參數就是動態調整的。如果容器感知到的cpu核數不對,那麼對程序的性能也會造成很大的影響。
內存
Java對內存的使用有幾個參數可以配置。以前的版本可以用-Xms, -Xmx來分別設置初始化Java堆大小和最大的Java堆大小。但因爲Java堆大小並不等於所有可用的內存大小,所以在設置memory limit的時候會加一個值。這樣避免Java使用的內存超過分配給容器的最大內存限制。這個增加的值需要一定的經驗和測試來獲取。
JVM後來提供了UseCGroupMemoryLimitForHeap參數來讓JVM自動根據我們提供的內存限制來分配堆的大小。這樣也就避免了我們人爲去確定應該給堆多大的空間。只要經過測試,確定這個Java程序佔用的總共空間就行了。使用方法是在java運行後面加上參數:java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap ⋯
CPU
配了上面的參數,我們還沒有完全解決問題。因爲JVM GC相關的參數跟CPU處理器核相關聯的,可使用的CPU核數越多,分配給GC的線程資源也越多。如果我們不設置正確的CPU核數給容器,那麼它看到的就是整個k8s worker node的CPU個數,比如我們限制容器可使用2core,但worker node有32core。那麼這個容器會給GC分配很多的線程資源,從而嚴重影響正常Java線程的運行。
CPU個數對JVM GC的影響
JVM提供了ActiveProcessorCount參數來設置這個值。但這個參數只在java 1.8.0_191以後版本才支持。下面我在筆記本上做了測試(total 8 cores),看看這個參數如何影響GC的參數。
Step1: 寫一個hello wold程序。
root@kyle:~# cat Hello.java
public class Hello{
public static void main(String[] args){
System.out.println("hello world");
}
Step2: 編譯
root@kyle:~# javac Hello.java
Step3: 不加參數運行
root@kyle:~# java -XX:+PrintFlagsFinal Hello > init.txt
[Global flags]
intx ActiveProcessorCount = -1 {product}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product}
…
Step4: 加不同參數值運行
root@kyle:~# java -XX:ActiveProcessorCount=1 -XX:+PrintFlagsFinal Hello > p1.txt
root@kyle:~# java -XX:ActiveProcessorCount=2 -XX:+PrintFlagsFinal Hello > p2.txt
root@kyle:~# java -XX:ActiveProcessorCount=4 -XX:+PrintFlagsFinal Hello > p4.txt
root@kyle:~# java -XX:ActiveProcessorCount=8 -XX:+PrintFlagsFinal Hello > p8.txt
Step5: 看看不同參數對GC的影響:
1個處理器跟2個處理器的比較:
root@kyle:~# diff p1.txt p2.txt
2c2
< intx ActiveProcessorCount := 1 {product}
---
> intx ActiveProcessorCount := 2 {product}
304c304
< uintx MarkSweepDeadRatio = 5 {product}
---
> uintx MarkSweepDeadRatio = 1 {product}
311c311
< uintx MaxHeapFreeRatio = 70 {manageable}
---
> uintx MaxHeapFreeRatio = 100 {manageable}
335,336c335,336
< uintx MinHeapDeltaBytes := 196608 {product}
< uintx MinHeapFreeRatio = 40 {manageable}
---
> uintx MinHeapDeltaBytes := 524288 {product}
> uintx MinHeapFreeRatio = 0 {manageable}
388c388
< uintx ParallelGCThreads = 0 {product}
---
> uintx ParallelGCThreads = 2 {product}
682,683c682,683
< bool UseParallelGC = false {product}
< bool UseParallelOldGC = false {product}
---
> bool UseParallelGC := true {product}
> bool UseParallelOldGC = true {product}
2個處理器跟4個處理器的比較:
root@kyle:~# diff p2.txt p4.txt
2c2
< intx ActiveProcessorCount := 2 {product}
---
> intx ActiveProcessorCount := 4 {product}
59c59
< intx CICompilerCount := 2 {product}
---
> intx CICompilerCount := 3 {product}
388c388
< uintx ParallelGCThreads = 2 {product}
---
> uintx ParallelGCThreads = 4 {product}
4個處理器跟8個處理器的比較:
root@kyle:~# diff p4.txt p8.txt
2c2
< intx ActiveProcessorCount := 4 {product}
---
> intx ActiveProcessorCount := 8 {product}
59c59
< intx CICompilerCount := 3 {product}
---
> intx CICompilerCount := 4 {product}
388c388
< uintx ParallelGCThreads = 4 {product}
---
> uintx ParallelGCThreads = 8 {product}
不加參數跟8個處理器的比較:
root@kyle:~# diff init.txt p8.txt
2c2
< intx ActiveProcessorCount = -1 {product}
---
> intx ActiveProcessorCount := 8 {product}
從上面比較可以看出,不設這個參數跟設置最大參數(當前系統是8core)是一樣的。2,4,8核設置隻影響ParallelGCThreads, CICompilerCount。但如果只用1核的話,UseParallelGC,UseParallelOldGC都變爲false,同時也會影響其它幾個參數。見上面diff p1.txt p2.txt比較結果。
CPU個數對Java程序的影響
CPU個數的設置除了對JVM GC性能產生影響外,對Java的工作線程也會產生影響。以下的代碼常用於Java庫,它會根據CPU的個數產生工作線程。如果沒有正確設置docker中的參數,對實際的程序性能會產生很大的影響。
Runtime.getRuntime().availableProcessors()
以下代碼摘自aliyun-log-java-producer庫,是根據可用處理器來產生相應個數的IO線程來發送loghub數據。
# ProducerConfig.java:
public class ProducerConfig {
public static final int DEFAULT_IO_THREAD_COUNT =
Math.max(Runtime.getRuntime().availableProcessors(), 1);
OpenJDK版本
我們運行以下命令檢查JDK的版本。openjdk version "1.8.0_131"以後支持UseCGroupMemoryLimitForHeap參數,"1.8.0_191"以後才支持ActiveProcessorCount這個參數。
root@kyle:~# java -version
openjdk version "1.8.0_191"
OpenJDK Runtime Environment (build 1.8.0_191-8u191-b12-2ubuntu0.16.04.1-b12)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)
改進方案
如果我們使用的JDK版本支持這2個參數,那麼我們只需要在運行Java程序時把這UseCGroupMemoryLimitForHeap參數加上,同時再給ActiveProcessorCount參數賦值實際分配給容器的cpu limit就可以了。如果目前的JDK版本低於1.8.0_191,即不支持ActiveProcessorCount,針對這個情況,有2種方法可以進行:
- 建議升級到191以後的版本,然後根據cpu limit配置ActiveProcessorCount。
- 不升級jdk版本,直接設置跟ActiveProcessorCount參數相關的GC參數:比如ParallelGCThreads,CICompilerCount。如果是1.8.0_131以前的版本,可以用-Xms, -Xmx參數進行堆空間的大小分配,注意這兩個參數只設置了分配給堆的大小,實際的memory limit應該比這個大。這種方案不是一個best practice,畢竟這樣沒有用到JVM自動適配的一些參數。最關鍵的,此種方法不能避免很多Java庫根據availableProcessors()來做相應邏輯處理。