8.4. 配置集群内存以满足容器内存和风险要求

作为集群管理员,您可以通过以下方式管理应用程序内存,从而帮助集群有效运作:

  • 确定容器化应用程序组件的内存和风险要求,并配置容器内存参数以满足这些要求。
  • 配置容器化应用程序运行时(如 OpenJDK),以最佳的方式遵守配置的容器内存参数。
  • 诊断并解决与在容器中运行相关的内存错误情况。

8.4.1. 了解管理应用程序内存

在继续操作前,建议您仔细阅读 OpenShift Container Platform 如何管理计算资源的概述。

对于每种资源(内存、CPU 或存储),OpenShift Container Platform 允许在 pod 中的各个容器上设置可选的 requestlimit 值。

注意以下关于内存请求和内存限制的信息:

  • 内存请求

    • 如果指定,内存请求值会影响 OpenShift Container Platform 调度程序。将容器调度到节点时,调度程序会考虑内存请求,然后在所选节点上隔离出请求的内存供该容器使用。
    • 如果节点的内存已用尽,OpenShift Container Platform 将优先驱除其内存用量超出内存请求最多的容器。在严重的内存耗尽情形中,节点 OOM 终止程序可以根据类似的指标选择并终止容器中的一个进程。
    • 集群管理员可以分配配额,或者分配内存请求值的默认值。
    • 集群管理员可以覆盖开发人员指定的内存请求值,以便管理集群过量使用。
  • 内存限制

    • 如果指定,内存限制值针对可在容器中所有进程间分配的内存提供硬性限制。
    • 如果分配给容器中所有进程的内存超过内存限制,则节点超出内存(OOM)终止程序将立即选择并终止容器中的一个进程。
    • 如果同时指定了内存请求和限制,则内存限制必须大于或等于内存请求量。
    • 集群管理员可以分配配额,或者分配内存限制值的默认值。
    • 最小内存限值为 12MB。如果容器因为一个 Cannot allocate memory pod 事件启动失败,这代表内存限制太低。增加或删除内存限制。删除限制可让 pod 消耗无限的节点资源。

8.4.1.1. 管理应用程序内存策略

如下是 OpenShift Container Platform 上调整应用程序内存大小的步骤:

  1. 确定预期的容器内存用量

    从经验判断(例如,通过独立的负载测试),根据需要确定容器内存用量的预期平均值和峰值。需要考虑容器中有可能并行运行的所有进程:例如,主应用程序是否生成任何辅助脚本?

  2. 确定风险嗜好

    确定用于驱除的风险嗜好。如果风险嗜好较低,则容器应根据预期的峰值用量加上一个安全裕度百分比来请求内存。如果风险嗜好较高,那么根据预期的平均用量请求内存可能更为妥当。

  3. 设定容器内存请求

    根据以上所述设定容器内存请求。请求越能准确表示应用程序内存用量越好。如果请求过高,集群和配额用量效率低下。如果请求过低,应用程序驱除的几率就会提高。

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

    在必要时,设定容器内存限制。如果容器中所有进程的总内存用量超过限制,那么设置限制会立即终止容器进程,所以这既有利也有弊。一方面,可能会导致过早出现意料之外的过量内存使用(“快速失败”);另一方面,也会突然终止进程。

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

    如果设置内存限制,其大小不应小于预期峰值容器内存用量加上安全裕度百分比。

  5. 确保应用程序经过性能优化

    在适当时,确保应用程序已根据配置的请求和限制进行了性能优化。对于池化内存的应用程序(如 JVM),这一步尤为相关。本页的其余部分将介绍这方面的内容。

8.4.2. 了解 OpenShift Container Platform 的 OpenJDK 设置

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

JVM 内存布局比较复杂,并且视版本而异,因此本文不做详细讨论。但作为在容器中运行 OpenJDK 的起点,至少以下三个于内存相关的任务非常重要:

  1. 覆盖 JVM 最大堆大小。
  2. 在可能的情况下,促使 JVM 向操作系统释放未使用的内存。
  3. 确保正确配置了容器中的所有 JVM 进程。

优化容器中运行的 JVM 工作负载已超出本文讨论范畴,并且可能涉及设置多个额外的 JVM 选项。

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

对于许多 Java 工作负载,JVM 堆是最大的内存用户。目前,OpenJDK 默认允许将计算节点最多 1/4 (1/-XX:MaxRAMFraction) 的内存用于该堆,不论 OpenJDK 是否在容器内运行。因此,务必要覆盖此行为,特别是设置了容器内存限制时。

达成以上目标至少有两种方式:

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

    注意

    UseCGroupMemoryLimitForHeap 选项已在 JDK 11 中删除。使用 -XX:+UseContainerSupport 替代

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

  • 直接覆盖 -XX:MaxRAM-XX:MaxHeapSize-Xmx

    这个选项涉及对值进行硬编码,但也有允许计算安全裕度的好处。

8.4.2.2. 了解如何促使 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 覆盖)。调节 Java 在 OpenShift 中的内存占用(第 1 部分)调节 Java 在 OpenShift 中的内存占用(第 2 部分)以及 OpenJDK 和容器提供了其他的详细信息。

8.4.2.3. 了解如何确保正确配置容器中的所有 JVM 进程

如果多个 JVM 在同一容器中运行,则必须保证它们的配置都正确无误。如果有许多工作负载,需要为每个 JVM 分配一个内存预算百分比,留出较大的额外安全裕度。

许多 Java 工具使用不同的环境变量(JAVA_OPTSGRADLE_OPTS 等)来配置其 JVM,并确保将正确的设置传递给正确的 JVM。

OpenJDK 始终尊重 JAVA_TOOL_OPTIONS 环境变量,在 JAVA_TOOL_OPTIONS 中指定的值会被 JVM 命令行中指定的其他选项覆盖。默认情况下,为了确保这些选项默认用于在基于 Java 的代理镜像中运行的所有 JVM 工作负载,OpenShift Container Platform Jenkins Maven 代理镜像集:

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

UseCGroupMemoryLimitForHeap 选项已在 JDK 11 中删除。使用 -XX:+UseContainerSupport 替代

这不能保证不需要额外选项,只是用作一个实用的起点。

8.4.3. 从 pod 中查找内存请求和限制

希望从 pod 中动态发现内存请求和限制的应用程序应该使用 Downward API。

流程

  1. 配置 pod,以添加 MEMORY_REQUESTMEMORY_LIMIT 小节:

    1. 创建一个类似以下示例的 YAML 文件:

      apiVersion: v1
      kind: Pod
      metadata:
        name: test
      spec:
        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
      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 文件从容器内部读取。

8.4.4. 了解 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 kill:

    $ 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 随后退出时(不论是否立即发生),它都将会具有原因为 OOMKilledFailed 阶段。被 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

8.4.5. 了解 pod 驱除

OpenShift Container Platform 可在节点内存耗尽时从节点上驱除 pod。根据内存耗尽的程度,驱除可能是安全操作,但也不一定。安全驱除表示,各个容器的主进程 (PID 1) 收到 SIGTERM 信号,稍等片刻后,如果进程还未退出,则会收到一个 SIGKILL 信号。非安全驱除暗示着各个容器的主进程会立即收到 SIGKILL 信号。

被驱除的 pod 具有 Failed 阶段,原因为 Evicted。无论 restartPolicy 的值是什么,该 pod 都不会重启。但是,复制控制器等控制器会看到 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