Java 容器化的歷史坑 - 資源限制篇

原文:https://blog.mygraphql.com/zh/posts/cloud/containerize/java-containerize/java-containerize-resource-limit/

由來

時間回到 2017 年,老東家要上 Kubernetes 了,有幸參與和學(xué)習(xí)(主要是學(xué)習(xí))。當時遇到的一了所有 Java 容器化者都遇到的坑:JDK8 不為容器化設(shè)計綜合癥。最簡單的例子是Runtime.getRuntime().availableProcessors()返回了主機的 CPU 數(shù),而非期望的容器自身的cpu share/quota,或說 k8s 的 cpu request/limit

時間到了 2021 年,一切本該云淡風(fēng)輕(雖然工資依然追不上CPI和房價)。雖然我在的項目還是使用 JDK8,但好歹也是 jdk 1.8.0_261 了,已經(jīng) backport 了很多容器化的特性到這個版本了。最近在做項目的性能優(yōu)化,在 Istio 的泥潭苦苦掙扎中。

突然前方同學(xué)傳來喜訊: 把 POD 的 cpu request 由 2 變 4 后,性能有明顯的優(yōu)化。我在羨慕嫉妒??的同時,好奇地研究了一下原理。

原理

直線思維邏輯

Kubernetes 使用 cgroup 進行資源限制:

  • cpu request 對應(yīng)于 cgroup 的 share 指標。在主機CPU不足,各容器需要爭搶CPU情況下,指定各容器的優(yōu)先級(數(shù)字大優(yōu)先,比例化)
  • cpu limit 對應(yīng)于 cgroup 的 limit 指標。這是硬限制,不能超。超了就卡慢線程。

那么問題來了,測試環(huán)境主機CPU 資源充足,不存在各容器需要爭搶CPU 的情況。那么,為何調(diào)大 cpu request后,會明顯優(yōu)化性能?

可能性:

  1. 直線思維:Linux CFS Scheduler(任務(wù)調(diào)度器)實現(xiàn)不太好,在非各容器需要爭搶CPU情況下,cpu request 仍然影響了調(diào)度
  2. 懷疑論者:新版本的 jdk8 只是依據(jù) cpu request 來自動計算各默認配置,如各線程池。

作為一個只懂 java 的程序員,我關(guān)注后者。

求證

作為只懂寫代碼的程序員,沒什么比運行的程序更能幫你說話了。起碼,機器不會因為你和他關(guān)系好,或等著你給他通點氣,或填個KPI,就跑你的程序快一點(不要和我說linux taskset),更不會生成一個和關(guān)系有關(guān)系的小報告。

回來吧,先看看 POD 的配置:

    resources:
      limits:
        cpu: "16"
      requests:
        cpu: "2"

進入 container:

$ cd /tmp
$ cat <<EOF > /tmp/Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println("Runtime.getRuntime().availableProcessors() = " +
                Runtime.getRuntime().availableProcessors());
    }
}
EOF

$ javac Main.java
$ java -cp . Main
Runtime.getRuntime().availableProcessors() = 2

加點CPU request :

    resources:
      limits:
        cpu: "16"
      requests:
        cpu: "4"

進入 container:

$ cd /tmp
$ java -cp . Main
Runtime.getRuntime().availableProcessors() = 4

可見,java 得到 cpu 數(shù),來源于 容器配置的 cpu request 。

availableProcessors() 的影響

再看看 availableProcessors() 的影響。-XX:+PrintFlagsFinal 的作用是在 jvm 啟動時打印計算后的默認配置。

# Request cpu=1 時
$ java -XX:+PrintFlagsFinal -cp . Main > req1.txt

# Request cpu=4 時
$ java -XX:+PrintFlagsFinal -cp . Main > req4.txt
$ diff req1.txt req4.txt

2c2
<      intx ActiveProcessorCount                      = -1                                  {product}
---
>      intx ActiveProcessorCount                     := 4                                   {product}
59c59
<      intx CICompilerCount                          := 2                                   {product}
---
>      intx CICompilerCount                          := 3                                   {product}
305c305
<     uintx MarkSweepDeadRatio                        = 5                                   {product}
---
>     uintx MarkSweepDeadRatio                        = 1                                   {product}
312c312
<     uintx MaxHeapFreeRatio                          = 70                                  {manageable}
---
>     uintx MaxHeapFreeRatio                          = 100                                 {manageable}
325c325
<     uintx MaxNewSize                               := 178913280                           {product}
---
>     uintx MaxNewSize                               := 178782208                           {product}
336,337c336,337
<     uintx MinHeapDeltaBytes                        := 196608                              {product}
<     uintx MinHeapFreeRatio                          = 40                                  {manageable}
---
>     uintx MinHeapDeltaBytes                        := 524288                              {product}
>     uintx MinHeapFreeRatio                          = 0                                   {manageable}
360c360
<     uintx NewSize                                  := 11141120                            {product}
---
>     uintx NewSize                                  := 11010048                            {product}
371c371
<     uintx OldSize                                  := 22413312                            {product}
---
>     uintx OldSize                                  := 22544384                            {product}
389c389
<     uintx ParallelGCThreads                         = 0                                   {product}
---
>     uintx ParallelGCThreads                         = 4                                   {product}
690,691c690,691
<      bool UseParallelGC                             = false                               {product}
<      bool UseParallelOldGC                          = false                               {product}
---
>      bool UseParallelGC                            := true                                {product}
>      bool UseParallelOldGC                          = true                                {product}
738c738
< Runtime.getRuntime().availableProcessors() = 1
---
> Runtime.getRuntime().availableProcessors() = 4

