Docker——JVM 感知容器的 CPU 和 Memory 資源限制


 

前言

對於那些在Java應用程序中使用Docker的CPU和內存限制的人來說,可能會遇到一些挑戰。特別是CPU限制,因為JVM在內部透明地設置GC線程和JIT編譯器線程的數量。

這些可以通過命令行選項 -XX:ParallelGCThreads 和 -XX:CICompilerCount 顯式設置。對於內存限制,也可以通過JVM命令行選項 -Xmx 顯式設置最大Java堆大小。

但是,在沒有指定上述JVM命令行選項的情況下,當使用Java SE 8u121和更早版本的Java應用程序在Docker容器中運行時,可能會出現以下問題:

  • 老的 JVM 版本並不能自動的發現Docker設置的內存限制,CPU限制。這將導致JVM不能穩定服務業務,容器會殺死你JVM進程,而健康檢查又將拉起你的JVM進程,進而導致一天重啟次數甚至能達到幾百次

首先Docker容器本質是是宿主機上的一個進程,它與宿主機共享一個/proc目錄,也就是說我們在容器內看到的/proc/meminfo/proc/cpuinfo

與直接在宿主機上看到的一致,如下:

Host:

1
2
3
4
cat /proc/meminfo 
MemTotal: 197869260 kB
MemFree: 3698100 kB
MemAvailable: 62230260 kB

容器:

1
2
3
4
docker run -it --rm alpine cat /proc/meminfo
MemTotal: 197869260 kB
MemFree: 3677800 kB
MemAvailable: 62210088 kB

那么Java是如何獲取到Host的內存信息的呢?沒錯就是通過/proc/meminfo來獲取到的。

默認情況下,JVM的Max Heap Size是系統內存的1/4,假如我們系統是8G,那么JVM將的默認Heap≈2G。

Docker通過CGroups完成的是對內存的限制,而/proc目錄是已只讀形式掛載到容器中的,由於默認情況下Java壓根就看不見CGroups的限制的內存大小,而默認使用/proc/meminfo中的信息作為內存信息進行啟動,

這種不兼容情況會導致,如果容器分配的內存小於JVM的內存,JVM進程會被理解殺死。

  • 發現 “Parallel GC Threads” 和 “C* CompilerThread” 的線程數量不正常

以一個 CPU 設置為 4 的 docker 容器為例,“Parallel GC Threads” 線程數的計算公式在 vm_version.cpp 中:

  1)如果cpu核心數目少於等於8,則GC線程數量和CPU數一致

  2)如果cpu核心數大於8,則前8個核,每個核心對應一個GC線;其他核,每8個核對應5個GC線程

如果 os::active_processor_count() 返回 4,那么線程數應該是 4;但是實際的線程數為 33,可以反推 JVM 獲取到的 CPU 核心數為 48,與物理機的核心數一致。

  • 使用Runtime.getRuntime().availableProcessors() ,會拿到宿主機CPU個數,而不是容器申請時的CPU個數

 

 

JDK 版本差異

  • 老的 JVM 版本(JDK 8u131以前)是無法感知容器的資源限制的。
  • JDK 8u131開始,在JDK 9,JVM可以透明地了解Docker的CPU限制。

 

 

CPU 限制

  • Java SE 8u131 和 JDK9

如果沒有將 -XX:paralllelgthreads-XX:CICompilerCount 指定為命令行選項,JVM將應用Docker CPU限制作為JVM在系統上看到的CPU數量。

然后,JVM將調整GC線程和JIT編譯器線程的數量,就像它在裸機系統上運行一樣,CPU數量設置為Docker CPU限制。

如果 -XX:ParallelGCThreads-XX:CICompilerCount 被指定為JVM命令行選項,並且指定了Docker CPU限制,JVM將使用 -XX:ParallelGCThreads-XX:CICompilerCount 值。

只支持 --cpuset-cpus 這種指定固定 CPU 的方式:

docker run -it --cpuset-cpus="0" ubuntu /bin/bash
  • Java SE 8u191 和 JDK10

JVM知道在Docker容器中運行,並將提取特定於容器的配置信息,而不是從宿主機提取。正在提取的信息是已分配給容器的CPU數量和總內存。

Java進程可用的cpu總數是根據任何指定的cpu集、cpu共享或cpu配額計算的。此支持僅在基於Linux的平台上可用。默認情況下,此新支持是啟用的,可以在命令行中使用JVM選項禁用:

-XX:-UseContainerSupport

此外,此更改還添加了一個JVM選項,該選項提供指定JVM將使用的cpu數量的能力:

-XX:ActiveProcessorCount=count

完整示例:

docker run -it --cpus=2 ubuntu /bin/bash

docker run -it --cpu-period=800000 --cpu-quota=100000 ubuntu /bin/bash

如果你對 docker 不太熟悉,可以通過官方文檔理解cpus、cpu_quota、cpu_period 這三個配置項

 

 

Memory 限制

  • Java SE 8u131 和 JDK9

對於Docker內存限制,最大Java堆的透明設置還有一些工作要做。要告訴JVM在沒有通過 -Xmx 設置最大Java堆的情況下注意Docker內存限制,需要兩個JVM命令行選項:

-XX:+UnlockExperimentalVMOptions 和 -XX:+UseCGroupMemoryLimitForHeap

-XX:+UnlockExperimentalVMOptions 是必需的,因為在將來的版本中,目標是透明地標識Docker內存限制。

當使用這兩個JVM命令行選項並且未指定 -Xmx 時,JVM將查看Linux cgroup配置,這是Docker容器用於設置內存限制的配置,以便透明地調整最大Java堆大小。

僅供參考,Docker容器也使用cGroup配置來限制CPU。

  • Java SE 8u191 和 JDK10

添加了三個新的JVM選項,以允許Docker容器用戶更細粒度地控制將用於Java堆的系統內存量:

-XX:InitialRAMPercentage    #初始百分比
-XX:MaxRAMPercentage       #最大百分比
-XX:MinRAMPercentage        #最小百分比

這些選項替換已棄用的分數形式(-XX:InitialRAMFraction、-XX:maxmRamFraction和-XX:MinRAMFraction)。

 

 

總結

CPU

  • java5/6/7/8u131以前:手動設置jvm相關的選項,如:
    • ParallelGCThreads
    • ConcGCThreads
    • G1ConcRefinementThreads
    • CICompilerCount / CICompilerCountPerCPU
  • java8u131+ 和 java9+
    • java 8u131+ 和 java 8u191以前:--cpuset-cpus
    • java 8u191+: UseContainerSupport默認開啟
  • java 10+:
    • 使用最新版就好了,UseContainerSupport默認開啟

Memory

  • java5/6/7/8u131以前:務必設置內存選項
  • java8u131+ 和 java9+
    • java 8u131+ 和 java 8u191以前:-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
    • java 8u191+: UseContainerSupport默認開啟
  • java10+
    • 使用最新版就好了,UseContainerSupport默認開啟

 

 

參考資料:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM