×

作为集群管理员,您可以通过管理应用程序内存来帮助您的集群高效运行,方法是:

  • 确定容器化应用程序组件的内存和风险需求,并配置容器内存参数以满足这些需求。

  • 配置容器化应用程序运行时(例如 OpenJDK)以最佳方式遵守已配置的容器内存参数。

  • 诊断和解决与在容器中运行相关的内存相关错误条件。

了解应用程序内存管理

建议在继续操作之前,请完整阅读 OpenShift Container Platform 如何管理计算资源的概述。

对于每种类型的资源(内存、CPU、存储),OpenShift Container Platform 允许对 Pod 中的每个容器放置可选的 **请求** 和 **限制** 值。

请注意以下关于内存请求和内存限制的内容

  • 内存请求

    • 如果指定了内存请求值,它会影响 OpenShift Container Platform 调度程序。调度程序在将容器调度到节点时会考虑内存请求,然后在所选节点上为容器使用请求的内存划定界限。

    • 如果节点的内存已耗尽,OpenShift Container Platform 会优先驱逐其内存使用量最超过其内存请求的容器。在严重的内存耗尽情况下,节点 OOM 杀手可能会根据类似的指标选择并终止容器中的进程。

    • 集群管理员可以分配配额或为内存请求值分配默认值。

    • 集群管理员可以覆盖开发人员指定的内存请求值,以管理集群超额分配。

  • 内存限制

    • 如果指定了内存限制值,它会为可以在容器中的所有进程中分配的内存提供硬限制。

    • 如果容器中所有进程分配的内存超过内存限制,节点内存不足 (OOM) 杀手将立即选择并终止容器中的一个进程。

    • 如果同时指定了内存请求和内存限制,则内存限制值必须大于或等于内存请求值。

    • 集群管理员可以分配配额或为内存限制值分配默认值。

    • 最小内存限制为 12 MB。如果容器由于 无法分配内存 的 Pod 事件而无法启动,则内存限制太低。请增加或删除内存限制。删除限制允许 Pod 使用无限的节点资源。

管理应用程序内存策略

在 OpenShift Container Platform 上调整应用程序内存大小的步骤如下所示

  1. 确定预期的容器内存使用情况

    确定预期的平均和峰值容器内存使用情况(如有必要,可通过单独的负载测试进行经验性确定)。请记住考虑所有可能在容器中并行运行的进程:例如,主应用程序是否会生成任何辅助脚本?

  2. 确定风险承受能力

    确定驱逐的风险承受能力。如果风险承受能力低,则容器应根据预期的峰值使用量加上一定的安全裕度来请求内存。如果风险承受能力较高,则根据预期的平均使用量请求内存可能更合适。

  3. 设置容器内存请求

    根据以上内容设置容器内存请求。请求越准确地代表应用程序的内存使用情况,效果越好。如果请求过高,则集群和配额使用效率将会降低。如果请求过低,则应用程序被驱逐的可能性会增加。

  4. 根据需要设置容器内存限制

    根据需要设置容器内存限制。如果容器中所有进程的内存使用总量超过限制,则设置限制会立即终止容器进程,这是一个利弊兼具的做法。一方面,它可以尽早发现意外的内存使用过量(“快速失败”);另一方面,它也会突然终止进程。

    请注意,一些 OpenShift Container Platform 集群可能需要设置限制值;一些集群可能会根据限制值覆盖请求值;一些应用程序镜像依赖于设置限制值,因为这比检测请求值更容易。

    如果设置了内存限制,则不应将其设置为小于预期的峰值容器内存使用量加上一定的安全裕度。

  5. 确保应用程序已调优

    如果适用,请确保应用程序已针对配置的请求和限制值进行了调优。此步骤与池化内存的应用程序(例如 JVM)尤其相关。本页的其余部分将对此进行讨论。

了解 OpenShift Container Platform 的 OpenJDK 设置

默认的 OpenJDK 设置在容器化环境中效果不佳。因此,在容器中运行 OpenJDK 时,始终必须提供一些额外的 Java 内存设置。

JVM 内存布局复杂,依赖于版本,详细描述不在本文档的范围之内。但是,作为在容器中运行 OpenJDK 的起点,至少以下三个与内存相关的任务至关重要

  1. 覆盖 JVM 最大堆大小。

  2. 鼓励 JVM 在适当的情况下将未使用的内存释放回操作系统。

  3. 确保容器中所有 JVM 进程都已正确配置。

