LIFULL Creators Blog

「株式会社LIFULL(ライフル)」の社員によるブログです。

Kubernetesクラスタで起きたPriorityClass適用による障害

KEELチームの相原です。

もう随分と前のことになるのですが、以前我々が管理するKubernetesクラスタであるKEELで起きた障害のふりかえりについて書きます。

今回起きた障害

既にサービスインしているKubernetesクラスタ に対して globalDefault: true なPriorityClassをデプロイした 影響で、まだPriorityClassが設定されていない priority: 0 なPodが一斉にPreemptされ一時的にサービスに障害が起きた。

PriorityClassとはなにか

詳しい説明は公式ドキュメントのPod Priority and Preemptionに譲りますが、初めにPriorityClassについて軽く説明しておきます。

kubernetes.io

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となるため、基準となる priorityglobalDefault: true として作成しておくとよいでしょう。

priority はKubernetesのAdmission Controllerによって設定されるため、次回のPodデプロイ時から有効になります。 後述しますがこれが今回の障害の原因となりました。

ちなみに、このPriorityClassにはデフォルトで system-cluster-criticalsystem-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.

kubernetes.io

なるべく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をチーム内で共有したばかりでした。

grafana.com

しかもこのPriorityClass導入の作業者である私がです。

Preemptionの挙動も頭に入っていたはずにも関わらずの大失態でしたが、Kubernetesの失敗談を集めたKubernetes Failure StoriesにもPriorityClassにまつわるエピソードは多いため、このエントリでの注意喚起をもって罪滅ぼしとさせてください。

k8s.af

Multi TenancyなKubernetesクラスタでは利用者による意図しないPreemptionを防ぐため、勝手にPriorityClassをデプロイできないよう権限を絞るのはもちろんのこと、ResourceQuotaやOPA Gatekeeperで利用できるPriorityClassを細かくコントロールする必要があることにもご注意ください。