歡迎光臨
每天分享高質量文章

容器中的JVM資源該如何被安全的限制?

前言

 

Java與Docker的結合,雖然更好的解決了application的封裝問題。但也存在著不相容,比如Java並不能自動的發現Docker設定的記憶體限制,CPU限制。
這將導致JVM不能穩定服務業務!容器會殺死你JVM行程,而健康檢查又將拉起你的JVM行程,進而導致你監控你的Pod一天重啟次數甚至能達到幾百次。
我們希望當Java行程執行在容器中時,Java能夠自動識別到容器限制,獲取到正確的記憶體和CPU資訊,而不用每次都需要在Kubernetes的yaml描述檔案中顯示的配置完容器,還需要配置JVM引數。
使用JVM MaxRAM引數或者解鎖實驗特性的JVM引數,升級JDK到10+,我們可以解決這個問題(也許吧~.~)。
首先Docker容器本質是是宿主機上的一個行程,它與宿主機共享一個/proc目錄,也就是說我們在容器內看到的/proc/meminfo,/proc/cpuinfo與直接在宿主機上看到的一致,如下。
Host:
  1. cat /proc/meminfo
  2. MemTotal:      197869260KB
  3. MemFree:         3698100KB
  4. MemAvailable:   62230260KB
容器:
  1. docker run -it --rm alpine cat /proc/meminfo
  2. MemTotal:      197869260KB
  3. MemFree:         3677800KB
  4. MemAvailable:   62210088KB
那麼Java是如何獲取到Host的記憶體資訊的呢?沒錯就是透過/proc/meminfo來獲取到的。
預設情況下,JVM的Max Heap Size是系統記憶體的1/4,假如我們系統是8G,那麼JVM將的預設Heap≈2G。
Docker透過CGroups完成的是對記憶體的限制,而/proc目錄是已只讀形式掛載到容器中的,由於預設情況下Java壓根就看不見CGroups的限制的記憶體大小,而預設使用/proc/meminfo中的資訊作為記憶體資訊進行啟動,這種不相容情況會導致,如果容器分配的記憶體小於JVM的記憶體,JVM行程會被理解殺死。
記憶體限制不相容

 

我們首先來看一組測試,這裡我們採用一臺記憶體為188G的物理機。
  1. #free -g
  2.          total used free shared buff/cache availableMem:      188   122   1      0     64           64
以下的測試中,我們將包含OpenJDK的hotspot虛擬機器,IBM的OpenJ9虛擬機器。
以下測試中,我們把正確識別到限制的JDK,稱之為安全(即不會超出容器限制不會被kill),反之稱之為危險。
測試用例1(OpenJDK)
這一組測試我們使用最新的OpenJDK8-12,給容器限制記憶體為4G,看JDK預設引數下的最大堆為多少?看看我們預設引數下多少版本的JDK是安全的命令如下,如果你也想試試看,可以用一下命令。
  1. docker run -m 4GB --rm  openjdk:8-jre-slim java  -XshowSettings:vm  -version
  2. docker run -m 4GB --rm  openjdk:9-jre-slim java  -XshowSettings:vm  -version
  3. docker run -m 4GB --rm  openjdk:10-jre-slim java -XshowSettings:vm  -version
  4. docker run -m 4GB --rm  openjdk:11-jre-slim java -XshowSettings:vm  -version
  5. docker run -m 4GB --rm  openjdk:12 java -XshowSettings:vm  -version
OpenJDK8(並沒有識別容器限制,26.67G) 危險。
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:8-jre-slim java  -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 26.67G
  4.    Ergonomics Machine Class: server
  5.    Using VM: OpenJDK 64-Bit Server VM
  6. openjdk version "1.8.0_181"
  7. OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)
  8. OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)
OpenJDK8 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap (正確的識別容器限制,910.50M)安全。
 
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:8-jre-slim java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 910.50M
  4.    Ergonomics Machine Class: server
  5.    Using VM: OpenJDK 64-Bit Server VM
  6. openjdk version "1.8.0_181"
  7. OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)
  8. OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)
OpenJDK 9(並沒有識別容器限制,26.67G)危險。
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:9-jre-slim java  -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 29.97G
  4.    Using VM: OpenJDK 64-Bit Server VM
  5. openjdk version "9.0.4"
  6. OpenJDK Runtime Environment (build 9.0.4+12-Debian-4)
  7. OpenJDK 64-Bit Server VM (build 9.0.4+12-Debian-4, mixed mode)
OpenJDK 9 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap(正確的識別容器限制,1G)安全。
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:9-jre-slim java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 1.00G
  4.    Using VM: OpenJDK 64-Bit Server VM
  5. openjdk version "9.0.4"
  6. OpenJDK Runtime Environment (build 9.0.4+12-Debian-4)
  7. OpenJDK 64-Bit Server VM (build 9.0.4+12-Debian-4, mixed mode)
OpenJDK 10(正確的識別容器限制,1G)安全。
  1. [root@xiaoke-test ~]# docker run -m 32GB --rm  openjdk:10-jre-slim java -XshowSettings:vm -XX:MaxRAMFraction=1  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 1.00G
  4.    Using VM: OpenJDK 64-Bit Server VM
  5. openjdk version "10.0.2" 2018-07-17
  6. OpenJDK Runtime Environment (build 10.0.2+13-Debian-2)
  7. OpenJDK 64-Bit Server VM (build 10.0.2+13-Debian-2, mixed mode)
OpenJDK 11(正確的識別容器限制,1G)安全。
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:11-jre-slim java -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 1.00G
  4.    Using VM: OpenJDK 64-Bit Server VM
  5. openjdk version "11.0.1" 2018-10-16
  6. OpenJDK Runtime Environment (build 11.0.1+13-Debian-3)
  7. OpenJDK 64-Bit Server VM (build 11.0.1+13-Debian-3, mixed mode, sharing)
OpenJDK 12(正確的識別容器限制,1G)安全。
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  openjdk:12 java -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 1.00G
  4.    Using VM: OpenJDK 64-Bit Server VM
  5. openjdk version "12-ea" 2019-03-19
  6. OpenJDK Runtime Environment (build 12-ea+23)
  7. OpenJDK 64-Bit Server VM (build 12-ea+23, mixed mode, sharing)
測試用例2(IBM OpenJ9)
  1. docker run -m 4GB --rm  adoptopenjdk/openjdk8-openj9:alpine-slim  java -XshowSettings:vm  -version
  2. docker run -m 4GB --rm  adoptopenjdk/openjdk9-openj9:alpine-slim  java -XshowSettings:vm  -version
  3. docker run -m 4GB --rm  adoptopenjdk/openjdk10-openj9:alpine-slim  java -XshowSettings:vm  -version
  4. docker run -m 4GB --rm  adoptopenjdk/openjdk11-openj9:alpine-slim  java -XshowSettings:vm  -version
OpenJDK8-OpenJ9(正確的識別容器限制,3G)安全。
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  adoptopenjdk/openjdk8-openj9:alpine-slim  java -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 3.00G
  4.    Ergonomics Machine Class: server
  5.    Using VM: Eclipse OpenJ9 VM
  6. openjdk version "1.8.0_192"
  7. OpenJDK Runtime Environment (build 1.8.0_192-b12_openj9)
  8. Eclipse OpenJ9 VM (build openj9-0.11.0, JRE 1.8.0 Linux amd64-64-Bit Compressed References 20181107_95 (JIT enabled, AOT enabled)
  9. OpenJ9   - 090ff9dcd
  10. OMR      - ea548a66
  11. JCL      - b5a3affe73 based on jdk8u192-b12)
OpenJDK9-OpenJ9 (正確的識別容器限制,3G)安全。
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  adoptopenjdk/openjdk9-openj9:alpine-slim  java -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 3.00G
  4.    Using VM: Eclipse OpenJ9 VM
  5. openjdk version "9.0.4-adoptopenjdk"
  6. OpenJDK Runtime Environment (build 9.0.4-adoptopenjdk+12)
  7. Eclipse OpenJ9 VM (build openj9-0.9.0, JRE 9 Linux amd64-64-Bit Compressed References 20180814_248 (JIT enabled, AOT enabled)
  8. OpenJ9   - 24e53631
  9. OMR      - fad6bf6e
  10. JCL      - feec4d2ae based on jdk-9.0.4+12)
OpenJDK10-OpenJ9 (正確的識別容器限制,3G)安全。
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  adoptopenjdk/openjdk10-openj9:alpine-slim  java -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 3.00G
  4.    Using VM: Eclipse OpenJ9 VM
  5. openjdk version "10.0.2-adoptopenjdk" 2018-07-17
  6. OpenJDK Runtime Environment (build 10.0.2-adoptopenjdk+13)
  7. Eclipse OpenJ9 VM (build openj9-0.9.0, JRE 10 Linux amd64-64-Bit Compressed References 20180813_102 (JIT enabled, AOT enabled)
  8. OpenJ9   - 24e53631
  9. OMR      - fad6bf6e
  10. JCL      - 7db90eda56 based on jdk-10.0.2+13)
