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

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

JVM 和 Kubernetes

Java 10 引入了一个新的 JVM 标志 —— -XX:+UseContainerSupport(默认设置为 true),如果 JVM 在资源有限的容器环境中运行,它允许 JVM 检测可用内存和 CPU。该标志与 -XX:MaxRAMPercentage 一起使用,让我们根据总可用内存的百分比设置最大堆大小。在 Kubernetes 的情况下,容器上的 limits 设置被用作此计算的基础。例如 —— 如果 pod 具有 2GB 的限制,并且将 MaxRAMPercentage 标志设置为 75%,则结果将是 1500MB 的最大堆大小。

这需要一些技巧,因为正如我们之前看到的,Java 应用程序的总体内存占用量高于堆(还有 Metaspace 、线程、垃圾回收、APM 代理等)。这意味着,需要在最大堆空间、非堆内存使用量和 pod 限制之间取得平衡。具体来说,前两个的总和不能超过最后一个,因为它会导致 OOMKilled(参见上一节)。

为了观察这两种机制的作用,我们将使用相同的示例项目,但这次我们将把它部署在(本地)Kubernetes 集群上。为了在 Kubernetes 上部署应用程序,我们将其打包为一个 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: heapkiller
spec:
  containers:
    - name: heapkiller
      image: heapkiller
      imagePullPolicy: Never
      resources:
        requests:
          memory: "500Mi"
          cpu: "500m"
        limits:
          memory: "500Mi"
          cpu: "500m"
      env:
        - name: JAVA_TOOL_OPTIONS
          value: '-XX:MaxRAMPercentage=70.0'

快速复习第一部分 —— 我们确定应用程序需要至少 380MB的堆内存才能正常运行。

场景 1 — Java Out Of Memory 错误

让我们首先了解我们可以操作的参数。它们是 — pod 内存的 requestslimits,以及 Java 的最大堆大小,在我们的例子中由 MaxRAMPercentage 标志控制。

在第一种情况下,我们将总内存的 70% 分配给堆。pod 请求和限制都设置为 500MB,这导致最大堆为 350MB(500MB 的 70%)。

我们执行 kubectl apply -f pod.yaml 部署 pod ,然后用 kubectl get logs -f pod/heapkiller 观察日志。应用程序启动后不久,我们会看到以下输出:

INFO  Started HeapDestroyerKt in 5.599 seconds (JVM running for 6.912)
INFO           Used          Free            Total
INFO       17.00 MB       5.00 MB       22.00 MB
...
INFO      260.00 MB      78.00 MB      338.00 MB
...
Exception in thread "main" java.lang.reflect.InvocationTargetException
Caused by: java.lang.OutOfMemoryError: Java heap space

如果我们执行 kubectl describe pod/heapkiller 拉出 pod 详细信息,我们将找到以下信息:

Containers:
  heapkiller:
    ....
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
...
Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
...
  Warning  BackOff    7s (x7 over 89s)   kubelet            Back-off restarting failed container

简而言之,这意味着 pod 以状态码 1 退出(Java Out Of Memory 的退出码),Kubernetes 将继续使用标准退避策略重新启动它(以指数方式增加重新启动之间的暂停时间)。下图描述了这种情况。

这种情况下的关键要点是 —— 如果 Java 因 OutOfMemory 错误而失败,您将在 pod 日志中看到它👌。

场景 2 — Pod 超出内存 limit 限制

为了实现这个场景,我们的 Java 应用程序需要更多内存。我们将 MaxRAMPercentage 从 70% 增加到 90%,看看会发生什么。我们按照与之前相同的步骤并查看日志。该应用程序运行良好了一段时间:

...
...
INFO      323.00 MB      83.00 MB      406.00 MB
INFO      333.00 MB      73.00 MB      406.00 MB

然后 …… 噗。没有更多的日志。我们运行与之前相同的 describe 命令以获取有关 pod 状态的详细信息。

Containers:
  heapkiller:
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
Events:
  Type     Reason     Age                  From              Message
 ----     ------     ----                 ----               ------