针对在容器中运行的 JVM 工作负载进行优化调整超出了本文档的范围,可能需要设置多个其他 JVM 选项。

了解如何覆盖 JVM 最大堆大小

对于许多 Java 工作负载,JVM 堆是最大的单一内存使用者。目前,OpenJDK 默认允许使用计算节点内存的最多 1/4 (1/-XX:MaxRAMFraction) 用于堆,无论 OpenJDK 是否在容器中运行。因此,**必须**覆盖此行为,尤其是在也设置了容器内存限制的情况下。

至少有两种方法可以实现上述目标

  • 如果设置了容器内存限制并且 JVM 支持实验选项,请设置 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap

    JDK 11 中已删除 UseCGroupMemoryLimitForHeap 选项。请改用 -XX:+UseContainerSupport

    这会将 -XX:MaxRAM 设置为容器内存限制,并将最大堆大小 (-XX:MaxHeapSize / -Xmx) 设置为 1/-XX:MaxRAMFraction(默认为 1/4)。

  • 直接覆盖 -XX:MaxRAM-XX:MaxHeapSize-Xmx 之一。

    此选项涉及硬编码值,但具有允许计算安全裕度的优点。

了解如何鼓励 JVM 将未使用的内存释放回操作系统

默认情况下,OpenJDK 不会积极地将未使用的内存返回给操作系统。这对于许多容器化的 Java 工作负载来说可能是合适的,但值得注意的例外情况包括在容器内与 JVM 同时存在其他活动进程的工作负载,无论这些其他进程是本机的、额外的 JVM 还是两者的组合。

基于 Java 的代理可以使用以下 JVM 参数来鼓励 JVM 将未使用的内存释放回操作系统

-XX:+UseParallelGC
-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10 -XX:GCTimeRatio=4
-XX:AdaptiveSizePolicyWeight=90.

这些参数旨在在分配的内存超过使用中内存的 110% (-XX:MaxHeapFreeRatio) 时,并在垃圾收集器中花费高达 20% 的 CPU 时间 (-XX:GCTimeRatio) 时,将堆内存返回给操作系统。应用程序堆分配在任何时候都不会小于初始堆分配(由 -XX:InitialHeapSize / -Xms 覆盖)。更多详细信息请访问 在 OpenShift 中调整 Java 的内存占用(第一部分)在 OpenShift 中调整 Java 的内存占用(第二部分)OpenJDK 和容器

了解如何确保容器中所有 JVM 进程都已正确配置

如果多个 JVM 在同一容器中运行,则必须确保它们都已正确配置。对于许多工作负载,需要为每个 JVM 分配一定百分比的内存预算,并可能留出相当大的额外安全裕度。

许多 Java 工具使用不同的环境变量(JAVA_OPTSGRADLE_OPTS 等)来配置它们的 JVM,确保将正确的设置传递给正确的 JVM 可能具有挑战性。

OpenJDK 始终遵循JAVA_TOOL_OPTIONS环境变量,在 JVM 命令行中指定的其他选项将覆盖JAVA_TOOL_OPTIONS中指定的参数。默认情况下,为确保这些选项被基于 Java 的 Agent 镜像中运行的所有 JVM 工作负载默认使用,OpenShift Container Platform Jenkins Maven Agent 镜像设置了

JAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap -Dsun.zip.disableMemoryMapping=true"

JDK 11 中已删除 UseCGroupMemoryLimitForHeap 选项。请改用 -XX:+UseContainerSupport

这并不能保证不需要其他选项,但它旨在作为一个有用的起点。

在 Pod 内查找内存请求和限制

希望动态发现其在 Pod 内的内存请求和限制的应用程序应使用向下 API。

步骤
  • 配置 Pod 以添加MEMORY_REQUESTMEMORY_LIMIT段落

    1. 创建一个类似于以下内容的 YAML 文件

      apiVersion: v1
      kind: Pod
      metadata:
        name: test
      spec:
        securityContext:
          runAsNonRoot: true
          seccompProfile:
            type: RuntimeDefault
        containers:
        - name: test
          image: fedora:latest
          command:
          - sleep
          - "3600"
          env:
          - name: MEMORY_REQUEST (1)
            valueFrom:
              resourceFieldRef:
                containerName: test
                resource: requests.memory
          - name: MEMORY_LIMIT (2)
            valueFrom:
              resourceFieldRef:
                containerName: test
                resource: limits.memory
          resources:
            requests:
              memory: 384Mi
            limits:
              memory: 512Mi
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: [ALL]
      1 添加此段落以发现应用程序内存请求值。
      2 添加此段落以发现应用程序内存限制值。
    2. 运行以下命令创建 Pod

      $ oc create -f <file_name>.yaml
