第41章 アプリケーションメモリーのサイジング

41.1. 概要

ここでは、アプリケーション開発者が OpenShift Container Platform を使用して以下を実行する際に役立つ情報を提供します。

  1. コンテナー化されたアプリケーションコンポーネントのメモリーおよびリスク要件を判別し、それらの要件を満たすようコンテナーメモリーパラメーターを設定する
  2. コンテナー化されたアプリケーションランタイム (OpenJDK など) を、設定されたコンテナーメモリーパラメーターに基づいて最適に実行されるよう設定する
  3. コンテナーでの実行に関連するメモリー関連のエラー状態を診断し、これを解決する

41.2. 背景情報

まず OpenShift Container Platform によるコンピュートリソースの管理方法の概要をよく読んでから次の手順に進むことをお勧めします。

アプリケーションメモリーのサイジングについては、以下が主要なポイントになります。

  • それぞれの種類のリソース (メモリー、cpu、ストレージ) については、OpenShift Container Platform ではオプションの 要求 および 制限 の値を Pod の各コンテナーに設定できます。ここでは、メモリー要求とメモリー制限のみに言及します。
  • メモリー要求

    • メモリー要求値は、指定される場合 OpenShift Container Platform スケジューラーに影響を与えます。スケジューラーは、コンテナーのノードへのスケジュール時にメモリー要求を考慮し、コンテナーの使用のために選択されたノードで要求されたメモリーをフェンスオフします。
    • ノードのメモリーが使い切られると、OpenShift Container Platform はメモリー使用がメモリー要求を最も超過しているコンテナーのエビクションを優先します。メモリー消費の深刻な状況が生じる場合、ノードの OOM killer は同様のメトリクスに基づいてコンテナーでプロセスを選択し、これを強制終了する場合があります。
  • メモリー制限

    • メモリー制限値が指定されている場合、コンテナーのすべてのプロセスに割り当て可能なメモリーにハード制限を指定します。
    • コンテナーのすべてのプロセスで割り当てられるメモリーがメモリー制限を超過する場合、ノードの OOM killer はコンテナーのプロセスをすぐに選択し、これを強制終了します。
    • メモリー要求とメモリー制限の両方が指定される場合、メモリー制限の値はメモリー要求の値よりも大きいか、またはこれと等しくなければなりません。
  • 管理

    • クラスター管理者はメモリーの要求値、制限値、これらの両方に対してクォータを割り当てるか、いずれにも割り当てないようにすることができます。
    • クラスター管理者はメモリーの要求値、制限値またはこれらの両方についてデフォルト値を割り当てることも、それらのいずれにもデフォルト値を割り当てないようにすることもできます。
    • クラスター管理者は、クラスターのオーバーコミットを管理するために開発者が指定するメモリー要求の値を上書きできます。これは OpenShift Online などで行われます。

41.3. ストラテジー

OpenShift Container Platform でアプリケーションメモリーをサイジングする手順は以下の通りです。

  1. 予想されるコンテナーのメモリー使用の判別

    必要時に予想される平均およびピーク時のコンテナーのメモリー使用を判別します (例: 別の負荷テストを実行)。コンテナーで並行して実行されている可能性のあるすべてのプロセスを必ず考慮に入れるようにしてください。たとえば、メインのアプリケーションは付属スクリプトを生成しているかどうかを確認します。

  2. リスク選好 (risk appetite) の判別

    エビクションのリスク選好を判別します。リスク選好のレベルが低い場合、コンテナーは予想されるピーク時の使用量と安全マージンのパーセンテージに応じてメモリーを要求します。リスク選好が高くなる場合、予想される平均の使用量に応じてメモリーを要求することがより適切な場合があります。

  3. コンテナーのメモリー要求の設定

    上記に基づいてコンテナーのメモリー要求を設定します。要求がアプリケーションのメモリー使用をより正確に表示することが望ましいと言えます。要求が高すぎると、クラスターおよびクォータの使用が非効率になり、要求が低すぎると、アプリケーションのエビクションの可能性が高くなります。

  4. コンテナーのメモリー制限の設定 (必要な場合)

    必要時にコンテナーのメモリー制限を設定します。制限を設定すると、コンテナーのすべてのプロセスのメモリー使用量の合計が制限を超える場合にコンテナーのプロセスがすぐに強制終了されるため、予期しないメモリー使用の超過を早期に明確にする (「fail fast (早く失敗する)」) と共に、プロセスをすぐに中止するという 2 つの利点があります。

    一部の OpenShift Container Platform クラスターでは制限値を設定する必要があります。制限に基づいて要求を上書きする場合があります。また、一部のアプリケーションイメージは、要求値よりも検出が簡単なことから設定される制限値に依存します。

    メモリー制限が設定される場合、これは予想されるピーク時のコンテナーのメモリー使用量と安全マージンのパーセンテージよりも低い値に設定することはできません。

  5. アプリケーションが調整されていることの確認

    適切な場合は、設定される要求および制限値に関連してアプリケーションが調整されていることを確認します。この手順は、JVM などのメモリーをプールするアプリケーションにおいてとくに当てはまります。残りの部分では、これについて説明します。

