Java 应用程序在 Kubernetes 上棘手的内存管理(上)

简介: Java 应用程序在 Kubernetes 上棘手的内存管理

引言

如何结合使用 JVM Heap 堆和 Kubernetes 内存的 requests 和 limits 并远离麻烦。

在容器环境中运行 Java 应用程序需要了解两者 —— JVM 内存机制和 Kubernetes 内存管理。这两个环境一起工作会产生一个稳定的应用程序,但是,错误配置最多可能导致基础设施超支,最坏情况下可能会导致应用程序不稳定或崩溃。我们将首先仔细研究 JVM 内存的工作原理,然后我们将转向 Kubernetes,最后,我们将把这两个概念放在一起。

JVM 内存模型简介

JVM 内存管理是一种高度复杂的机制,多年来通过连续发布不断改进,是 JVM 平台的优势之一。对于本文,我们将只介绍对本主题有用的基础知识。在较高的层次上,JVM 内存由两个空间组成 —— Heap 和 Metaspace。

JVM 内存模型

非 Heap 内存

JVM 使用许多内存区域。最值得注意的是 Metaspace。Metaspace 有几个功能。它主要用作方法区,其中存储应用程序的类结构和方法定义,包括标准库。内存池和常量池用于不可变对象,例如字符串,以及类常量。堆栈区域是用于线程执行的后进先出结构,存储原语和对传递给函数的对象的引用。根据 JVM 实现和版本,此空间用途的一些细节可能会有所不同。

我喜欢将 Metaspace 空间视为一个管理区域。这个空间的大小可以从几 MB 到几百 MB 不等,具体取决于代码库及其依赖项的大小,并且在应用程序的整个生命周期中几乎保持不变。默认情况下,此空间未绑定并会根据应用程序需要进行扩展。

Metaspace 是在 Java 8 中引入的,取代了 Permanent Generation,后者存在垃圾回收问题。

其他一些值得一提的非堆内存区域是代码缓存、线程、垃圾回收。更多关于非堆内存参考这里。

Heap 堆内存

如果 Metaspace 是管理空间,那么 Heap 就是操作空间。这里存放着所有的实例对象,并且垃圾回收机制在这里最为活跃。该内存的大小因应用程序而异,取决于工作负载的大小 —— 应用程序需要满足单个请求和流量特征所需的内存。大型应用程序通常具有以GB为单位的堆大小。

我们将使用一个示例应用程序用于探索内存机制。源代码在此处。

这个演示应用程序模拟了一个真实世界的场景,在该场景中,为传入请求提供服务的系统会在堆上累积对象,并在请求完成后成为垃圾回收的候选对象。该程序的核心是一个无限循环,通过将大型对象添加到列表并定期清除列表来创建堆上的大型对象。

val list = mutableListOf<ByteArray>()
generateSequence(0) { it + 1 }.forEach {
    if (it % (HEAP_TO_FILL / INCREMENTS_IN_MB) == 0) list.clear()
    list.add(ByteArray(INCREMENTS_IN_MB * BYTES_TO_MB))
}

以下是应用程序的输出。在预设间隔(本例中为350MB堆大小)内,状态会被清除。重要的是要理解,清除状态并不会清空堆 - 这是垃圾收集器内部实现的决定何时将对象从内存中驱逐出去。让我们使用几个堆设置来运行此应用程序,以查看它们对JVM行为的影响。

首先,我们将使用 4 GB 的最大堆大小(由 -Xmx 标志控制)。以下是应用程序的输出。在预设间隔(本例中为350MB堆大小)内,状态会被清除。重要的是要理解,清除状态并不会清空堆 - 这是垃圾收集器内部实现的决定何时将对象从内存中驱逐出去。让我们使用几个堆设置来运行此应用程序,以查看它们对JVM行为的影响。

首先,我们将使用 4 GB 的最大堆大小(由 -Xmx 标志控制)。

~ java -jar -Xmx4G app/build/libs/app.jar
INFO           Used          Free            Total
INFO       14.00 MB      36.00 MB       50.00 MB
INFO       66.00 MB      16.00 MB       82.00 MB
INFO      118.00 MB     436.00 MB      554.00 MB
INFO      171.00 MB     383.00 MB      554.00 MB
INFO      223.00 MB     331.00 MB      554.00 MB
INFO      274.00 MB     280.00 MB      554.00 MB
INFO      326.00 MB     228.00 MB      554.00 MB
INFO  State cleared at ~ 350 MB.
INFO           Used          Free            Total
INFO      378.00 MB     176.00 MB      554.00 MB
INFO      430.00 MB     208.00 MB      638.00 MB
INFO      482.00 MB     156.00 MB      638.00 MB
INFO      534.00 MB     104.00 MB      638.00 MB
INFO      586.00 MB      52.00 MB      638.00 MB
INFO      638.00 MB      16.00 MB      654.00 MB
INFO      690.00 MB      16.00 MB      706.00 MB
INFO  State cleared at ~ 350 MB.
INFO           Used          Free            Total
INFO      742.00 MB      16.00 MB      758.00 MB
INFO      794.00 MB      16.00 MB      810.00 MB
INFO      846.00 MB      16.00 MB      862.00 MB
INFO      899.00 MB      15.00 MB      914.00 MB
INFO      951.00 MB      15.00 MB      966.00 MB
INFO     1003.00 MB      15.00 MB     1018.00 MB
INFO     1055.00 MB      15.00 MB     1070.00 MB
...
...

