关于java:Java-容器化的历史坑史坑-资源限制篇

7次阅读

共计 4908 个字符,预计需要花费 13 分钟才能阅读完成。

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

由来

工夫回到 2017 年,老东家要上 Kubernetes 了,有幸参加和学习(次要是学习)。过后遇到的一了所有 Java 容器化者都遇到的坑:JDK8 不为容器化设计综合症。最简略的例子是 Runtime.getRuntime().availableProcessors() 返回了主机的 CPU 数,而非冀望的容器本身的cpu share/quota,或说 k8s 的 cpu request/limit

工夫到了 2021 年,所有本该云淡风轻(尽管工资仍然追不上 CPI 和房价)。尽管我在的我的项目还是应用 JDK8,但好歹也是 jdk 1.8.0_261 了,曾经 backport 了很多容器化的个性到这个版本了。最近在做我的项目的性能优化,在 Istio 的泥潭苦苦挣扎中。

忽然后方同学传来喜讯:把 POD 的 cpu request 由 2 变 4 后,性能有显著的优化。我在艳羡嫉妒😋的同时,好奇地钻研了一下原理。

原理

直线思维逻辑

Kubernetes 应用 cgroup 进行资源限度:

  • cpu request 对应于 cgroup 的 share 指标。在主机 CPU 有余,各容器须要争抢 CPU 状况下,指定各容器的优先级(数字大优先,比例化)
  • cpu limit 对应于 cgroup 的 limit 指标。这是硬限度,不能超。超了就卡慢线程。

那么问题来了,测试环境主机 CPU 资源短缺,不存在 各容器须要争抢 CPU 的状况。那么,为何调大 cpu request后,会显著优化性能?

可能性:

  1. 直线思维:Linux CFS Scheduler(任务调度器)实现不太好,在非 各容器须要争抢 CPU状况下,cpu request 依然影响了调度
  2. 怀疑论者:新版本的 jdk8 只是根据 cpu request 来主动计算各默认配置,如各线程池。

作为一个只懂 java 的程序员,我关注后者。

求证

作为只懂写代码的程序员,没什么比运行的程序更能帮你谈话了。起码,机器不会因为你和他关系好,或等着你给他通点气,或填个 KPI,就跑你的程序快一点(不要和我说linux taskset),更不会生成一个和关系有关系的小报告。

回来吧,先看看 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 数,来源于 容器配置的 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 线程数,JIT 线程数,甚至是 GC 算法。更大问题是一些 servlet container(如 Jetty)和 Netty 默认也会应用这个数字去配置他们的线程池。

反证

如果还是感觉 Linux CFS Scheduler(任务调度器)在主机 CPU 过剩时,调度还是受到了 cgroup share(cpu request) 影响 这个可能性须要排除。那么在 POD 拉起后,间接应用 linux 终端,去批改 cgroup 的 share 文件,增加一倍,再测试,就能够晓得。对,反模式是排除问题的罕用办法。但我没做这个测试,因我不想太迷信🙃凡事留一线。

填坑

填坑是程序员的天职,无论你喜不喜欢,无论这个坑是你挖的,还是前度留下的。这个坑有几个填法:

  1. 批改 POD CPU request 为忙时使用量,即加大 request,limit 不变
  2. 降级到 JDK11,使用期默认关上的 PreferContainerQuotaForCPUCount 参数,即 availableProcessors() 返回 CPU limit 数。
  3. 所有默认应用availableProcessors() 的中央,批改为显式指定,如 GC 线程数,Netty 线程数……
  4. CPU request/limit 不变,即 request 大大 小于 limit。但显式通知 JVM 能够应用的 CPU 数。

国内习惯,我选用了 4。起因:

  • POD 如果配置了大的 request,相当于锁定独占了主机的资源。主机理论资源利用率肯定升高。而这个 request 其实只是个忙时峰值需要,如启动时的编译,或电商的抢购。
  • 为所有默认应用availableProcessors() 的中央,批改为显式指定。这个工作量大,对将来未知的应用到 availableProcessors() 的中央不可控。
  • 降级 JDK11,不是我等程序员能定的

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

话说,从 JDK 8u191后,反对了 -XX:ActiveProcessorCount=count 参数,通知 JVM 真正可用的 CPU 数。所以,只有:

java -XX:+PrintFlagsFinal -XX:ActiveProcessorCount=$POD_CPU_LIMIT -cp . Main
# 当然,如果感觉 $POD_CPU_LIMIT 太大,就自行调整吧

-XX:ActiveProcessorCount的阐明见:https://www.oracle.com/java/t…

总结

很显著,这是个应该早几年就写的 Blog。当初预计你家曾经不应用 JDK8 了。而个别间接到 JDK11 LTS 了。或者,本文想说的是一种求证问题的办法和态度。它或者不能间接给你带来什么益处,有时候,甚至很让一些人厌恶,影响你进升的大好前程。不过,一个行业如果要提高,还得依赖这种情怀。英文有个词:Nerd。专门形容这种态度。


扩大浏览

史前的修改 availableProcessors() 大法

在 JDK8 还没为容器化设计前,大神们只能先自行解决了。办法两种(层):

  1. mount bind 批改内核层 cpu 数的 system file
  2. 重载 gun libc 的 sysconf 函数
  3. 在 Linux 的动静 link .so 时重载 JVM_ActiveProcessorCount 函数,定制后返回

办法 3 绝对简略。这里只说办法 2:

参考:https://stackoverflow.com/que…

#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 生态的同样无效,但实现须要理解一些 Linux。能够参考:https://geek-tips.imtqy.com/a…、https://github.com/jvm-profil…

参考

https://christopher-batey.med…

https://www.batey.info/docker…

https://mucahit.io/2020/01/27…

https://blog.gilliard.lol/201…

https://cloud.google.com/run/…

https://stackoverflow.com/que…

https://www.oracle.com/java/t…

https://stackoverflow.com/que…

https://bugs.openjdk.java.net…

https://programmer.group/5ce1…

正文完
 0