41.4. OpenShift Container Platform での OpenJDK のサイジング

デフォルトの OpenJDK 設定はコンテナー化された環境では機能しません。コンテナーで OpenJDK を実行する場合は常に追加の Java メモリー設定を指定することがルールとなっているためです。

JVM のメモリーレイアウトは複雑で、バージョンに依存しており、本書ではこれについて詳細には説明しません。ただし、コンテナーで OpenJDK を実行する際のスタートにあたって少なくとも以下の 3 つのメモリー関連のタスクが主なタスクになります。

  1. JVM 最大ヒープサイズを上書きする。
  2. JVM が未使用メモリーをオペレーティングシステムに解放するよう促す (適切な場合)。
  3. コンテナー内のすべての JVM プロセスが適切に設定されていることを確認する。

コンテナーでの実行に向けて JVM ワークロードを最適に調整する方法については本書では扱いませんが、これには複数の JVM オプションを追加で設定することが必要になる場合があります。

41.4.1. JVM 最大ヒープサイズの上書き

数多くの Java ワークロードにおいて、JVM ヒープはメモリーの最大かつ単一のコンシューマーです。現時点で OpenJDK は、OpenJDK がコンテナー内で実行されているかにかかわらず、ヒープに使用されるコンピュートノードのメモリーの最大 1/4 (1/-XX:MaxRAMFraction) を許可するようデフォルトで設定されます。そのため、コンテナーのメモリー制限も設定されている場合には、この動作をオーバーライドすることが 必須 です。

上記を実行する方法として、2 つ以上の方法を使用できます。

  1. コンテナーのメモリー制限が設定されており、JVM で実験的なオプションがサポートされている場合には、-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap を設定します。

    これにより、-XX:MaxRAM がコンテナーのメモリー制限に設定され、最大ヒープサイズ (-XX:MaxHeapSize / -Xmx) が 1/-XX:MaxRAMFraction に設定されます (デフォルトでは 1/4)。

  2. -XX:MaxRAM-XX:MaxHeapSize または -Xmx のいずれかを直接上書きします。

    このオプションには、値のハードコーディングが必要になりますが、安全マージンを計算できるという利点があります。

41.4.2. JVM が未使用メモリーをオペレーティングシステムに解放するよう促す

デフォルトで、OpenJDK は未使用メモリーをオペレーティングシステムに積極的に返しません。これは多くのコンテナー化された Java ワークロードには適していますが、例外として、コンテナー内に JVM と共存する追加のアクティブなプロセスがあるワークロードの場合を考慮する必要があります。それらの追加のプロセスはネイティブのプロセスである場合や追加の JVM の場合、またはこれら 2 つの組み合わせである場合もあります。

OpenShift Container Platform Jenkins maven スレーブイメージ は以下の JVM 引数を使用して JVM に未使用メモリーをオペレーティングシステムに解放するよう促します: -XX:+UseParallelGC -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 これらの引数は、割り当てられたメモリーが使用中のメモリー (-XX:MaxHeapFreeRatio) の 110% を超え、ガベージコレクター (-XX:GCTimeRatio) での CPU 時間の 20% を使用する場合は常にヒープメモリーをオペレーティングシステムに返すことが意図されています。アプリケーションのヒープ割り当てが初期のヒープ割り当て (-XX:InitialHeapSize / -Xms で上書きされる) を下回ることはありません。詳細情報については、「Tuning Java’s footprint in OpenShift (Part 1)」、「Tuning Java’s footprint in OpenShift (Part 2)」、および「OpenJDK and Containers」を参照してください。