有趣的是,尽管状态已被清除并准备好进行垃圾回收,但可以看到使用的内存(第一列)仍在增长。为什么会这样呢?由于堆有足够的空间可以扩展,JVM 延迟了通常需要大量 CPU 资源的垃圾回收,并优化为服务主线程。让我们看看不同堆大小如何影响此行为。

~ java -jar -Xmx380M app/build/libs/app.jar
INFO           Used          Free            Total
INFO       19.00 MB     357.00 MB      376.00 MB
INFO       70.00 MB     306.00 MB      376.00 MB
INFO      121.00 MB     255.00 MB      376.00 MB
INFO      172.00 MB     204.00 MB      376.00 MB
INFO      208.00 MB     168.00 MB      376.00 MB
INFO      259.00 MB     117.00 MB      376.00 MB
INFO      310.00 MB      66.00 MB      376.00 MB
INFO  State cleared at ~ 350 MB.
INFO           Used          Free            Total
INFO       55.00 MB     321.00 MB      376.00 MB
INFO      106.00 MB     270.00 MB      376.00 MB
INFO      157.00 MB     219.00 MB      376.00 MB
INFO      208.00 MB     168.00 MB      376.00 MB
INFO      259.00 MB     117.00 MB      376.00 MB
INFO      310.00 MB      66.00 MB      376.00 MB
INFO      361.00 MB      15.00 MB      376.00 MB
INFO  State cleared at ~ 350 MB.
INFO           Used          Free            Total
INFO       55.00 MB     321.00 MB      376.00 MB
INFO      106.00 MB     270.00 MB      376.00 MB
INFO      157.00 MB     219.00 MB      376.00 MB
INFO      208.00 MB     168.00 MB      376.00 MB
INFO      259.00 MB     117.00 MB      376.00 MB
INFO      310.00 MB      66.00 MB      376.00 MB
INFO      361.00 MB      15.00 MB      376.00 MB
INFO  State cleared at ~ 350 MB.
INFO           Used          Free            Total
INFO       55.00 MB     321.00 MB      376.00 MB
INFO      106.00 MB     270.00 MB      376.00 MB
INFO      157.00 MB     219.00 MB      376.00 MB
INFO      208.00 MB     168.00 MB      376.00 MB
...
...

在这种情况下,我们分配了刚好足够的堆大小(380 MB)来处理请求。我们可以看到,在这些限制条件下,GC立即启动以避免可怕的内存不足错误。这是 JVM 的承诺 - 它将始终在由于内存不足而失败之前尝试进行垃圾回收。为了完整起见,让我们看一下它的实际效果:

~ java -jar -Xmx150M app/build/libs/app.jar
INFO           Used          Free            Total
INFO       19.00 MB     133.00 MB      152.00 MB
INFO       70.00 MB      82.00 MB      152.00 MB
INFO      106.00 MB      46.00 MB      152.00 MB
Exception in thread "main"
...
...
Caused by: java.lang.OutOfMemoryError: Java heap space
 at com.dansiwiec.HeapDestroyerKt.blowHeap(HeapDestroyer.kt:28)
 at com.dansiwiec.HeapDestroyerKt.main(HeapDestroyer.kt:18)
 ... 8 more

对于 150 MB 的最大堆大小,进程无法处理 350MB 的工作负载,并且在堆被填满时失败,但在垃圾收集器尝试挽救这种情况之前不会失败。

Java Out Of Memory

我们也来看看 Metaspace 的大小。为此,我们将使用 jstat(为简洁起见省略了输出)

~ jstat -gc 35118
MU
4731.0

输出表明 Metaspace 利用率约为 5 MB。记住 Metaspace 负责存储类定义,作为实验,让我们将流行的 Spring Boot 框架添加到我们的应用程序中。

~ jstat -gc 34643
MU
28198.6

Metaspace 跃升至近 30 MB,因为类加载器占用的空间要大得多。对于较大的应用程序,此空间占用超过 100 MB 的情况并不罕见。接下来让我们进入 Kubernetes 领域。


