CPUに余力があってもメモリー要求で配置が制約される

Kubernetesは、コンテナ化されたアプリを管理する基盤です。Podは、1つ以上のコンテナをまとめた実行単位です。

標準スケジューラー(Podをノードに割り当てる機能)は、配置時にCPUとメモリーを別々に判定します。判定基準には、ノード(Podを動かすマシン)上でPodに割り当て可能な容量(allocatable)が用いられます。KubernetesのNode Allocatable文書は、その定義とスケジューラーがallocatableを超えて配置しないことを説明しています。

既存Podと新規Podのrequests(配置判断用のリソース要求量)の合計がallocatableを超える場合、標準スケジューラーは新規Podをそのノードに配置しません。[1]

このとき、標準スケジューラーはSwap(メモリーの一部をストレージへ退避する仕組み)容量を配分可能メモリーとして計算に含めません。[1] そのため、ノードのCPU計算リソースに空きがあっても、memory requestsを満たす枠がallocatableになければ新規Podは配置できません。[1]

この動作は、Kubernetesにおけるリソース指定の役割分担に基づいています。requests は主にスケジューリング時の配置判断に用いられる要求量です。[2] ノードに余力がある環境では、コンテナが設定されたrequestを超えてメモリーを使用することが許される場合があります。[2]

一方で limits は、Podの起動後にcgroup(プロセス群をグループ化して資源を管理するLinuxカーネル機能)を通じて適用される実行時の制約です。[2] コンテナがmemory limitを超えてメモリーを確保しようとすると、LinuxカーネルのOOM killer(メモリー不足時にプロセスを終了させる仕組み)が作動し、コンテナ内のプロセス停止につながる場合があります。[2]

後輩ノードのCPUが余っている場合でも、メモリー要求の合計だけでPodが配置できなくなるのですね。

先輩スケジューラーは設定された要求量の合計値に基づいて空き枠を判定するからだよ。

後輩それならrequestsの設定値を小さくすれば、配置枠を増やせますか?

先輩配置条件と、アプリが必要とする実メモリー量を併せて検証しよう。実使用量がlimitsに達したときのリスクを考慮する必要があるね。

cgroup v2とLimitedSwapで何が変わるのか:カーネルとkubeletの役割分担

Node Swapは、Linuxカーネルが匿名メモリーをストレージへ退避し、物理RAM(主記憶)の空きを確保する仕組みです。

匿名メモリーはファイルを後ろ盾に持たないメモリーで、Linuxカーネルのメモリー管理文書に定義されています。[3] Node Swapは、大きなメモリー領域を持ち、その一部だけを随時参照するワークロード(実行するアプリケーションや処理)で有効に機能します。[3]

ただし、活発なワーキングセット(プロセスが頻繁に参照するメモリー領域)まで退避されると、ストレージからの読み戻しにより長いI/O(データ入出力)待ちが生じます。[3] Node Swapは突発的なメモリー消費に対する緩衝として機能する設計であり、継続的に参照するメモリーには物理RAMを確保する設計が必要です。[3]

KubernetesでPodがSwapを利用するには、ノード環境でcgroup v2(cgroup管理方式の第2版)が有効である必要があります。[5] 運用担当者はノード上で stat -fc %T /sys/fs/cgroup/ を実行し、ファイルシステム種別が cgroup2fs と表示されることを確認します。[5]

kubelet(ノード上でPodの管理を担うエージェント)側の設定も必要です。

failSwapOn: false は、Swap有効ノードでkubeletを起動するための設定です。PodのSwap利用は、memorySwap.swapBehavior の設定で制御します。[5] 既定の memorySwap.swapBehavior: NoSwap ではPodのSwap利用が禁止されるため、運用担当者はLimitedSwapを設定し、Podに制限付きのSwap利用を許可します。[5]

Linuxカーネルが匿名ページをストレージへ退避します。[3] 運用担当者は、kubeletのmemorySwap.swapBehaviorと、ランタイムがコンテナへ設定したmemory.swap.maxを照合します。CRI(Container Runtime Interface)は、kubeletとコンテナランタイムの連携仕様です。[5]

cgroup v2では、memory.swap.max がSwap使用量の上限を定め、memory.swap.current が現在の使用量を示します。[4]

memory.statはcgroup v2のメモリー統計ファイルです。active_anonとinactive_anonは、ページ回収リスト上の匿名メモリーの状態を示します。workingset_refault_anonは、退避された匿名ページの再読み込み回数を示します。[4] 本稿では、これらをI/O待ち時間や応答遅延と併せ、負荷評価の材料として比較する方法を提案します。

後輩kubeletの設定とカーネルの動作は、どう分担しているのですか?

