Kubernetes之路 1 - Java应用资源限制的迷思

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 随着容器技术的成熟,越来越多的企业客户在企业中选择Docker和Kubernetes作为应用平台的基础。然而在实践过程中,还会遇到很多具体问题。本文分析并解决了Java应用在容器使用过程中关于Heap大小设置的一个常见问题。

image

本系列文章记录了企业客户在应用Kubernetes时的一些常见问题

随着容器技术的成熟,越来越多的企业客户在企业中选择Docker和Kubernetes作为应用平台的基础。然而在实践过程中,还会遇到很多具体问题。本系列文章会记录阿里云容器服务团队在支持客户中的一些心得体会和最佳实践。我们也欢迎您通过邮件和钉钉群和我们联系,分享您的思路和遇到的问题。

在对Java应用容器化部署的过程中,有些同学反映:自己设置了容器的资源限制,但是Java应用容器在运行中还是会莫名奇妙地被OOM Killer干掉。

这背后一个非常常见的原因是:没有正确设置容器的资源限制以及对应的JVM的堆空间大小。

我们拿一个tomcat应用为例,其实例代码和Kubernetes部署文件可以从Github中获得。

git clone https://github.com/denverdino/system-info
cd system-info`

下面是一个Kubernetes的Pod的定义描述:

  1. Pod中的app是一个初始化容器,负责把一个JSP应用拷贝到 tomcat 容器的 “webapps”目录下。: 镜像中JSP应用index.jsp用于显示JVM和系统资源信息。
  2. tomcat 容器会保持运行,而且我们限制了容器最大的内存用量为256MB内存。
apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  initContainers:
  - image: registry.cn-hangzhou.aliyuncs.com/denverdino/system-info
    name: app
    imagePullPolicy: IfNotPresent
    command:
      - "cp"
      - "-r"
      - "/system-info"
      - "/app"
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: tomcat:9-jre8
    name: tomcat
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - mountPath: /usr/local/tomcat/webapps
      name: app-volume
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "256Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: "500m"
  volumes:
  - name: app-volume
    emptyDir: {}

我们执行如下命令来部署、测试应用

$ kubectl create -f test.yaml
pod "test" created
$ kubectl get pods test
NAME      READY     STATUS    RESTARTS   AGE
test      1/1       Running   0          28s
$ kubectl exec test curl http://localhost:8080/system-info/
...

我们可以看到HTML格式的系统CPU/Memory等信息,我们也可以用 html2text 命令将其转化成为文本格式。

注意:本文是在一个 2C 4G的节点上进行的测试,在不同环境中测试输出的结果会有所不同

$ kubectl exec test curl http://localhost:8080/system-info/ | html2text

Java version     Oracle Corporation 1.8.0_162
Operating system Linux 4.9.64
Server           Apache Tomcat/9.0.6
Memory           Used 29 of 57 MB, Max 878 MB
Physica Memory   3951 MB
CPU Cores        2
                                          **** Memory MXBean ****
Heap Memory Usage     init = 65011712(63488K) used = 19873704(19407K) committed
                      = 65536000(64000K) max = 921174016(899584K)
Non-Heap Memory Usage init = 2555904(2496K) used = 32944912(32172K) committed =
                      33882112(33088K) max = -1(-1K)

我们可以发现,容器中看到的系统内存是 3951MB,而JVM Heap Size最大是 878MB。纳尼?!我们不是设置容器资源的容量为256MB了吗?如果这样,当应用内存的用量超出了256MB,JVM还没对其进行GC,而JVM进程就会被系统直接OOM干掉了。

问题的根源在于:

  • 对于JVM而言,如果没有设置Heap Size,就会按照宿主机环境的内存大小缺省设置自己的最大堆大小。
  • Docker容器利用CGroup对进程使用的资源进行限制,而在容器中的JVM依然会利用宿主机环境的内存大小和CPU核数进行缺省设置,这导致了JVM Heap的错误计算。

类似,JVM缺省的GC、JIT编译线程数量取决于宿主机CPU核数。如果我们在一个节点上运行多个Java应用,即使我们设置了CPU的限制,应用之间依然有可能因为GC线程抢占切换,导致应用性能收到影响。

了解了问题的根源,我们就可以非常简单地解决问题了

解决思路

开启CGroup资源感知

Java社区也关注到这个问题,并在JavaSE8u131+和JDK9 支持了对容器资源限制的自动感知能力 https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory-limits

其用法就是添加如下参数

java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap …

我们在上文示例的tomcat容器添加环境变量 “JAVA_OPTS”参数

apiVersion: v1
kind: Pod
metadata:
  name: cgrouptest
spec:
  initContainers:
  - image: registry.cn-hangzhou.aliyuncs.com/denverdino/system-info
    name: app
    imagePullPolicy: IfNotPresent
    command:
      - "cp"
      - "-r"
      - "/system-info"
      - "/app"
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: tomcat:9-jre8
    name: tomcat
    imagePullPolicy: IfNotPresent
    env:
    - name: JAVA_OPTS
      value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
    volumeMounts:
    - mountPath: /usr/local/tomcat/webapps
      name: app-volume
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "256Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: "500m"
  volumes:
  - name: app-volume
    emptyDir: {}

我们部署一个新的Pod,并重复相应的测试

$ kubectl create -f cgroup_test.yaml
pod "cgrouptest" created

$ kubectl exec cgrouptest curl http://localhost:8080/system-info/ | html2txt
Java version     Oracle Corporation 1.8.0_162
Operating system Linux 4.9.64
Server           Apache Tomcat/9.0.6
Memory           Used 23 of 44 MB, Max 112 MB
Physica Memory   3951 MB
CPU Cores        2
                                          **** Memory MXBean ****
Heap Memory Usage     init = 8388608(8192K) used = 25280928(24688K) committed =
                      46661632(45568K) max = 117440512(114688K)
Non-Heap Memory Usage init = 2555904(2496K) used = 31970840(31221K) committed =
                      32768000(32000K) max = -1(-1K)

我们看到JVM最大的Heap大小变成了112MB,这很不错,这样就能保证我们的应用不会轻易被OOM了。随后问题又来了,为什么我们设置了容器最大内存限制是256MB,而JVM只给Heap设置了112MB的最大值呢?

这就涉及到JVM的内存管理的细节了,JVM中的内存消耗包含Heap和Non-Heap两类;类似Class的元信息,JIT编译过的代码,线程堆栈(thread stack),GC需要的内存空间等都属于Non-Heap内存,所以JVM还会根据CGroup的资源限制预留出部分内存给Non Heap,来保障系统的稳定。(在上面的示例中我们可以看到,tomcat启动后Non Heap占用了近32MB的内存)

Java对Docker的支持演进

在JDK 8u191和JDK 10之后,社区对JVM在容器中运行做了进一步的优化和增强。JVM可以自动感知Docker容器内部的CPU和内存资源限制。Java进程可用CPU核数由cpu sets, cpu shares 和 cpu quotas等参数计算而来。

设置内存

$ echo "Runtime.getRuntime().maxMemory()" | docker run -i -m512M openjdk:10-jdk
Dec 25, 2018 5:34:39 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().maxMemory()
$1 ==> 129761280

设置 CPU Set

$ echo "Runtime.getRuntime().availableProcessors()" | docker run -i --cpuset-cpus="1,2,3" openjdk:10-jdk
Dec 25, 2018 5:36:18 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 3

设置 CPU Shares

$ echo "Runtime.getRuntime().availableProcessors()" | docker run -i --cpu-shares=2048 openjdk:10-jdk
Dec 25, 2018 5:40:32 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 2

设置CPU Period与Quota: —cpus (或 —cpu-period 和 —cpu-quota, cpus = quota/period) link

$ echo "Runtime.getRuntime().availableProcessors()" | docker run -i --cpus=2 openjdk:10-jdk
Dec 25, 2018 5:37:12 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 2

如需禁止自动资源计算,可以指明如下参数

-XX:-UseContainerSupport

如果需要指明JVM可用CPU核数,可以指明如下参数

-XX:ActiveProcessorCount=count

参考信息

容器内部感知CGroup资源限制

如果无法利用JDK 8/9的新特性,比如还在使用JDK6的老应用,我们还可以在容器内部利用脚本来获取容器的CGroup资源限制,并通过设置JVM的Heap大小。

Docker1.7开始将容器cgroup信息挂载到容器中,所以应用可以从 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件获取内存、 CPU等设置,在容器的应用启动命令中根据Cgroup配置正确的资源设置 -Xmx, -XX:ParallelGCThreads等参数

https://yq.aliyun.com/articles/18037 一文中已经有相应的示例和代码,本文不再赘述

总结

本文分析了Java应用在容器使用中一个常见Heap设置的问题。容器与虚拟机不同,其资源限制通过CGroup来实现。而容器内部进程如果不感知CGroup的限制,就进行内存、CPU分配可能导致资源冲突和问题。

我们可以非常简单地利用JVM的新特性和自定义脚本来正确设置资源限制。这个可以解决绝大多数资源限制的问题。

关于容器应用中资源限制还有一类问题是,一些比较老的监控工具或者free/top等系统命令,在容器中运行时依然会获取到宿主机的CPU和内存,这导致了一些监控工具在容器中运行时无法正常计算资源消耗。社区中常见的做法是利用 lxcfs 来让容器在资源可见性的行为和虚机保持一致,后续文章会介绍其在Kubernetes上的使用方案。

阿里云Kubernetes服务 全球首批通过Kubernetes一致性认证,简化了Kubernetes集群生命周期管理,内置了与阿里云产品集成,也将进一步简化Kubernetes的开发者体验,帮助用户关注云端应用价值创新。

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
1天前
|
JavaScript 前端开发 Java
Java 8 新特性详解及应用示例
Java 8 新特性详解及应用示例
|
5天前
|
缓存 负载均衡 Dubbo
Dubbo技术深度解析及其在Java中的实战应用
Dubbo是一款由阿里巴巴开源的高性能、轻量级的Java分布式服务框架,它致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
24 6
|
1天前
|
Java API
Java中的Lambda表达式及其应用
本文将深入探讨Java中的Lambda表达式,通过简洁易懂的语言和示例代码,帮助读者理解Lambda表达式的定义、优势以及在实际开发中的应用。同时,我们将解析一些常见的使用场景,并展示如何利用Lambda表达式简化代码,提高编程效率。
8 2
|
8天前
|
Kubernetes Java Maven
揭秘无服务器革命:Quarkus如何让Java应用在云端“零”负担起飞?
本文介绍如何使用Quarkus从零开始开发无服务器应用,通过示例代码和详细步骤引导读者掌握这一技术。无服务器架构让开发者无需管理服务器,具有自动扩展和成本效益等优势。Quarkus作为Kubernetes Native Java框架,优化了Java应用的启动速度和内存使用,适合无服务器环境。文章涵盖环境搭建、项目创建及部署全流程,并介绍了Quarkus的扩展性和监控工具,助力高效开发与应用性能提升。
22 9
|
5天前
|
Java 开发者
Java中的多线程基础与应用
【9月更文挑战第22天】在Java的世界中,多线程是一块基石,它支撑着现代并发编程的大厦。本文将深入浅出地介绍Java中多线程的基本概念、创建方法以及常见的应用场景,帮助读者理解并掌握这一核心技术。
|
1天前
|
Java 开发者 UED
Java中的异常处理机制:理解与应用
本文深入探讨Java的异常处理机制,通过实例解析如何有效使用try-catch-finally块、throws关键字及自定义异常,以提升代码的健壮性和可维护性。我们将从基础概念入手,逐步过渡到高级应用,为Java开发者提供全面指导。
|
3天前
|
分布式计算 Java API
深入解析Java中的Lambda表达式及其应用
本文将深入探讨Java中Lambda表达式的定义、优势及其在实际编程中的应用。通过具体示例,帮助读者更好地理解和使用这一强大的编程工具。
|
9天前
|
Kubernetes Cloud Native Java
探索未来编程新纪元:Quarkus带你秒建高性能Kubernetes原生Java应用,云原生时代的技术狂欢!
Quarkus 是专为 Kubernetes 设计的全栈云原生 Java 框架,凭借其轻量级、快速启动及高效执行特性,在 Java 社区脱颖而出。通过编译时优化与原生镜像支持,Quarkus 提升了应用性能,同时保持了 Java 的熟悉度与灵活性。本文将指导你从创建项目、编写 REST 控制器到构建与部署 Kubernetes 原生镜像的全过程,让你快速上手 Quarkus,体验高效开发与部署的乐趣。
13 0
|
JSON Kubernetes 安全
使用 Java 操作 Kubernetes API
使用 Java 操作 Kubernetes API
3883 0
使用 Java 操作 Kubernetes API
|
3天前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理

相关产品

  • 容器服务Kubernetes版