6.3. 配置集群内存以满足容器内存和风险要求
作为集群管理员,您可以通过以下方式管理应用程序内存,从而帮助集群有效运作:
- 确定容器化应用程序组件的内存和风险要求,并配置容器内存参数以满足这些要求。
- 配置容器化应用程序运行时(如 OpenJDK),以最佳的方式遵守配置的容器内存参数。
- 诊断并解决与在容器中运行相关的内存错误情况。
6.3.1. 了解管理应用程序内存
在继续操作前,建议您先通篇阅读有关 OpenShift Container Platform 如何管理计算资源的概述。
对于每种资源(内存、CPU 或存储),OpenShift Container Platform 允许在 pod 中的各个容器上设置可选的 request 和 limit 值。
注意以下关于内存请求和内存限制的信息:
内存请求
- 如果指定,内存请求值会影响 OpenShift Container Platform 调度程序。将容器调度到节点时,调度程序会考虑内存请求,然后在所选节点上隔离出请求的内存供该容器使用。
- 如果节点的内存已用尽,OpenShift Container Platform 将优先驱除其内存用量超出内存请求最多的容器。在严重的内存耗尽情形中,节点 OOM 终止程序可以根据类似的指标选择并终止容器中的一个进程。
- 集群管理员可以分配配额,或者分配内存请求值的默认值。
- 集群管理员可以覆盖开发者指定的内存请求值,以便管理集群过量使用。
内存限制
- 如果指定,内存限制值针对可在容器中所有进程间分配的内存提供硬性限制。
- 如果分配给容器中所有进程的内存超过内存限制,则节点 OOM 终止程序将立即选择并终止容器中的一个进程。
- 如果同时指定了内存请求和限制,则内存限制必须大于或等于内存请求量。
- 集群管理员可以分配配额,或者分配内存限制值的默认值。
6.3.1.1. 管理应用程序内存策略
如下是 OpenShift Container Platform 上调整应用程序内存大小的步骤:
确定预期的容器内存用量
从经验判断(例如,通过独立的负载测试),根据需要确定容器内存用量的预期平均值和峰值。需要考虑容器中有可能并行运行的所有进程:例如,主应用程序是否生成任何辅助脚本?
确定风险嗜好
确定用于驱除的风险嗜好。如果风险嗜好较低,则容器应根据预期的峰值用量加上一个安全裕度百分比来请求内存。如果风险嗜好较高,那么根据预期的平均用量请求内存可能更为妥当。
设定容器内存请求
根据以上所述设定容器内存请求。请求越能准确表示应用程序内存用量越好。如果请求过高,集群和配额用量效率低下。如果请求过低,应用程序驱除的几率就会提高。
根据需要设定容器内存限制
在必要时,设定容器内存限制。如果容器中所有进程的总内存用量超过限制,那么设置限制会立即终止容器进程,所以这既有利也有弊。一方面,可能会导致过早出现意料之外的过量内存使用(“快速失败”);另一方面,也会突然终止进程。
需要注意的是,有些 OpenShift Container Platform 集群可能要求设置限制;有些集群可能会根据限制覆盖请求;而且有些应用程序镜像会依赖于设置的限制,因为这比请求值更容易检测。
如果设置内存限制,其大小不应小于预期峰值容器内存用量加上安全裕度百分比。
确保应用程序经过性能优化
在适当时,确保应用程序已根据配置的请求和限制进行了性能优化。对于池化内存的应用程序(如 JVM),这一步尤为相关。本页的其余部分将介绍这方面的内容。
6.3.2. 了解 OpenShift Container Platform 的 OpenJDK 设置
默认的 OpenJDK 设置在容器化环境中效果不佳。因此在容器中运行 OpenJDK 时,务必要提供一些额外的 Java 内存设置。
JVM 内存布局比较复杂,并且视版本而异,因此本文不做详细讨论。但作为在容器中运行 OpenJDK 的起点,至少以下三个于内存相关的任务非常重要:
- 覆盖 JVM 最大堆大小。
- 在可能的情况下,促使 JVM 向操作系统释放未使用的内存。
- 确保正确配置了容器中的所有 JVM 进程。
优化容器中运行的 JVM 工作负载已超出本文讨论范畴,并且可能涉及设置多个额外的 JVM 选项。
6.3.2.1. 了解如何覆盖 JVM 最大堆大小
对于许多 Java 工作负载,JVM 堆是最大的内存用户。目前,OpenJDK 默认允许将计算节点最多 1/4 (1/-XX:MaxRAMFraction
) 的内存用于该堆,不论 OpenJDK 是否在容器内运行。因此,务必要覆盖此行为,特别是设置了容器内存限制时。
达成以上目标至少有两种方式:
如果设置了容器内存限制,并且 JVM 支持那些实验性选项,请设置
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
。这会将
-XX:MaxRAM
设置为容器内存限制,并将最大堆大小 (-XX:MaxHeapSize
/-Xmx
) 设置为 1/-XX:MaxRAMFraction
(默认为 1/4)。直接覆盖
-XX:MaxRAM
、-XX:MaxHeapSize
或-Xmx
。这个选项涉及对值进行硬编码,但也有允许计算安全裕度的好处。
6.3.2.2. 了解如何促使 JVM 向操作系统释放未用的内存
默认情况下,OpenJDK 不会主动向操作系统退还未用的内存。这可能适合许多容器化的 Java 工作负载,但也有明显的例外,例如额外活跃进程与容器内 JVM 共存的工作负载,这些额外进程是原生或附加的 JVM,或者这两者的组合。
OpenShift Container Platform Jenkins maven slave 镜像使用以下 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 和容器提供了其他的详细信息。
6.3.2.3. 了解如何确保正确配置容器中的所有 JVM 进程
如果多个 JVM 在同一容器中运行,则必须保证它们的配置都正确无误。如果有许多工作负载,需要为每个 JVM 分配一个内存预算百分比,留出较大的额外安全裕度。
许多 Java 工具使用不同的环境变量(JAVA_OPTS
、GRADLE_OPTS
和 MAVEN_OPTS
等)来配置它们的 JVM,或许难以确保将正确的设置传递给正确的 JVM。
OpenJDK 始终尊重 JAVA_TOOL_OPTIONS
环境变量,在 JAVA_TOOL_OPTIONS
中指定的值会被 JVM 命令行中指定的其他选项覆盖。默认情况下,为确保在 slave 镜像中运行的所有 JVM 工作负载都默认使用这些选项,OpenShift Container Platform Jenkins maven slave 镜像将进行以下设置:
JAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Dsun.zip.disableMemoryMapping=true"
这不能保证不需要额外选项,只是用作一个实用的起点。
6.3.3. 从 pod 中查找内存请求和限制
希望从 pod 中动态发现内存请求和限制的应用程序应该使用 Downward API。
流程
-
配置 pod,以添加
MEMORY_REQUEST
和MEMORY_LIMIT
小节:
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
内存限制值也可由 /sys/fs/cgroup/memory/memory.limit_in_bytes
文件从容器内部读取。
6.3.4. 了解 OOM 终止策略
如果容器中所有进程的内存总用量超过内存限制,或者在严重的节点内存耗尽情形下,OpenShift Container Platform 可以终止容器中的某个进程。
当一个进程被 OOM 终止时,这有可能会造成容器立即退出,但也不一定。如果容器 PID 1 进程收到 SIGKILL,则容器会立即退出。否则,容器行为将取决于其他进程的行为。
例如,某个容器进程以代码 137 退出,这表示它收到了 SIGKILL 信号。
如果容器没有立即退出,则能够检测到 OOM 终止,如下所示:
使用远程 shell 访问 pod:
# oc rsh test
/sys/fs/cgroup/memory/memory.oom_control
中的 oom_kill 计数器递增$ grep '^oom_kill ' /sys/fs/cgroup/memory/memory.oom_control oom_kill 0 $ sed -e '' </dev/zero # provoke an OOM kill Killed $ echo $? 137 $ grep '^oom_kill ' /sys/fs/cgroup/memory/memory.oom_control oom_kill 1
如果 pod 中的一个或多个进程遭遇 OOM 终止,那么当 pod 随后退出时(不论是否立即发生),它都将会具有原因为 OOMKilled 的 Failed 阶段。OOM 终止的 pod 可以根据 restartPolicy
的值来重新启动。如果不重启,ReplicationController 等控制器会看到 pod 的失败状态,并创建一个新 pod 来取代旧 pod。
如果不重启,pod 状态如下:
$ oc get pod test NAME READY STATUS RESTARTS AGE test 0/1 OOMKilled 0 1m $ oc get pod test -o yaml ... status: containerStatuses: - name: test ready: false restartCount: 0 state: terminated: exitCode: 137 reason: OOMKilled phase: Failed
如果重启,其状态如下:
$ oc get pod test NAME READY STATUS RESTARTS AGE test 1/1 Running 1 1m $ oc get pod test -o yaml ... status: containerStatuses: - name: test ready: true restartCount: 1 lastState: terminated: exitCode: 137 reason: OOMKilled state: running: phase: Running
6.3.5. 了解 pod 驱除
OpenShift Container Platform 可在节点内存耗尽时从节点上驱除 pod。根据内存耗尽的程度,驱除可能是安全操作,但也不一定。安全驱除表示,各个容器的主进程 (PID 1) 收到 SIGTERM 信号,稍等片刻后,如果进程还未退出,则会收到一个 SIGKILL 信号。非安全驱除暗示着各个容器的主进程会立即收到 SIGKILL 信号。
被驱除的 pod 将具有原因为 Evicted 的 Failed 阶段。无论 restartPolicy
的值是什么,该 pod 都不会重启。但是,ReplicationController 等控制器会看到 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