先輩匿名ページを退避するのはLinuxカーネルだよ。kubeletはコンテナごとのSwap上限値をランタイム経由で設定する役割を担当しているね。

後輩回収リストの状態と、アプリの応答時間を併せて確認するんですね。

先輩その通りだね。ページの再読み込み回数やストレージのI/O待ち時間と併せて確認することが大切だよ。

gVisorベンチマークで何が起きたのか:収容数3倍と実行遅延の測定データ

Node Swapの効果と挙動を確認するため、公開されたベンチマーク試験の測定結果を照合します。検証環境には、GKE(Google Kubernetes Engine)上の c4-standard-8 インスタンス(8 vCPU(仮想CPU)、物理RAM 30 GB、allocatable RAM 27 GB)が使用されました。[6]

Swap用ストレージには専用のLocal SSD(ノード直結の半導体ストレージ。NVMe(SSDなどの不揮発性メモリー向けの接続規格)で接続)が構成されています。[6]

試験では、gVisor(アプリケーションをホストから隔離する保護環境)上で、MovieLens 20Mデータセットの500万行を対象に、Pythonのデータ分析ライブラリpandasで、データを項目ごとにまとめるgroupby処理を実行しました。[6]

PodにはCPU request 15m(0.015コア)、memory request 100Mi、memory limit 2Giを設定しています。MiとGiはメモリー容量を示す2進単位で、MiはMiB、GiはGiBに相当します。システム用にCPUを2コア予約し、Podを1.8秒間隔で起動しています。[7]

このベンチマークは、集計処理を終えた親プロセスが終了し、fork(プロセスの複製)された子プロセスが約375 MBのデータを600秒間保持して待機する設計です。[7] そのため測定値は、処理後に待機メモリーを保持するワークロードにおける収容密度を示しています。[7] 全プロセスが計算を続ける条件で、単位時間あたりの処理量(スループット)を測る試験とは前提が異なります。[7]

報告された測定データでは、Swap無効のbaseline(比較基準)環境では80 Pod中80件が成功しました。平均実行時間は2.04秒、P99実行時間(実行時間の99パーセンタイル値)は4.97秒、Swap使用量は0 GBでした。[8] 一方、Swap有効環境では240 Pod中240件が成功し、平均実行時間は3.43秒、P99実行時間は12.83秒、Swap使用量は48.78 GBを記録しました。[8]

条件成功数 / 試行数平均実行時間P99実行時間Swap使用量baseline比密度
baseline(Swap無効)80 / 80 件2.04 秒4.97 秒0 GB基準(1倍)
Swap有効240 / 240 件3.43 秒12.83 秒48.78 GB3倍(+200%)

Swapの有効化により、収容数は80 Podから240 Podへと3倍(baseline比で+200%)に増加しました。[8] 同時に、P99実行時間は4.97秒から12.83秒へと約2.58倍に増加しています。[8] ただし、80並行と240並行ではノード上で競合するPodの総数が異なるため、遅延の増加要因をSwap機構のみに断定することはできません。

なお、baseline環境で100 Podを試行したケースでは Failed (Node NotReady) と記録されています。[9]

クラスタ構築スクリプト deploy_cluster.sh の MAX_PODS_PER_NODE 既定値は256です。[9] この失敗記録はノードの状態変化を示しており、個別のPod単位のOOMKill(メモリー不足によるコンテナの終了)として解釈することは避ける必要があります。[9]

表の80件と240件の成功数は、当該環境で観測された値です。他ワークロードへの適用範囲は、個別に検証してください。[9]

後輩収容密度が3倍になった一方で、P99実行時間も2.5倍以上に伸びていますね。

先輩並行して動くPodの数自体が80から240に増えているから、リソース競合の影響も含まれている可能性があるよ。

後輩処理が終わった後に600秒間メモリーを保持して待機する設計だからこそ、この密度が達成できたのですね。

先輩その通りだね。すべての処理が同時に活発な計算を継続する環境では、異なる結果になり得るよ。

誰に影響するのか:Burstable QoSへの限定と上限計算式をめぐる文書記述の違い

LimitedSwapの適用範囲は、PodのQoS(Quality of Service:資源管理区分)によって明確に制限されます。[10] 上流Kubernetesの仕様では、LimitedSwapを利用できるのはBurstableクラスのPodに限定されます。[10] Burstableは、GuaranteedとBestEffortのどちらにも当てはまらないPodのQoSクラスです。GuaranteedおよびBestEffortの各クラスに属するPodは、Swapの利用対象外となります。[10] コンテナ単位で資源を指定する通常の構成では、Guaranteedは全コンテナのCPU・メモリーに0より大きい値でrequestとlimitを設定し、それぞれの値が一致するクラスです。BestEffortは、CPU・メモリーのrequestとlimitを指定しないクラスです。GKEの公式文書でも、利用対象はBurstable QoSのPodに限定されています。[10]

