关于后端:Java-应用程序在-Kubernetes-上棘手的内存管理

36次阅读

共计 11705 个字符,预计需要花费 30 分钟才能阅读完成。

引言

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

在容器环境中运行 Java 应用程序须要理解两者 —— JVM 内存机制和 Kubernetes 内存治理。这两个环境一起工作会产生一个稳固的应用程序,然而,谬误配置最多可能导致基础设施超支,最坏状况下可能会导致应用程序不稳固或解体。咱们将首先认真钻研 JVM 内存的工作原理,而后咱们将转向 Kubernetes,最初,咱们将把这两个概念放在一起。

JVM 内存模型简介

JVM 内存治理是一种高度简单的机制,多年来通过间断公布不断改进,是 JVM 平台的劣势之一。对于本文,咱们将只介绍对本主题有用的基础知识。在较高的档次上,JVM 内存由两个空间组成 —— Heap 和 Metaspace。

非 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 标记管制)。

~ 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 的工作负载,并且在堆被填满时失败,但在垃圾收集器尝试解救这种状况之前不会失败。

咱们也来看看 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 内存管理机制的指标是确保工作负载被调度到资源短缺的节点上,并将它们放弃在肯定的限度范畴内。

在定义工作负载时,用户有两个参数能够操作 — requestslimits。这些是在容器级别定义的,然而,为了简略起见,咱们将依据 pod 参数来思考它,这些参数只是容器设置的总和。

当申请 pod 时,kube-scheduler(管制立体的一个组件)查看资源申请并抉择一个具备足够资源的节点来包容 pod。一旦调度,容许 pod 超过其内存requests(只有节点有闲暇内存)但禁止超过其limits

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

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

对于驱赶的外部运作,必定还有很多货色须要理解。无关此简单过程的更多信息,请点击此处。对于这个故事,咱们就此打住,当初看看这两种机制 —— JVM 内存治理和 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 限度谬误,您将“疾速失败”。无关此倡议的更多信息,请点击此处。

Kubernetes 资源管理和 JVM 内存区域的主题很深,本文只是浅尝辄止。以下是另外一些参考资料:

  • https://learnk8s.io/setting-cpu-memory-limits-requests
  • https://srvaroa.github.io/jvm/kubernetes/memory/docker/oomkil…
  • https://home.robusta.dev/blog/kubernetes-memory-limit
  • https://forums.oracle.com/ords/r/apexds/community/q?question=…

文本翻译自: https://danoncoding.com/tricky-kubernetes-memory-management-f…


正文完
 0