验证
  1. 使用远程 shell 访问 Pod

    $ oc rsh test
  2. 检查请求的值是否已应用

    $ env | grep MEMORY | sort
    示例输出
    MEMORY_LIMIT=536870912
    MEMORY_REQUEST=402653184

也可以通过/sys/fs/cgroup/memory/memory.limit_in_bytes文件在容器内部读取内存限制值。

理解 OOM 终止策略

如果容器中所有进程的总内存使用量超过内存限制,或者在节点内存严重耗尽的情况下,OpenShift Container Platform 可以终止容器中的进程。

当进程因内存不足 (OOM) 而被终止时,这可能会导致容器立即退出。如果容器 PID 1 进程收到SIGKILL信号,则容器将立即退出。否则,容器的行为取决于其他进程的行为。

例如,容器进程以代码 137 退出,表示它收到了 SIGKILL 信号。

如果容器没有立即退出,则可以检测到 OOM 终止,如下所示

  1. 使用远程 shell 访问 Pod

    # oc rsh test
  2. 运行以下命令查看/sys/fs/cgroup/memory/memory.oom_control中的当前 OOM 终止计数

    $ grep '^oom_kill ' /sys/fs/cgroup/memory/memory.oom_control
    示例输出
    oom_kill 0
  3. 运行以下命令以引发 OOM 终止

    $ sed -e '' </dev/zero
    示例输出
    Killed
  4. 运行以下命令查看sed命令的退出状态

    $ echo $?
    示例输出
    137

    代码137表示容器进程以代码 137 退出,表示它收到了 SIGKILL 信号。

  5. 运行以下命令查看/sys/fs/cgroup/memory/memory.oom_control中的 OOM 终止计数器是否已递增

    $ grep '^oom_kill ' /sys/fs/cgroup/memory/memory.oom_control
    示例输出
    oom_kill 1

    如果 Pod 中的一个或多个进程被 OOM 终止,当 Pod 随后退出(无论是否立即退出)时,其阶段将为Failed,原因将为OOMKilled。OOM 终止的 Pod 是否会重新启动取决于restartPolicy的值。如果未重新启动,则诸如复制控制器的控制器将注意到 Pod 的失败状态,并创建一个新的 Pod 来替换旧的 Pod。

    使用以下命令获取 Pod 状态

    $ oc get pod test
    示例输出
    NAME      READY     STATUS      RESTARTS   AGE
    test      0/1       OOMKilled   0          1m
    • 如果 Pod 未重新启动,请运行以下命令查看 Pod

      $ oc get pod test -o yaml
      示例输出
      ...
      status:
        containerStatuses:
        - name: test
          ready: false
          restartCount: 0
          state:
            terminated:
              exitCode: 137
              reason: OOMKilled
        phase: Failed
    • 如果已重新启动,请运行以下命令查看 Pod

      $ oc get pod test -o yaml
      示例输出
      ...
      status:
        containerStatuses:
        - name: test
          ready: true
          restartCount: 1
          lastState:
            terminated:
              exitCode: 137
              reason: OOMKilled
          state:
            running:
        phase: Running

理解 Pod 驱逐

当节点内存耗尽时,OpenShift Container Platform 可能会将其节点上的 Pod 驱逐出去。根据内存耗尽的程度,驱逐可能是优雅的,也可能不是优雅的。优雅驱逐意味着每个容器的主进程 (PID 1) 都收到 SIGTERM 信号,然后一段时间后,如果进程尚未退出,则会收到 SIGKILL 信号。非优雅驱逐意味着每个容器的主进程都会立即收到 SIGKILL 信号。

被驱逐的 Pod 的阶段为Failed,原因为Evicted。它不会重新启动,无论restartPolicy的值如何。但是,诸如复制控制器的控制器将注意到 Pod 的失败状态,并创建一个新的 Pod 来替换旧的 Pod。

$ oc get pod test
示例输出
NAME      READY     STATUS    RESTARTS   AGE
test      0/1       Evicted   0          1m
$ oc get pod test -o yaml
示例输出
...
status:
  message: 'Pod The node was low on resource: [MemoryPressure].'
  phase: Failed
  reason: Evicted