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

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 随着容器技术的成熟,越来越多的企业客户在企业中选择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搭建和管理企业级网站应用
目录
相关文章
|
12天前
|
Java 程序员
JAVA程序员的进阶之路:掌握URL与URLConnection,轻松玩转网络资源!
在Java编程中,网络资源的获取与处理至关重要。本文介绍了如何使用URL与URLConnection高效、准确地获取网络资源。首先,通过`java.net.URL`类定位网络资源;其次,利用`URLConnection`类实现资源的读取与写入。文章还提供了最佳实践,包括异常处理、连接池、超时设置和请求头与响应头的合理配置,帮助Java程序员提升技能,应对复杂网络编程场景。
37 9
|
9天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
2天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
12 3
|
12天前
|
Java 开发者
JAVA高手必备:URL与URLConnection,解锁网络资源的终极秘籍!
在Java网络编程中,URL和URLConnection是两大关键技术,能够帮助开发者轻松处理网络资源。本文通过两个案例,深入解析了如何使用URL和URLConnection从网站抓取数据和发送POST请求上传数据,助力你成为真正的JAVA高手。
31 11
|
12天前
|
SQL 监控 Java
技术前沿:Java连接池技术的最新发展与应用
本文探讨了Java连接池技术的最新发展与应用,包括高性能与低延迟、智能化管理和监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,为开发者提供了一份详尽的技术指南。
22 7
|
10天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
25 3
|
10天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
25 2
|
11天前
|
缓存 Java 数据库连接
Hibernate:Java持久层框架的高效应用
通过上述步骤,可以在Java项目中高效应用Hibernate框架,实现对关系数据库的透明持久化管理。Hibernate提供的强大功能和灵活配置,使得开发者能够专注于业务逻辑的实现,而不必过多关注底层数据库操作。
9 1
|
15天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。
|
15天前
|
SQL 监控 Java
Java性能优化:提升应用效率与响应速度的全面指南
【10月更文挑战第21】Java性能优化:提升应用效率与响应速度的全面指南

相关产品

  • 容器服务Kubernetes版
  • 下一篇
    无影云桌面