Kubernetes 内存管理

Kubernetes 内存控制在操作系统级别运行,与管理分配给它的内存的 JVM 形成对比。K8s 内存管理机制的目标是确保工作负载被调度到资源充足的节点上,并将它们保持在一定的限制范围内。

Kubernetes Cluster 示例

在定义工作负载时,用户有两个参数可以操作 — requestslimits。这些是在容器级别定义的,但是,为了简单起见,我们将根据 pod 参数来考虑它,这些参数只是容器设置的总和。

当请求 pod 时,kube-scheduler(控制平面的一个组件)查看资源请求并选择一个具有足够资源的节点来容纳 pod。一旦调度,允许 pod 超过其内存requests(只要节点有空闲内存)但禁止超过其limits

Kubelet(节点上的容器运行时)监视 pod 的内存利用率,如果超过内存限制,它将重新启动 pod 或在节点资源不足时将其完全从节点中逐出(有关更多详细信息,请参阅有关此主题的官方文档。这会导致臭名昭著的 OOMKilled(内存不足)的 pod 状态。

当 pod 保持在其限制范围内,但超出了节点的可用内存时,会出现一个有趣的场景。这是可能的,因为调度程序会查看 pod 的请求(而不是限制)以将其调度到节点上。在这种情况下,kubelet 会执行一个称为节点压力驱逐的过程。简而言之,这意味着 pod 正在终止,以便回收节点上的资源。根据节点上的资源状况有多糟糕,驱逐可能是软的(允许 pod 优雅地终止)或硬的。此场景如下图所示。

Pod 驱逐场景

关于驱逐的内部运作,肯定还有很多东西需要了解。有关此复杂过程的更多信息,请点击此处。对于这个故事,我们就此打住,现在看看这两种机制 —— JVM 内存管理和 Kubernetes 是如何协同工作的。

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
1月前
|
安全 Java
Java异常处理:程序世界的“交通规则
Java异常处理:程序世界的“交通规则
317 98
|
2月前
|
监控 Kubernetes Java
使用 New Relic APM 和 Kubernetes Metrics 监控 EKS 上的 Java 微服务
在阿里云AKS上运行Java微服务常遇性能瓶颈与OOMKilled等问题。本文教你通过New Relic实现集群与JVM双层监控,集成Helm部署、JVM代理注入、GC调优及告警仪表盘,打通从节点资源到应用内存的全链路观测,提升排障效率,保障服务稳定。
200 1
存储 jenkins 持续交付
493 2
|
2月前
|
存储 Java 编译器
对比Java学习Go——程序结构与变量
本节对比了Java与Go语言的基础结构,包括“Hello, World!”程序、代码组织方式、入口函数定义、基本数据类型及变量声明方式。Java强调严格的面向对象结构,所有代码需置于类中,入口方法需严格符合`public static void main(String[] args)`格式;而Go语言结构更简洁,使用包和函数组织代码,入口函数为`func main()`。两种语言在变量声明、常量定义、类型系统等方面也存在显著差异,体现了各自的设计哲学。
|
3月前
|
人工智能 监控 安全
智慧工地解决方案,java智慧工地程序代码
智慧工地系统融合物联网、AI、大数据等技术,实现对施工现场“人、机、料、法、环”的全面智能监控与管理,提升安全、效率与决策水平。
131 2
|
8月前
|
存储 Java 数据库连接
【YashanDB知识库】Java程序调用存储过程,在提取clob时报YAS-00004
【YashanDB知识库】Java程序调用存储过程,在提取clob时报YAS-00004
|
8月前
|
搜索推荐 Java Android开发
课时146:使用JDT开发Java程序
在 Eclipse 之中提供有 JDT环境可以实现java 程序的开发,下面就通过一些功能进行演示。 项目开发流程
308 0
|
8月前
|
Java C语言
课时8:Java程序基本概念(标识符与关键字)
课时8介绍Java程序中的标识符与关键字。标识符由字母、数字、下划线和美元符号组成,不能以数字开头且不能使用Java保留字。建议使用有意义的命名,如student_name、age。关键字是特殊标记,如蓝色字体所示。未使用的关键字有goto、const;特殊单词null、true、false不算关键字。JDK1.4后新增assert,JDK1.5后新增enum。
173 4
|
8月前
|
Java 编译器
课时7:Java程序基本概念(注释)
课时7介绍了Java程序中的注释。编程语言有其语法和语义,注释有助于理解代码需求,防止断档。Java支持三类注释:单行(//)、多行(/* */)和文档注释(/** */)。注释不会被编译器编译。范例中展示了如何在代码中使用注释,并强调了注释对项目文档管理的重要性。
186 3

推荐镜像

更多