[转帖]Java 容器化的历史坑(史坑) - 资源限制篇

java,容器,历史,资源,限制 · 浏览次数 : 0

小编点评

**问题:** 如何设置 Linux cgroup share(cpu request) 值以增加 availableProcessors() 的线程数? **答案:** 1. **修改 POD CPU request 为忙时使用量:** - 修改 POD CPU request 为 `request 大大 小于 limit`。 - 此方法可能不可控,因为它会影响所有默认使用 `availableProcessors()` 的地方。 2. **使用期默认打开的PreferContainerQuotaForCPUCount参数:** - 修改 `GC thread数`、`Netty 线程数`等设置以使用期默认打开的 `PreferContainerQuotaForCPUCount` 参数。 - 此方法也可以影响所有默认使用 `availableProcessors()` 的地方。 3. **显式指定 availableProcessors() 的线程数:** - 修改 `GC thread数`、`Netty 线程数`等设置以显式指定 CPU 数。 - 此方法可以避免使用 `availableProcessors()` 的其他影响。 4. **升级 JDK11:** - JDK11 支持 `-XX:ActiveProcessorCount=count` 参数,其中 `count` 是可用核心数量。 - 此参数可以用于定制 `availableProcessors()` 的线程数。

正文

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

-XX:ActiveProcessorCount=$POD_CPU_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 的配置:

1
2
3
4
5
    resources:
      limits:
        cpu: "16"
      requests:
        cpu: "2"

进入 container:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ 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 :

1
2
3
4
5
    resources:
      limits:
        cpu: "16"
      requests:
        cpu: "4"

进入 container:

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

可见,java 得到 cpu 数,来源于 容器配置的 cpu request 。

availableProcessors() 的影响

再看看 availableProcessors() 的影响。-XX:+PrintFlagsFinal 的作用是在 jvm 启动时打印计算后的默认配置。

1
2
3
4
5
# 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数。所以,只要:

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

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

总结

很明显,这是个应该早几年就写的 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/questions/22741859/deceive-the-jvm-about-the-number-of-available-cores-on-linu

1
2
3
4
5
6
7
#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:

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

Then run Java as follows:

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

方法1、2比较通用,对 JNI 等非 java 生态的同样有效,但实现需要了解一些 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

与[转帖]Java 容器化的历史坑(史坑) - 资源限制篇相似的内容:

[转帖]Java 容器化的历史坑(史坑) - 资源限制篇

https://blog.mygraphql.com/zh/posts/cloud/containerize/java-containerize/java-containerize-resource-limit/ -XX:ActiveProcessorCount=$POD_CPU_LIMIT 由来

[转帖]讨论在 Linux Control Groups 中运行 Java 应用程序的暂停问题原创

https://heapdump.cn/article/1930426 说明 本篇原文来自 LinkedIn 的 Zhenyun Zhuang,原文:Application Pauses When Running JVM Inside Linux Control Groups[1],在容器化的进程中

[转帖]把大象装入货柜里——Java容器内存拆解

https://blog.mygraphql.com/zh/notes/java/native-mem/java-native-mem-case/ 介绍 测试环境 配置容量 POD 容量配置 JVM 容量配置 神秘的 MaxDirectMemorySize 默认值 maxThreadCount 最大

[转帖]限制容器中的jvm

在rancher中部署完java应用之后,需要对java程序的jvm进行设置,这个非常重要,不然可能会引起比较严重的后果:容器无限制的重启或者主机的内存被耗尽。 在开始之前,先来看一个问题: 在容器中跑了一个java应用,那怎么来限制这个jvm的memory呢? 按照传统的思路对memory进行限制

[转帖]一次操作系统报错OutOfMemory Error的处理记录

在启动公司内嵌的tomcat容器时出现报错, 如下: # There is insufficient memory for the Java Runtime Environment to continue.# Native memory allocation (malloc) failed to a

[转帖]Cat导致内存不足原因分析

背景 线上几亿的数据在回刷的时候容器服务会出现OOM而重启,导致任务中断 内存泄露分析 jmap -histo pid 找出了有几十亿的java.lang.StackTraceElement对象,找不到被谁引用了 jmap -dump:format=b,file=heapdump.hprof pid

[转帖]docker容器自动重启,看完这篇彻底明白了

一. JVM内存区域的划分 1.1 java虚拟机运行时数据区 java虚拟机运行时数据区分布图: JVM栈(Java Virtual Machine Stacks): Java中一个线程就会相应有一个线程栈与之对应,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈,因此栈存储的信息都是跟当

[转帖]总结:Servlet

一、背景 开发了很久的web服务,使用了很多web框架,都忘记web技术最原始的模样了,今天来回顾下。 二、Servlet是什么? Servlet是sun公司提供的一门用于开发动态web资源的技术。我们普通的Java类实现了Servlet接口后,可将我们的服务部署在Web容器中,这样我们的服务就可以

[转帖]Springboot容器化镜像设置堆内存大小

参考资料:Best Practices: Java Memory Arguments for Containers - DZone Java 有三种方式设置最大堆内存大小: 1. -Xmx 2. -XX:MaxRAMFraction, -XX:MinRAMFraction 3.-XX:MaxRAMP

[转帖] shell管道咋堵住了

https://www.cnblogs.com/codelogs/p/16060378.html 背景# 起因是这样的,我们想开发一个小脚本,当cpu使用率过高时,使用jstack将java的线程栈保存下来,以便后面分析。 获取cpu使用率# 获取cpu使用率是比较容易的,使用vmstat就可以了,