可見,availableProcessors() 不但影響了 jvm 的 GC 線程數(shù),JIT 線程數(shù),甚至是 GC算法。更大問題是一些 servlet container(如 Jetty)和 Netty 默認也會使用這個數(shù)字去配置他們的線程池。

反證

如果還是覺得Linux CFS Scheduler(任務(wù)調(diào)度器)在主機CPU過剩時,調(diào)度還是受到了 cgroup share(cpu request)影響 這個可能性需要排除。那么在POD拉起后,直接使用 linux 終端,去修改 cgroup 的 share 文件,增加一倍,再測試,就可以知道。對,反模式是排除問題的常用方法。但我沒做這個測試,因我不想太科學(xué)??凡事留一線。

填坑

填坑是程序員的天職,無論你喜不喜歡,無論這個坑是你挖的,還是前度留下的。這個坑有幾個填法:

  1. 修改 POD CPU request 為忙時使用量,即加大request,limit 不變
  2. 升級到 JDK11,使用期默認打開的PreferContainerQuotaForCPUCount參數(shù),即 availableProcessors() 返回 CPU limit 數(shù)。
  3. 所有默認使用availableProcessors() 的地方,修改為顯式指定,如GC線程數(shù),Netty 線程數(shù)……
  4. CPU request/limit 不變,即 request 大大 小于 limit。但顯式告訴 JVM 可以使用的 CPU 數(shù)。

國際習(xí)慣,我選用了 4。原因:

  • POD 如果配置了大的 request,相當于鎖定獨占了主機的資源。主機實際資源利用率一定降低。而這個 request 其實只是個忙時峰值需求,如啟動時的編譯,或電商的搶購。
  • 為所有默認使用availableProcessors() 的地方,修改為顯式指定。這個工作量大,對未來未知的使用到 availableProcessors() 的地方不可控。
  • 升級 JDK11,不是我等程序員能定的

明白了我能做什么后,就 Just do it 了。

話說,從 JDK 8u191后,支持了-XX:ActiveProcessorCount=count參數(shù),告訴JVM真正可用的CPU數(shù)。所以,只要:

java -XX:+PrintFlagsFinal -XX:ActiveProcessorCount=$POD_CPU_LIMIT -cp . Main
# 當然,如果覺得 $POD_CPU_LIMIT 太大,就自行調(diào)整吧

-XX:ActiveProcessorCount的說明見:https://www.oracle.com/java/technologies/javase/8u191-relnotes.html#JDK-8146115

總結(jié)

很明顯,這是個應(yīng)該早幾年就寫的 Blog。現(xiàn)在估計你家已經(jīng)不使用JDK8了。而一般直接到 JDK11 LTS 了?;蛘撸疚南胝f的是一種求證問題的方法和態(tài)度。它或者不能直接給你帶來什么好處,有時候,甚至很讓一些人討厭,影響你進升的大好前程。不過,一個行業(yè)如果要進步,還得依賴這種情懷。英文有個詞:Nerd。專門形容這種態(tài)度。


擴展閱讀

史前的修正 availableProcessors() 大法

在 JDK8 還沒為容器化設(shè)計前,大神們只能先自行解決了。方法兩種(層):

  1. mount bind 修改內(nèi)核層 cpu 數(shù)的 system file
  2. 重載 gun libc 的 sysconf 函數(shù)
  3. 在 Linux 的動態(tài) link .so 時重載 JVM_ActiveProcessorCount 函數(shù),定制后返回

方法3相對簡單。這里只說方法2:

參考: https://stackoverflow.com/questions/22741859/deceive-the-jvm-about-the-number-of-available-cores-on-linu

#include <stdlib.h>
#include <unistd.h>

int JVM_ActiveProcessorCount(void) {
    char* val = getenv("_NUM_CPUS");
    return val != NULL ? atoi(val) : sysconf(_SC_NPROCESSORS_ONLN);
}

First, make a shared library of this:

gcc -O3 -fPIC -shared -Wl,-soname,libnumcpus.so -o libnumcpus.so numcpus.c

Then run Java as follows:

$ LD_PRELOAD=/path/to/libnumcpus.so _NUM_CPUS=2 java AvailableProcessors

方法1、2比較通用,對 JNI 等非 java 生態(tài)的同樣有效,但實現(xiàn)需要了解一些 Linux??梢詤⒖迹?https://geek-tips.imtqy.com/articles/493531/index.html、https://github.com/jvm-profiling-tools/async-profiler/issues/176

參考

https://christopher-batey.medium.com/cpu-considerations-for-java-applications-running-in-docker-and-kubernetes-7925865235b7

https://www.batey.info/docker-jvm-k8s.html

https://mucahit.io/2020/01/27/finding-ideal-jvm-thread-pool-size-with-kubernetes-and-docker/

https://blog.gilliard.lol/2018/01/10/Java-in-containers-jdk10.html

https://cloud.google.com/run/docs/tips/java

https://stackoverflow.com/questions/59882464/does-javas-activeprocessorcount-limit-the-number-of-cpus-the-jvm-can-use

https://www.oracle.com/java/technologies/javase/8u191-relnotes.html#JDK-8146115

https://stackoverflow.com/questions/64489101/optimal-number-of-gc-threads-for-a-single-cpu-machine

https://bugs.openjdk.java.net/browse/JDK-8264136?focusedCommentId=14409876&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel

https://programmer.group/5ce18f3f02631.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容