...
...
 Warning  BackOff    6s (x7 over 107s)    kubelet            Back-off restarting failed container

乍看之下,这与之前的场景类似 —— pod crash,现在处于 CrashLoopBackOff(Kubernetes 一直在重启),但实际上却大不相同。之前,pod 中的进程退出(JVM 因内存不足错误而崩溃),在这种情况下,是 Kubernetes 杀死了 pod。该 OOMKill 状态表示 Kubernetes 已停止 pod,因为它已超出其分配的内存限制。这怎么可能?

通过将 90% 的可用内存分配给堆,我们假设其他所有内容都适合剩余的 10% (50MB),而对于我们的应用程序,情况并非如此,这导致内存占用超过 500MB 限制。下图展示了超出 pod 内存限制的场景。

要点 —— OOMKilled 在 pod 的状态中查找。

场景 3 — Pod 超出节点的可用内存

最后一种不太常见的故障情况是 pod 驱逐。在这种情况下 — 内存requestlimit是不同的。Kubernetes 根据request参数而不是limit参数在节点上调度 pod。如果一个节点满足请求,kube-scheduler将选择它,而不管节点满足限制的能力如何。在我们将 pod 调度到节点上之前,让我们先看一下该节点的一些详细信息:

~ kubectl describe node/docker-desktop
Allocatable:
  cpu:                4
  memory:             1933496Ki
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests     Limits
  --------           --------     ------
  cpu                850m (21%)   0 (0%)
  memory             240Mi (12%)  340Mi (18%)

我们可以看到该节点有大约 2GB 的可分配内存,并且已经占用了大约 240MB(由kube-system pod,例如etcdcoredns)。

对于这种情况,我们调整了 pod 的参数 —— request: 500Mi(未更改),limit: 2500Mi 我们重新配置应用程序以将堆填充到 2500MB(之前为 350MB)。当 pod 被调度到节点上时,我们可以在节点描述中看到这种分配:

Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests     Limits
  --------           --------     ------
  cpu                1350m (33%)  500m (12%)
  memory             740Mi (39%)  2840Mi (150%)

当 pod 到达节点的可用内存时,它会被杀死,我们会在 pod 的描述中看到以下详细信息:

~ kubectl describe pod/heapkiller
Status:           Failed
Reason:           Evicted
Message:          The node was low on resource: memory.
Containers:
  heapkiller:
    State:          Terminated
      Reason:       ContainerStatusUnknown
      Message:      The container could not be located when the pod was terminated
      Exit Code:    137
      Reason:       OOMKilled

这表明由于节点内存不足,pod 被逐出。我们可以在节点描述中看到更多细节:

~ kubectl describe node/docker-desktop
Events:
  Type     Reason                   Age                 From     Message
  ----     ------                   ----                ----     -------
  Warning  SystemOOM                1s                  kubelet  System OOM encountered, victim process: java, pid: 67144

此时,CrashBackoffLoop 开始,pod 不断重启。下图描述了这种情况。

关键要点 —— 在 pod 的状态中查找 Evicted 以及通知节点内存不足的事件。

场景 4 — 参数配置良好,应用程序运行良好

最后一个场景显示应用程序在正确调整的参数下正常运行。为此,我们将pod 的requestlimit 都设置为 500MB,将 -XX:MaxRAMPercentage 设置为 80%。

让我们收集一些统计数据,以了解节点级别和更深层次的 Pod 中正在发生的情况。

~ kubectl describe node/docker-desktop
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests     Limits
  --------           --------     ------
  cpu                1350m (33%)  500m (12%)
  memory             740Mi (39%)  840Mi (44%)

节点看起来很健康,有空闲资源👌。让我们看看 pod 的内部。

# Run from within the container
~ cat /sys/fs/cgroup/memory.current
523747328

这显示了容器的当前内存使用情况。那是 499MB,就在边缘。让我们看看是什么占用了这段内存:

# Run from within the container
~ ps -o pid,rss,command ax
  PID   RSS   COMMAND
    1 501652  java -XX:NativeMemoryTracking=summary -jar /app.jar
   36   472   /bin/sh
   55  1348   ps -o pid,rss,command ax

RSS,*Resident Set Size,*是对正在占用的内存进程的一个很好的估计。上面显示 490MB(501652 bytes)被 Java 进程占用。让我们再剥离一层,看看 JVM 的内存分配。我们传递给 Java 进程的标志 -XX:NativeMemoryTracking 允许我们收集有关 Java 内存空间的详细运行时统计信息。

~ jcmd 1 VM.native_memory summary
Total: reserved=1824336KB, committed=480300KB
-                 Java Heap (reserved=409600KB, committed=409600KB)
                            (mmap: reserved=409600KB, committed=409600KB)
-                     Class (reserved=1049289KB, committed=4297KB)
                            (classes #6760)
                            (  instance classes #6258, array classes #502)
                            (malloc=713KB #15321)
                            (mmap: reserved=1048576KB, committed=3584KB)
                            (  Metadata:   )
                            (    reserved=32768KB, committed=24896KB)
                            (    used=24681KB)
                            (    waste=215KB =0.86%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=3584KB)
                            (    used=3457KB)
                            (    waste=127KB =3.55%)
-                    Thread (reserved=59475KB, committed=2571KB)
                            (thread #29)
                            (stack: reserved=59392KB, committed=2488KB)
                            (malloc=51KB #178)
                            (arena=32KB #56)
-                      Code (reserved=248531KB, committed=14327KB)
                            (malloc=800KB #4785)
                            (mmap: reserved=247688KB, committed=13484KB)
                            (arena=43KB #45)
-                        GC (reserved=1365KB, committed=1365KB)
                            (malloc=25KB #83)
                            (mmap: reserved=1340KB, committed=1340KB)
-                  Compiler (reserved=204KB, committed=204KB)
                            (malloc=39KB #316)
                            (arena=165KB #5)
-                  Internal (reserved=283KB, committed=283KB)
                            (malloc=247KB #5209)
                            (mmap: reserved=36KB, committed=36KB)
-                     Other (reserved=26KB, committed=26KB)
                            (malloc=26KB #3)
-                    Symbol (reserved=6918KB, committed=6918KB)
                            (malloc=6206KB #163986)
                            (arena=712KB #1)
-    Native Memory Tracking (reserved=3018KB, committed=3018KB)
                            (malloc=6KB #92)
                            (tracking overhead=3012KB)
-        Shared class space (reserved=12288KB, committed=12224KB)
                            (mmap: reserved=12288KB, committed=12224KB)
-               Arena Chunk (reserved=176KB, committed=176KB)
                            (malloc=176KB)
-                   Logging (reserved=5KB, committed=5KB)
                            (malloc=5KB #219)
-                 Arguments (reserved=1KB, committed=1KB)
                            (malloc=1KB #53)
-                    Module (reserved=229KB, committed=229KB)
                            (malloc=229KB #1710)
-                 Safepoint (reserved=8KB, committed=8KB)
                            (mmap: reserved=8KB, committed=8KB)
-           Synchronization (reserved=48KB, committed=48KB)
                            (malloc=48KB #574)
-            Serviceability (reserved=1KB, committed=1KB)
                            (malloc=1KB #14)
-                 Metaspace (reserved=32870KB, committed=24998KB)
                            (malloc=102KB #52)
                            (mmap: reserved=32768KB, committed=24896KB)
-      String Deduplication (reserved=1KB, committed=1KB)
                            (malloc=1KB #8)

这可能是不言而喻的 —— 这个场景仅用于说明目的。在现实生活中的应用程序中,我不建议使用如此少的资源进行操作。您所感到舒适的程度将取决于您可观察性实践的成熟程度(换句话说——您多快注意到有问题),工作负载的重要性以及其他因素,例如故障转移。

结语

感谢您坚持阅读这篇长文章!我想提供一些建议,帮助您远离麻烦:

  1. 设置内存的 requestlimit 一样,这样你就可以避免由于节点资源不足而导致 pod 被驱逐(缺点就是会导致节点资源利用率降低)。
  2. 仅在出现 Java OutOfMemory 错误时增加 pod 的内存限制。如果发生 OOMKilled 崩溃,请将更多内存留给非堆使用。
  3. 将最大和初始堆大小设置为相同的值。这样,您将在堆分配增加的情况下防止性能损失,并且如果堆百分比/非堆内存/pod 限制错误,您将“快速失败”。有关此建议的更多信息,请点击此处。
相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
3天前
|
Java 数据库连接 开发者
Java的Shutdown Hook机制:优雅地关闭应用程序
Java的Shutdown Hook机制:优雅地关闭应用程序
22 1
|
1天前
|
移动开发 前端开发 JavaScript
Java和web前端,IT新人该如何选择?,2024年最新Web前端内存优化面试
Java和web前端,IT新人该如何选择?,2024年最新Web前端内存优化面试
|
2天前
|
存储 算法 Java
Java一分钟之-Java内存模型与垃圾回收机制概览
【5月更文挑战第16天】本文简述Java内存模型(JMM)和垃圾回收(GC)机制。JMM包括栈、堆、方法区、程序计数器和本地方法栈。GC负责回收不再使用的对象内存,常用算法有新生代、老年代和全堆GC。文章讨论了内存溢出、死锁和GC性能等问题,提出了解决方案,如调整JVM参数和优化GC策略。此外,还强调了避免内存泄漏、大对象管理及正确释放资源的重要性。理解这些概念有助于提升Java应用的性能和稳定性。
13 1
|
3天前
|
Kubernetes Cloud Native 持续交付
构建高效稳定的云原生应用:容器编排与微服务治理实践
【5月更文挑战第14天】 随着企业数字化转型的深入,云原生技术以其弹性、敏捷和可扩展的特性成为现代应用开发的首选模式。本文将探讨如何通过容器编排工具如Kubernetes以及微服务架构的有效治理,构建和维护高效且稳定的云原生应用。我们将分析容器化技术的优势,并结合案例讨论在多云环境下实现持续集成、持续部署(CI/CD)的最佳实践,同时解决微服务带来的分布式复杂性问题。通过本文的阐述,读者将获得一套提升系统可靠性和业务连续性的策略框架。
7 0
|
3天前
|
运维 Kubernetes Linux
Kubernetes详解(七)——Service对象部署和应用
Kubernetes详解(七)——Service对象部署和应用
11 3
|
3天前
|
前端开发 Java 应用服务中间件
【异常解决】java程序连接MinIO报错The request signature we calculated does not match the signature you provided.
【异常解决】java程序连接MinIO报错The request signature we calculated does not match the signature you provided.
17 0
|
3天前
|
Kubernetes 应用服务中间件 nginx
Kubernetes详解(六)——Pod对象部署和应用
在Kubernetes系列中,本文聚焦Pod对象的部署和管理。首先,通过`kubectl run`命令创建Pod,如`kubectl run pod-test --image=nginx:1.12 --port=80 --replicas=1`。接着,使用`kubectl get deployment`或`kubectl get pods`查看Pod信息,添加`-o wide`参数获取详细详情。然后,利用Pod的IP地址进行访问。最后,用`kubectl delete pods [Pod名]`删除Pod,但因Controller控制器,删除后Pod可能自动重建。了解更多细节,请参阅原文链接。
14 5
|
3天前
|
Java Linux C语言
一步带你了解java程序逻辑控制
一步带你了解java程序逻辑控制
17 2
|
3天前
|
Java 数据安全/隐私保护
java中程序控制的典例
java中程序控制的典例
13 1
|
3天前
|
消息中间件 监控 NoSQL
中间件应用合理配置内存
【5月更文挑战第4天】中间件应用合理配置内存
15 2
中间件应用合理配置内存

推荐镜像

更多