同じメッセージ、違う原因 — GPU 障害の分類表
一言でいうと
「GPU Podが起動しない」という報告を受けたら、まずPodオブジェクトがあるかないかで2つに分け、あるならPendingかRunningかでさらに分けたうえで、ノードのstatusとPodのspecを照合して、7つの原因のどれかに絞ります。スケジューラーが残す文は、3つの異なる原因から1文字も違わず同じように出てくるので、メッセージを読むだけでは終わりません。
なぜ必要なのか
GPUクラスターの運用に最もよく来る問い合わせは、いつも同じ一文です。「私のジョブが動きません」。ところが、この一文の裏にある実際の状態は、少なくとも次のくらい違います。
- Podを作ろうとしたのに、
kubectl applyがエラーを出しました。Podは存在しません。 - Podは作られたのに、何時間も
Pendingです。 - Podは
Runningなのに、中でnvidia-smiがデバイスを見つけられません。
この3つは、調べるべき場所がまったく違います。1つ目はアドミッション(クォータ・RuntimeClass・ポリシー)、2つ目はスケジューラー、3つ目はPodのspecとノードのランタイムです。ところが、報告者はこの3つを区別してくれません。そのため、受け取った側が最初の3分で切り分けを決める手順を持っている必要があります。その手順がないと、device pluginのログを1時間調べたあとで、「ああ、Podがそもそも作られていなかった」と知ることになります。
さらに悪い事情があります。Kubernetesのスケジューラーは、失敗の理由を親切に残すほうですが、拡張リソースについては、その親切さが原因を覆い隠す方向に働きます。次の3つの状況で出てくる文が、同じです。
- そのノードに
nvidia.com/gpu自体がアドバタイズされていません(device pluginが落ちているか、そもそもありません)。 capacityには4枚あるのに、allocatableが0です(ドライバーの検証に失敗して、ノードがGPUを出せずにいる状態です)。- アドバタイズも問題なく、枠もあるのに、すでに他のPodがすべて確保しています。
3つの場合とも、Insufficient nvidia.com/gpuです。原因はそれぞれ、「プラグインを復活させる」「ノードを直す」「空きを待つか増やす」と、まったく違うのに、メッセージは区別してくれません。この1点が、このモジュールが存在する理由です。
症状を分ける最初の2つの質問
質問1: Podオブジェクトはあるか
kubectl get pod <이름> -n <네임스페이스> -o json
なければ、スケジューラーはこの件とまったく関係がありません。オブジェクトは、APIサーバーのアドミッションを通過して初めて作られるのですが、GPU関連でアドミッションが止める代表的な2つが、ResourceQuotaの超過と、存在しないRuntimeClassの参照です。どちらも、kubectl applyを実行した人のターミナルにだけエラーが表示され、クラスターには何の痕跡も残りません。報告者がその画面を閉じてしまったなら、同じマニフェストをもう一度適用して、エラーを再現するのが最も速いです。
質問2: spec.nodeNameは空か
kubectl get pod <이름> -n <네임스페이스> -o jsonpath='{.spec.nodeName}'
空なら、スケジューリングの問題です。埋まっているのに問題があるなら、それはノード上の問題(ランタイムハンドラー、ドライバー、リクエストをしていないマニフェスト)です。kubectl get podのSTATUS欄ではなく、このフィールドを見る理由は、Pendingという1つの単語が、「ノードを選べなかった」と「ノードは選んだのにコンテナを作れない」の両方を覆い隠してしまうからです。
原因がどの欄に現れるか
| 原因 | 決定的な証拠がある場所 | 確認するコマンド |
|---|---|---|
| クォータ超過 | Podがない。作成時のエラー文言 | kubectl get resourcequota -n <ns> -o json |
| RuntimeClassがない | Podがない。作成時のエラー文言 | kubectl get runtimeclass |
| リクエストをしていない | spec.containers[].resources.limitsにnvidia.com/gpuがない |
kubectl get pod -o json |
| ラベルの不一致 | spec.nodeSelectorを満たすノードが0個 |
kubectl get nodes --show-labels |
| テイント | 候補ノードのspec.taintsを、Podのtolerationsが許容できない |
kubectl get node -o json |
| リソース未アドバタイズ | 候補ノードのstatus.capacityに、リソース名そのものがない |
kubectl get node -o json |
| allocatableが0 | capacityは正の数なのに、status.allocatableが0 |
同じ出力の別の欄 |
| 空きの枯渇 | allocatableは正の数なのに、そのノードのPodの要求の合計が、その数に達している | kubectl get pods -A -o jsonを合計する |
この表の価値は、「何を見るか」ではなく、「どの順序で見るか」にあります。順序は上から下です。前のものが真なら、後ろのものは見る必要がなく、順序を守らないと、見当違いの結論になります。たとえば、nodeSelectorがどのノードとも合わないPodは、テイントもリソースも検査する対象がそもそもありません。ところが、「テイントを消したのに起動しない」というような対処を先にしてしまうと、問題のないクラスター設定だけが壊れます。
最後の行(空きの枯渇)が、唯一計算を求めます。ノードオブジェクトを見るだけではわからず、そのノードに配置されたPodを集めて、GPUの要求を足す必要があります。ここに1つ落とし穴があります。SucceededやFailedで終わったPodは、枠を確保していないので、合計から外す必要があります。外さないと、実際には空いているノードを「いっぱいだ」と誤って判定します。
拡張リソースだからこその事情
nvidia.com/gpuは、cpuやmemoryとは違い、kubeletが自分では数えられない拡張リソースです。公式ドキュメントは、拡張リソースの規則を2つに固定しています。オーバーコミットをサポートせず、requestsとlimitsが同じで、値は整数でなければなりません。そのため、GPUの要求を読むときは、limitsだけを見れば十分です。逆にいえば、半枚や1.5枚のような値は存在できず、足りない分だけ受け取るような動作もありません。要求は、丸ごと満たされるか、Podが待つか、どちらかです。
そして、拡張リソースは「デバイスがあるか」とは無関係に成立します。ノードのstatusに数字だけが書かれていれば、スケジューラーはそのノードにPodを送ります。逆に、カードが8枚接続されていても、statusに数字がなければ、スケジューラーにとって、そのノードはGPUがないノードです。調査するときは、物理的な事実ではなく、ノードオブジェクトが、スケジューラーにとって唯一の真実であることを、押さえておく必要があります。
現場での姿
第一に、kubectl describeだけを読んで原因を決めつける習慣が、最も高くつきます。先ほど見たように、3つの原因が同じ文を出します。特に夜間に「Insufficient nvidia.com/gpu」を見て、自動でノードを増やすスクリプトを設定していた組織がありましたが、実際の原因は、ノード1台のallocatableが0に落ちたことでした。ノードを増やしても、そのノードはずっと0のままで、コストだけが増えました。
第二に、人が判定すると、順序を守れません。そのため、この判定はツールとして固めておくほうがよいです。入力はPod1つ、出力は原因1単語。こうしたツールがあれば、一次対応者が、報告を受けたその場で「これはクォータです」と答えられ、判定基準が人によって変わりません。このモジュールのラボが作るのは、まさにそのツールです。
第三に、出力は1単語であって初めて役に立ちます。文を出力すると、人が読み直す必要があり、自動化につなげられません。no-gpu-node・allocatable-zero・exhaustedのように、次の行動が1つに決まる語彙を選んでおけば、そのままアラートのルーティングキーになります。原因の語彙を決めることが、実は運用手順を決めることです。
第四に、この環境の正直な限界です。ラボのクラスターには、GPUもdevice pluginもありません。そのため、「デバイスプラグインが落ちた」という状態は、プラグインを落とすのではなく、ノードのstatusにリソースを書かないことで作ります。これは模擬ではなく、同じ状態です。実際の障害でも、スケジューラーが見るのはstatusだけだからです。ただし、「PodはRunningなのに、コンテナの中でデバイスが見えない」の後半は、コンテナが実際には動かないので、確認できません。その分岐は、Podのspecにリソース要求がないという事実までを判定します。
参考ドキュメント
- ノードに拡張リソースをアドバタイズする: https://kubernetes.io/docs/tasks/administer-cluster/extended-resource-node/
- コンテナのリソース管理(拡張リソースの規則): https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
- テイントとトレラレーション: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
- リソースクォータ: https://kubernetes.io/docs/concepts/policy/resource-quotas/
- Podのデバッグ: https://kubernetes.io/docs/tasks/debug/debug-application/debug-pods/
- GPU Operatorのトラブルシューティング: https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/troubleshooting.html
次のラボですること
kwokが起動した本物のスケジューラー上に、7つの状態を1つずつ作ります。アドバタイズが問題ないノード、リソース名そのものがないノード、capacityだけがあってallocatableが0のノードを立てて、そこにそれぞれPodを差し込み、3つの原因が同じ文を出すことを、直接見ます。テイントだけが違うPodとnodeSelectorだけが違うPodを加え、アドミッションで拒否されてオブジェクトすら作られない2つ(RuntimeClass・クォータ)を体験します。最後に、kubectl get -o jsonだけを見て原因を1単語で答える分類ツールを作り、7つのPodすべてにかけてみます。