41.4.3. コンテナー内のすべての JVM プロセスが適切に設定されていることを確認する

複数の JVM が同じコンテナーで実行される場合、それらすべてが適切に設定されていることを確認する必要があります。多くのワークロードでは、それぞれの JVM に memory budget のパーセンテージを付与する必要があります。これにより大きな安全マージンが残される場合があります。

多くの Java ツールは JVM を設定するために各種の異なる環境変数 (JAVA_OPTSGRADLE_OPTSMAVEN_OPTS など) を使用します。適切な設定が適切な JVM に渡されていることを確認するのが容易でない場合もあります。

JAVA_TOOL_OPTIONS 環境変数は常に OpenJDK によって使用され、JAVA_TOOL_OPTIONS で指定される値は JVM コマンドラインで指定される他のオプションによって上書きされます。デフォルトで、OpenShift Container Platform Jenkins maven スレーブイメージJAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Dsun.zip.disableMemoryMapping=true" を設定してこれらのオプションがスレーブイメージで実行されるすべての JVM ワークロードに対してデフォルトで使用されるようにします。これは、追加のオプションが不要になることを保証する訳ではありませんが、開始時には役立ちます。

41.5. Pod 内でのメモリー要求および制限の検索

Pod 内からメモリー要求および制限を動的に検出するアプリケーションは Downward API を使用する必要があります。以下のスニペットはこれがどのように実行されるかを示しています。

apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  containers:
  - name: test
    image: fedora:latest
    command:
    - sleep
    - "3600"
    env:
    - name: MEMORY_REQUEST
      valueFrom:
        resourceFieldRef:
          containerName: test
          resource: requests.memory
    - name: MEMORY_LIMIT
      valueFrom:
        resourceFieldRef:
          containerName: test
          resource: limits.memory
    resources:
      requests:
        memory: 384Mi
      limits:
        memory: 512Mi
# oc rsh test
$ env | grep MEMORY | sort
MEMORY_LIMIT=536870912
MEMORY_REQUEST=402653184

メモリー制限値は、/sys/fs/cgroup/memory/memory.limit_in_bytes ファイルによってコンテナー内から読み取ることもできます。

41.6. OOM による強制終了の診断

OpenShift Container Platform は、コンテナーのすべてのプロセスのメモリー使用量の合計がメモリー制限を超えるか、またはノードのメモリーを使い切られるなどの深刻な状態が生じる場合にコンテナーのプロセスを強制終了する場合があります。

プロセスが OOM によって強制終了される場合、コンテナーがすぐに終了する場合もあれば、終了しない場合もあります。コンテナーの PID 1 プロセスが SIGKILL を受信する場合、コンテナーはすぐに終了します。それ以外の場合、コンテナーの動作は他のプロセスの動作に依存します。

コンテナーがすぐに終了しない場合、OOM による強制終了は以下のように検出できます。

  1. コンテナーのプロセスは SIGKILL シグナルを受信したことを示すコード 137 で終了する。
  2. /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 の 1 つ以上のプロセスが OOM で強制終了され、Pod がこれに続いて終了する場合 (即時であるかどうかは問わない)、フェーズは Failed、理由は OOMKilled になります。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

41.7. エビクトされた Pod の診断

OpenShift Container Platform は、ノードのメモリーが使い切られとそのノードから Pod をエビクトする場合があります。メモリー消費の度合いによって、エビクションは正常に行われる場合もあれば、そうでない場合もあります。正常なエビクションは、各コンテナーのメインプロセス (PID 1) が SIGTERM シグナルを受信してから、プロセスがすでに終了していない場合は後になって SIGKILL シグナルを受信することを意味します。正常ではないエビクションは各コンテナーのメインプロセスが SIGKILL シグナルを即時に受信することを示します。

エビクトされた Pod のフェーズは Failed に、理由 は Evicted になります。この場合、restartPolicy の値に関係なく再起動されません。ただし、 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