[轉帖]Java程序在K8S容器部署CPU和Memory資源限制相關設置

https://developer.aliyun.com/article/700701

 


 
簡介: 背景在k8s docker環境中執行Java程序,因爲我們設置了cpu,memory的limit,所以Java程序執行時JVM的參數沒有跟我們設置的參數關聯,導致JVM感知到的cpu和memory是我們k8s的work node上的cpu和memory大小。

背景

在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種方法可以進行:

  1. 建議升級到191以後的版本,然後根據cpu limit配置ActiveProcessorCount。
  2. 不升級jdk版本,直接設置跟ActiveProcessorCount參數相關的GC參數:比如ParallelGCThreads,CICompilerCount。如果是1.8.0_131以前的版本,可以用-Xms, -Xmx參數進行堆空間的大小分配,注意這兩個參數只設置了分配給堆的大小,實際的memory limit應該比這個大。這種方案不是一個best practice,畢竟這樣沒有用到JVM自動適配的一些參數。最關鍵的,此種方法不能避免很多Java庫根據availableProcessors()來做相應邏輯處理。

參考資料

  1. Assign Memory Resources to Containers and Pods
  2. Assign CPU Resources to Containers and Pods
  3. Kubernetes Demystified: Restrictions on Java Application Resources
  4. JVM 對 docker 容器 CPU 限制的兼容
  5. Java SE support for Docker CPU and memory limits
  6. 關於Jvm知識看這一篇就夠了
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章