KEELチームの相原です。
もう随分と前のことになるのですが、以前我々が管理するKubernetesクラスタであるKEELで起きた障害のふりかえりについて書きます。
今回起きた障害
既にサービスインしているKubernetesクラスタ に対して globalDefault: true
なPriorityClassをデプロイした 影響で、まだPriorityClassが設定されていない priority: 0
なPodが一斉にPreemptされ一時的にサービスに障害が起きた。
PriorityClassとはなにか
詳しい説明は公式ドキュメントのPod Priority and Preemptionに譲りますが、初めにPriorityClassについて軽く説明しておきます。
PriorityClassとは以下のように定義するKubernetes組み込みのリソースで、KubernetesがPodをスケジューリングする時の優先度を表します。
apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high value: 1000
Pod側の priorityClassName
で以下のように指定することで有効になり、このPodのスケジューリング時にNodeに十分なリソースがなければ priority
の値が 1000
より小さいPodを対象のNodeから選択してPreemptするというものです。
apiVersion: v1 kind: Pod metadata: name: example spec: priorityClassName: high ...
Preemptionとはkube-schedulerによって実行される単なるPodの終了のことで、通常のPodの終了と同じように preStop
の実行 -> SIGTERMの送信 -> terminationGracePeriodSeconds
の秒数経ってPodが終了しなければSIGKILLを送信する といったフローで実行されます。
PriorityClassに preemptionPolicy: Never
と書けばPreemptionを無効にできますが、この場合リソースに空きが出た時に優先的にスケジューリングされるというだけなので当然待ち時間は長くなります。
globalDefault: true
と書くことで priorityClassName
を指定していないPodに自動的にPriorityClassを設定することも可能です。
デフォルトではPodの priority
の値は0となるため、基準となる priority
を globalDefault: true
として作成しておくとよいでしょう。
priority
はKubernetesのAdmission Controllerによって設定されるため、次回のPodデプロイ時から有効になります。
後述しますがこれが今回の障害の原因となりました。
ちなみに、このPriorityClassにはデフォルトで system-cluster-critical
と system-node-critical
の2つのPriorityClassが存在していて、これらはKubernetes 1.13で廃止された scheduler.alpha.kubernetes.io/critical-pod
の代わりとして用意されるようになったものです。
絶対にPreemptされたくないクラスタの重要なコンポーネントであるDaemonSetに付与することが想定されていて、 kube-system
以外では利用することができせん。
経緯
我々が管理するKubernetesクラスタであるKEELではPodのほとんどをAWSのスポットインスタンス上で稼働させるべく、スポットフリートの導入や、スポットインスタンス売り切れ時のフォールバック機能やスポットインスタンス上のPodの安全な終了処理の開発などを進めてきました。
フォールバック機能は開発したものの依然スケジューリング待ちは予想できたため、待ち時間をできるだけ短くするべくスポットインスタンス上のPodの中でもスケジューリングの優先度をつける必要が出てきました。
そう、PriorityClassです。
そこで我々は以下の3つのPriorityClassを用意して適用しました。
- PreemptされてもよいPodに付与する
value: 100
の low PriorityClass - 標準的なPodに付与する
globalDefault: true
かつvalue: 1000
の medium PriorityClass - 優先度の高いPodに付与する
value: 10000
の high PriorityClass
適用後、無事にlow PriorityClassを持ったPodのPreemptionを確認することができ、これで更に安定してスポットインスタンスを利用できるようになりました。
と、思ったのもつかの間、突如チームに対して大量のアラートが届きます。
アラートの発生元は外形監視で、どうもクラスタ外からのリクエストのエラーレートが急上昇していそうです。
調査をしてみると、クラスタ外からのリクエストを受け付けるリバースプロキシである istio-ingressgateway
をはじめとして多くのPodに再起動した形跡が見られました。
クラスタ内のリクエストはKEELで利用しているService MeshであるIstioによってリトライされていたため大きな影響はありませんでしたが、クラスタ外からのリクエストを受け付けるリバースプロキシの台数が減ったことによって外からのエラーレートが急上昇していました。
何が起きたか
この障害は globalDefault: true
によって引き起こされました。
前述の通り、デフォルトのPodの priority
の値は0です。
PriorityClassを導入する前にデプロイされたPodにはすべて priority: 0
が設定されています。
priority
はAdmission Controllerによって設定されるため、PriorityClassを適用しても既存のPodは引き続き priority: 0
のままです。
そこに今回 value: 1000
なPriorityClassに globalDefault: true
を設定した影響で、十分なリソースがないNodeに新たに priority: 1000
なPodがデプロイされ、PriorityClass導入以前のPodが一斉にPreemptされました。
なぜPodDisruptionBudgetは機能しなかったのか
KubernetesにはEvictionというPreemptionに似た挙動が存在します。
EvictionはNode-pressureが起きた際やTaintによってPodをNodeから退避させるものですが、PodDisruptionBudgetによって一度に退避させるPod数に制限をかけることができます。
KEEL上にデプロイされているPodにはほぼすべてPodDisruptionBudgetが設定されているため、こういった現象は防がれていたはずです。
しかし、Preemptionのドキュメントには以下のようにあります。
Kubernetes supports PDB when preempting Pods, but respecting PDB is best effort. The scheduler tries to find victims whose PDB are not violated by preemption, but if no such victims are found, preemption will still happen, and lower priority Pods will be removed despite their PDBs being violated.
なるべくPodDisruptionBudgetの通りにPreemptしようとするが、完全に保証されるわけではありません。
そのため、PodDisruptionBudgetを設定しているにも関わらず今回のような障害が起きてしまいました。
どうすればよかったか
まず第一に、稼働しているクラスタに対してPriorityClassを設定することには慎重にならなければなりません。
全てのPodにはデフォルトで priority: 0
が設定されているため、PriorityClassの設定によって意図しないPreemptionが発生してしまう可能性があります。
globalDefault: true
を設定するならなおさらで、以降デプロイされる全てのPodには priority
が設定されるため、これが0を超えている場合PriorityClass導入以前の全てのPodがPreemptionの対象となってしまいます。
priority
の取りうる数値は n < 2000000000
と広いため、つい幅を持ってPriorityClassを作りたくなってしまいますが、恐らくそれほど細かく priority
を設定することは少ないため、KEELでは以下のようなPriorityClassの定義に落ち着きました。
apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: low value: -10 --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: low-nonpreempting value: -10 preemptionPolicy: Never --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: medium value: 0 globalDefault: true --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: medium-nonpreempting value: 0 preemptionPolicy: Never --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high value: 10 --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-nonpreempting value: 10 preemptionPolicy: Never
globalDefault: true
なPriorityClassの value
を0にしているため、万が一後からPriorityClassを設定することになっても既存のPodがPreemptされることはありません。
KEELはStatelessなKubernetesクラスタであるため、大きな変更を伴うバージョンアップの際にはクラスタの稼働系を2つ用意してCanary Deploymentをするので、念のため後からもPriorityClassを設定できるようにしました。
また、kubernetes/autoscalerのcluster-autoscalerを利用している場合、--expendable-pods-priority-cutoff
を下回った priority
は無視されるためデフォルト値の-10を下限としています。
最後に
実は恐ろしいことに、この障害の2週間前にGrafana社での同様の障害のPostmortemをチーム内で共有したばかりでした。
しかもこのPriorityClass導入の作業者である私がです。
Preemptionの挙動も頭に入っていたはずにも関わらずの大失態でしたが、Kubernetesの失敗談を集めたKubernetes Failure StoriesにもPriorityClassにまつわるエピソードは多いため、このエントリでの注意喚起をもって罪滅ぼしとさせてください。
Multi TenancyなKubernetesクラスタでは利用者による意図しないPreemptionを防ぐため、勝手にPriorityClassをデプロイできないよう権限を絞るのはもちろんのこと、ResourceQuotaやOPA Gatekeeperで利用できるPriorityClassを細かくコントロールする必要があることにもご注意ください。