Calico-nodeより業務podがPreempted障害対応のためBallon podを導入した話

ゼロバンク・デザインファクトリー株式会社(ZDF)のSRE張です、kubernetesのスペシャリストとして日々GKEの世話をしています、出会った問題や経験を共有したいと思い、この記事を書くとこになりました。

# 背景
銀行業務では夜間帯にお金の計算するバッチジョブがいっぱい動いています。
ある日、ジョブでエラーが発生したので、原因を調べたら、calico-nodeによるものだったことが分かりました。

calico-nodeはCNI(Container Network Interface)のプラグインであり、GKEでkubernetesを運用している場合、GoogleCloud側で管理されているものになります。ネットワークインターフェースなので、非常に重要なパーツです。calico-nodeはCPUを使うもので、GKEクラスターのnodeサイズが増えると使用するCPUも増えます。

そのため、calico-node-vertical-autoscalerというものも(GoogleCloud側で)用意されました。calico-node-vertical-autoscalerはクラスターのNodeサイズを見て、動的にcalico-nodeのCPUリクエストを調整しています。Nodeのスケールイン、スケールアウトにつれて頻繫にcalico-nodeを更新(再起動)しています。

calico-node自身のPriorityが高いため(ネットワーク制御パーツのため)、Node上のリソースが足りない場合、他のPriorityが低いPodをPreemptする振る舞いになります。それが今回の障害の原因でした。

夜間帯のバッチジョブによって、GKEクラスターのNode数が増え、Node数が増えた結果calico-node-vertical-autoscalerはcalico-nodeの更新をかけ、リソース競合が発生し、よりPriorityが低いジョブPodが追い出され、ジョブが失敗で終わりました。

# preemptionとは
Podには優先度(Priority)を付けることができます。優先度は、他のPodと比較したPodの重要性を示します。Podをスケジュールできない場合、スケジューラーは、保留中のPodのスケジュールを可能にするために、優先度の低いPodをpreempt (evict) しようとします。

Podの優先度が有効になっている場合、スケジューラーは保留中のPodを優先度に従って順序付けし、保留中のPodは、スケジューリングキュー内で優先度の低い他の保留中のPodよりも前に配置されます。その結果スケジューリング要件が満たされている場合、優先度の高いPodが優先度の低いPodよりも早くスケジューリングします。まだそのPodをスケジュールできない場合、スケジューラーは続行し、他の優先度の低いPodのスケジュールを試行します。

Podが作成されると、キューに入れられ、スケジュールされるまで待機します。スケジューラはキューからPodを選択し、それをNode上でスケジュールしようとします。Podの指定された要件をすべて満たすNodeが見つからない場合、保留中のPodに対してpreemptionロジックがトリガーされます。保留中のPodをPと呼びます。preemptionロジックは、Pよりも優先度の低い1つ以上のPodを削除すると、そのノード上でPをスケジュールできるようになるNodeを見つけようとします。そのようなNodeが見つかった場合、1つ以上の優先度の低いPodがNodeから削除されます。Podがなくなった後、Node上でPをスケジュールできます。

今回の障害の場合、Pはcalico-nodeに該当し、1つ以上の優先度の低いPodが失敗したJob podになりました。

preemptionの詳細は[pod-priority-preemption](https://kubernetes.io/ja/docs/concepts/scheduling-eviction/pod-priority-preemption/)参考してください。
kubernetes側の実装は[こちら](https://github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/framework/preemption/preemption.go
)になります。

# Priority Classについて
リソースの競合が発生するとき、Priorityを見て、低いものを犠牲にし、高いものの稼働を確保する振る舞いになっています。

Priority Classが指定していないPodはデフォルト0になります。
Priority Classのデフォルト設定はこちらになります:
```
preemptionPolicy: PreemptLowerPriority
priority: 0
```

Job podにPriorityが高いPriorityClassを設定することで、Preemptedされることが防げます(代わり別の低優先度のPodがPreempted)
高い優先度の例:
```
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: Job-high-priority
value: 100000
globalDefault: false
description: "この優先度クラスはJob Podに対してのみ使用"
```


# Balloon podについて
名前の通りで、風船みたいな中身が空のPodです。Priorityを一番低く設定することによって、クラスター内リソース競合が発生の時、風船のよう割られ、先にpreemptedされるPodです。それによってリソースに余裕ができ、Podのスケジュールがスムーズにできる仕組みです。
![image-2023-6-13_21-57-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3307746/a4fb1a5f-7973-8249-b388-130255a317ee.png)


Balloon podの設定例:
```
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: balloon-priority
value: -10
preemptionPolicy: Never
globalDefault: false
description: "Balloon pod priority."
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: balloon
spec:
  replicas: 1
  selector:
    matchLabels:
      app: balloon-pod
  template:
    metadata:
      labels:
        app: balloon-pod
    spec:
      priorityClassName: balloon-priority
      terminationGracePeriodSeconds: 0
      containers:
      - name: ubuntu
        image: ubuntu
        command: ["sleep"]
        args: ["infinity"]
        resources:
            # Node capacity * 0.6
            requests:
              cpu: 5
              memory: 37Gi
```



# まとめ
今回は想定外のcalico-node更新により業務のJob podがPreemptedされ失敗しました。その対応策として、Job pod用に高優先度のpriorityClassを設定し、更にPreempted対象になりやすいBalloon podを導入することでJob podを守ることにしました。


# 参考資料

* https://kubernetes.io/ja/docs/concepts/scheduling-eviction/pod-priority-preemption/
* https://github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/framework/preemption/preemption.go
* https://kubernetes.io/ja/docs/concepts/scheduling-eviction/pod-priority-preemption/#%E5%84%AA%E5%85%88%E5%BA%A6%E3%81%A8%E3%83%97%E3%83%AA%E3%82%A8%E3%83%B3%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%BF%E3%81%86%E6%96%B9%E6%B3%95