前言
對於那些在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 |
cat /proc/meminfo |
容器:
1 |
docker run -it --rm alpine cat /proc/meminfo |
那么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限制。
- 順着 JDK 8 Update Release Notes 查看,JDK 8u191 提供了更完善的 docker 容器支持。
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默認開啟
- java 8u131+ 和 java 8u191以前:
- java10+
- 使用最新版就好了,UseContainerSupport默認開啟
參考資料:
- https://www.liangzl.com/get-article-detail-153252.html
- jdk8u131:https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory-limits
- jdk8u191:https://www.oracle.com/technetwork/java/javase/8u191-relnotes-5032181.html 、https://cloud.tencent.com/developer/article/1438099
- jdk 10:https://yq.aliyun.com/articles/576200
- docker:https://docs.docker.com/config/containers/resource_constraints/#cpu
- https://www.cnblogs.com/cheyunhua/p/10208059.html
- https://www.cnblogs.com/duanxz/p/10248762.html
- 詳細:容器(docker)中運行java需關注的幾個小問題
- 生產:https://juejin.im/post/5cd96e0be51d453b5854b8b9