上流Swap文書は、高優先度Podも対象外と説明しています。実装上の判定は、導入する版で確認します。

v1.34.0の上流コードでは、IsCriticalPodで重要Podと判定したPodにNoSwapを設定します。その判定は、static Pod(kubeletが直接管理するPod)、mirror Pod(static Podの状態を管理基盤から確認するためのPod)、またはシステム用のcritical優先度(2,000,000,000)以上のPodを含みます。

PriorityClass(Podの優先度を定める設定)の公式文書では、system-cluster-criticalの値は2,000,000,000、system-node-criticalは2,000,001,000です。運用担当者は、導入するKubernetes版の除外条件と、Podに設定された優先度を照合します。

Burstableクラスに属するPodであっても、コンテナ単位で memory request == memory limit と設定されている場合は、Swapの利用から除外されます。[10] この設定により、特定のコンテナだけSwap利用をオプトアウトすることが可能です。[10]

QoSクラスコンテナのメモリー設定条件LimitedSwapの利用可否
Burstablememory requestとlimitが異なる・critical Pod以外利用可能(ノード設定と導入版の判定を確認)
critical Podv1.34.0上流実装の追加除外条件利用不可
Burstablememory request == memory limit利用不可(オプトアウト)
Guaranteed全コンテナでrequest == limit利用不可
BestEffortrequest・limitの指定なし利用不可

各コンテナに割り当てられるSwap上限値の計算式については、参照資料間に記述の相違が見られます。[11] 上流Kubernetesの文書では、containerMemoryRequest を分子とした割合計算式が示されています。[11]

Swap上限 = (containerMemoryRequest / nodeTotalMemory) × totalPodsSwapAvailable

上流文書では、containerMemoryRequestはコンテナのメモリー要求量、nodeTotalMemoryはノードの総物理メモリー量です。totalPodsSwapAvailableはPodが利用できるSwapの総量で、システム用に予約する分がある場合はそれを除きます。

これに対し、Google CloudのGKE Standard向け文書では、コンテナのメモリー制限値(limits)とノードの総メモリーに基づいてSwap上限を計算すると説明されています。[11] 提供された公開文書の記述から、両者が同一の計算式を採用しているか断定することは困難です。[11]

後輩GuaranteedクラスのPodでSwapが利用できないのは、どのような理由からですか?

先輩上流仕様ではSwapを使えるPodをBurstableに限定しているよ。GuaranteedとBestEffortは対象外とされているね。

後輩上流とGKEでSwap上限の計算基準がrequestsとlimitsで異なって書かれている点も注意が必要ですね。

先輩文書の記述に差異があるから、実環境で memory.swap.max に設定される実際の値を直接確認する必要があるね。

実環境への導入をどう進めるのか:GKE要件の確認と段階的な検証手順

GKE Standard環境でNode Swapを導入するには、クラスタのバージョンが 1.34.1-gke.1341000 以降であることが求められます。[12] 使用可能なSwapストレージには、boot disk(起動ディスク)、ephemeral local SSD(一時利用型のLocal SSD)、dedicated local SSD(専用のLocal SSD)の3種類が規定されています。[12]

boot diskをSwap領域として使用する場合、構成可能なSwapサイズはディスク総容量の50%が上限となります。[12]

Local SSDを使う場合は、対応するマシンタイプを選びます。必要なリソースを確保する作業(プロビジョニング)も必要です。GKEでは、ファイルシステムを介さずブロックデバイスを直接使うraw block storageは利用できません。[12]

また、GKEでは機密データ保護のため、Swap領域が一時暗号化鍵(ephemeral key)により既定で暗号化されます。[12]

上流Kubernetes文書は、Swap有効ノードで暗号化を推奨しています。本稿では、暗号化の適用状態を確認し、検証用ノードプール(同じ設定を共有するノード群)で段階的に試す方法を提案します。

運用担当者は、導入後にSwap使用量を示す container_swap_usage_bytes や上限値の container_swap_limit_bytes、カーネル側の workingset_refault_anon を継続的に監視します。[4, 5] 本稿では、応答遅延やストレージのI/O待ち、追加コストを収容効率と比較してから本番適用を判断する方法を提案します。

後輩クラスタ全体に一括で適用する前に、どのような手順を踏むべきでしょうか?

先輩専用の検証ノードプールを用意して、同じ処理負荷でSwapの有効時と無効時の遅延を比較するのが堅実だよ。

後輩Local SSDの利用コストやboot diskの容量上限(50%)も考慮に入れる必要がありますね。

先輩その通りだね。ストレージ費用と応答遅延の増加を、向上する収容密度と照らし合わせて判断することが重要だよ。