OpenJDK11-OpenJ9(正確的識別容器限制,3G)安全。
  1. [root@xiaoke-test ~]# docker run -m 4GB --rm  adoptopenjdk/openjdk11-openj9:alpine-slim  java -XshowSettings:vm  -version
  2. VM settings:
  3.    Max. Heap Size (Estimated): 3.00G
  4.    Using VM: Eclipse OpenJ9 VM
  5. openjdk version "11.0.1" 2018-10-16
  6. OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.1+13)
  7. Eclipse OpenJ9 VM AdoptOpenJDK (build openj9-0.11.0, JRE 11 Linux amd64-64-Bit Compressed References 20181020_70 (JIT enabled, AOT enabled)
  8. OpenJ9   - 090ff9dc
  9. OMR      - ea548a66
  10. JCL      - f62696f378 based on jdk-11.0.1+13)
分析
分析之前我們先瞭解這麼一個情況:
  1. JavaMemory (MaxRAM) = 元資料+執行緒+程式碼快取+OffHeap+Heap...
一般我們都只配置Heap即使用-Xmx來指定JVM可使用的最大堆。而JVM預設會使用它獲取到的最大記憶體的1/4作為堆的原因也是如此。
 
安全性(即不會超過容器限制被容器kill)
OpenJDK:
OpenJdk8-12,都能保證這個安全性的特點(8和9需要特殊引數,-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap)。
OpenJ9:
IBM OpenJ9所有的版本都能識別到容器限制。
 
資源利用率
OpenJDK:
自動識別到容器限制後,OpenJDK把最大堆設定為了大概容器記憶體的1/4,對記憶體的浪費不可謂不大。
當然可以配合另一個JVM引數來配置最大堆。-XX:MaxRAMFraction=int。下麵是我整理的一個常見記憶體設定的表格,從中我們可以看到似乎JVM預設的最大堆的取值為MaxRAMFraction=4,隨著記憶體的增加,堆的閑置空間越來越大,在16G容器記憶體時,Java堆只有不到4G。
  1. MaxRAMFraction取值    堆佔比 容器記憶體=1G 容器記憶體=2G 容器記憶體=4G   容器記憶體=8G   容器記憶體=16G
  2. 1                     90%  910.50M     1.78G       3.56G      7.11G      14.22G
  3. 2                     50%  455.50M     910.50M     1.78G      3.56G      7.11G
  4. 3                     33%  304.00M     608.00M     1.19G      2.37G      4.74G
  5. 4                     25%  228.00M     455.50M     910.50M    1.78G      3.56G
OpenJ9:
關於OpenJ9的的詳細介紹你可以從https://www.eclipse.org/openj9/docs/xxusecontainersupport/瞭解更多。
對於記憶體利用率OpenJ9的策略是優於OpenJDK的。以下是OpenJ9的策略表格。
  1. 容器記憶體<size>    最大Java堆大小
  2. 小於1GB           50%<size>
  3. 1GB-2GB          -512MB
  4. 大於2GB           大於2GB
結論
註意:這裡我們說的是容器記憶體限制,和物理機記憶體不同。
 
自動檔
如果你想要的是,不顯示的指定-Xmx,讓Java行程自動的發現容器限制。
如果你想要的是JVM行程在容器中安全穩定的執行,不被容器kiil,並且你的JDK版本小於10(大於等於JDK10的版本不需要設定,參考前面的測試)。
你需要額外設定JVM引數-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap,即可保證你的Java行程不會因為記憶體問題被容器Kill。
當然這個方式使用起來簡單,可靠,缺點也很明顯,資源利用率過低(參考前面的表格MaxRAMFraction=4)。
如果想在基礎上我還想提高一些記憶體資源利用率,並且容器記憶體為1GB – 4GB,我建議你設定-XX:MaxRAMFraction=2,在大於8G的可以嘗試設定-XX:MaxRAMFraction=1(參考上表格)。
 
手動擋
如果你想要的是手動擋的體驗,更加進一步的利用記憶體資源,那麼你可能需要回到手動配置時代-Xmx。
手動擋部分,請可以完全忽略上面我的BB。
上面我們說到了自動擋的配置,用起來很簡單很舒服,自動發現容器限制,無需擔心和思考去配置-Xmx。
比如你有記憶體1G那麼我建議你的-Xmx750M,2G建議配置-Xmx1700M,4G建議配置-Xmx3500-3700M,8G建議設定-Xmx7500-7600M,總之就是至少保留300M以上的記憶體留給JVM的其他記憶體。如果堆特別大,可以預留到1G甚至2G。
手動擋用起來就沒有那麼舒服了,當然資源利用率相對而言就更高了。
原文連結:https://qingmu.io/2018/12/17/How-to-securely-limit-JVM-resources-in-a-container/

贊(0)

分享創造快樂