30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

重要な Pod の優先度を設定することでスケジューリングされやすくする

Kubernetes の Pod にはpriorityという属性があり、その名の通り Pod の優先度を示している。
重要な Pod のpriorityを高く設定することで、 Node へのスケジューリングが行われやすくすることができる。
この記事では、priorityの使い方やそれが解決する課題を具体的に見ていく。

この記事の内容は Kubernetes の1.34で動作確認した。

要求しているだけのリソースがない Node には Pod はスケジューリングされない

事前準備としてまずクラスタを用意する。この記事では Amazon EKS を使う。
t3.smallの Node を 1 つだけ用意した。

$ kubectl get nodes
NAME                                           STATUS   ROLES    AGE   VERSION
ip-10-0-1-16.ap-northeast-1.compute.internal   Ready    <none>   62m   v1.34.2-eks-ecaa3a6

$ kubectl topコマンドを使えるようにしたいので、$ kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yamlを実行しておく。

現在の状況を確認してみる。

この Node の使用可能なメモリは約 1.4 Gi 。

$ kubectl describe node ip-10-0-1-16.ap-northeast-1.compute.internal | grep -A5 Allocatable
Allocatable:
  cpu:                1930m
  ephemeral-storage:  18181869946
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             1466568Ki

そのうち 471 Mi が現在使われている。

$ kubectl top nodes
NAME                                           CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-0-1-16.ap-northeast-1.compute.internal   28m          1%     471Mi           32%

この Node に配置されている Pod のresources.requests.memoryの合計値は 340 Mi 。
まだ何のリソースも作成していないのに0ではないのは、Kubernetes によって自動的に作られる Pod があるため。それらの合計が 340 Mi 。

$ kubectl describe node ip-10-0-1-16.ap-northeast-1.compute.internal | grep -A10 "Allocated resources"
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests     Limits
  --------           --------     ------
  cpu                450m (23%)   0 (0%)
  memory             340Mi (23%)  340Mi (23%)
  ephemeral-storage  0 (0%)       0 (0%)
  hugepages-1Gi      0 (0%)       0 (0%)
  hugepages-2Mi      0 (0%)       0 (0%)
Events:              <none>

このクラスタにリソースを作成して、挙動を確認していく。

まず、以下の内容のmanifest.yamlを作成し、$ kubectl apply -f manifest.yamlする。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: resource-filler
spec:
  replicas: 2
  selector:
    matchLabels:
      app: filler
  template:
    metadata:
      labels:
        app: filler
    spec:
      containers:
        - name: nginx
          image: docker.io/library/nginx:1.27
          resources:
            requests:
              memory: "500Mi"
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: important-cronjob
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: work
              image: docker.io/library/busybox:1.36
              command:
                ["sh", "-c", "for i in $(seq 1 60); do date; sleep 1; done"]
              resources:
                requests:
                  memory: "300Mi"

Nginx を image とした Pod が 2 つ作られ、それとは別にimportant-cronjobという名前の CronJob も作られた。
この CronJob は 5 分毎に Job を作成する。作成された Job は 1 分間、現在時刻の出力を毎秒行う。

$ kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
resource-filler-6767d9ddc6-gdhsz   1/1     Running   0          5s
resource-filler-6767d9ddc6-mdm8c   1/1     Running   0          5s

$ kubectl get cronjobs
NAME                SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
important-cronjob   */5 * * * *   <none>     False     0        <none>          11s

ここで重要なのは、 Nginx のresources.requests.memory500Miであること。
Pod が 2 つあるため、resources.requests.memoryの合計値は1000Mi増えて1340Miになった。

$ kubectl describe node ip-10-0-1-16.ap-northeast-1.compute.internal | grep -A10 "Allocated resources"
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests      Limits
  --------           --------      ------
  cpu                450m (23%)    0 (0%)
  memory             1340Mi (93%)  340Mi (23%)
  ephemeral-storage  0 (0%)        0 (0%)
  hugepages-1Gi      0 (0%)        0 (0%)
  hugepages-2Mi      0 (0%)        0 (0%)
Events:              <none>

そして、 Job を実行するための Pod のresources.requests.memory300Miである。
しかし Node の使用可能メモリは約 1.4 Gi なので、もう300Miも残っていない。
このようなときにどうなるのかというと、 Job を実行するための Pod を Node にスケジューリングすることができない。

数分待つとimportant-cronjob-29438710-sh5q4という名前の Pod が作られるが、Pendingとなっている。

$ kubectl get jobs
NAME                         STATUS    COMPLETIONS   DURATION   AGE
important-cronjob-29438710   Running   0/1           3m2s       3m2s

$ kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
important-cronjob-29438710-sh5q4   0/1     Pending   0          3m5s
resource-filler-6767d9ddc6-gdhsz   1/1     Running   0          7m16s
resource-filler-6767d9ddc6-mdm8c   1/1     Running   0          7m16s

調べてみると、メモリ不足(Insufficient memory)のため Node に配置できない、というイベントが発生している。

$ kubectl get events --field-selector involvedObject.name=important-cronjob-29438710-sh5q4
LAST SEEN   TYPE      REASON             OBJECT                                 MESSAGE
4m31s       Warning   FailedScheduling   pod/important-cronjob-29438710-sh5q4   0/1 nodes are available: 1 Insufficient memory. no new claims to deallocate, preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.

スケジューリングできるかどうかはresources.requestsで決まることに注意する。
実際のリソース使用量ではない。

実際のメモリ使用量を確認してみると484Miであり、まだまだ余裕がある。

$ kubectl top nodes
NAME                                           CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-0-1-16.ap-northeast-1.compute.internal   29m          1%     484Mi           33%

しかしresources.requests.memoryの合計は1340Miであるため、resources.requests.memory300Miの Pod はスケジューリングできない。

priority に基づいた Pod の退避とスケジューリング

上記のケースにおいて Job の Pod のpriorityを Nginx の Pod のそれより高くした場合、 Nginx の Pod が Node から退避させられることでresources.requests.memoryの空きが生まれ、 Job の Pod をスケジューリングさせることができる。

先程見た 3 つの Pod のpriorityは、デフォルト値の0

$ kubectl get pod important-cronjob-29438710-sh5q4 -o jsonpath='{.spec.priority}'; echo
0

$ kubectl get pod resource-filler-6767d9ddc6-gdhsz -o jsonpath='{.spec.priority}'; echo
0

$ kubectl get pod resource-filler-6767d9ddc6-mdm8c -o jsonpath='{.spec.priority}'; echo
0

priorityは数値が大きいほど優先度が高い。
なので Job の Pod のpriority1以上にすれば、スケジューリングされるようになるはずである。

$ kubectl delete -f manifest.yamlを実行し、一旦現状のリソースを全て消す。

manifest.yamlに以下の変更を加える。

@@ -1,3 +1,10 @@
+apiVersion: scheduling.k8s.io/v1
+kind: PriorityClass
+metadata:
+  name: middle-priority
+value: 10
+globalDefault: false
+---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
@@ -29,6 +36,7 @@ spec:
     spec:
       template:
         spec:
+          priorityClassName: middle-priority
           restartPolicy: Never
           containers:
             - name: work

priorityを設定したい場合、マニフェストファイルに直接priorityを書くのではなく、 PriorityClass というリソースを通して設定する。
上記の例ではmiddle-priorityという名前の PriorityClass を作っている。valueが優先度。globalDefaultについては後述する。
そして Pod のpriorityClassNameフィールドに、その Pod に適用する PriorityClass の名前を書く。

この状態で$ kubectl apply -f manifest.yamlする。

PriorityClass 一覧を見てみると、middle-priorityという PriorityClass が作られている。それ以外の PriorityClass は Kubernetes が自動的に作成したもの。

$ kubectl get pc
NAME                      VALUE        GLOBAL-DEFAULT   AGE     PREEMPTIONPOLICY
middle-priority           10           false            2m30s   PreemptLowerPriority
system-cluster-critical   2000000000   false            137m    PreemptLowerPriority
system-node-critical      2000001000   false            137m    PreemptLowerPriority

Nginx の Pod が 2 つあるが、どちらもpriorityClassNameフィールドは存在せず、priority0

$ kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
resource-filler-6767d9ddc6-255gf   1/1     Running   0          16s
resource-filler-6767d9ddc6-qqsks   1/1     Running   0          16s

$ kubectl get pod resource-filler-6767d9ddc6-255gf -o jsonpath='{.spec.priorityClassName}'; echo


$ kubectl get pod resource-filler-6767d9ddc6-255gf -o jsonpath='{.spec.priority}'; echo
0

$ kubectl get pod resource-filler-6767d9ddc6-qqsks -o jsonpath='{.spec.priorityClassName}'; echo


$ kubectl get pod resource-filler-6767d9ddc6-qqsks -o jsonpath='{.spec.priority}'; echo
0

数分経つとimportant-cronjob-29438765-bk9b7が作られるが、priorityClassNameが設定されており、priority10になっている。

$ kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
important-cronjob-29438765-bk9b7   1/1     Running   0          12s
resource-filler-6767d9ddc6-255gf   1/1     Running   0          3m57s
resource-filler-6767d9ddc6-txmm2   0/1     Pending   0          12s

$ kubectl get pod important-cronjob-29438765-bk9b7 -o jsonpath='{.spec.priorityClassName}'; echo
middle-priority

$ kubectl get pod important-cronjob-29438765-bk9b7 -o jsonpath='{.spec.priority}'; echo
10

そして、important-cronjob-29438765-bk9b7Runningである。つまり Node にスケジューリングされている。
また、よく見ると Nginx の Pod が先程とは変わっている。

before after
resource-filler-6767d9ddc6-255gf
✅️ Running
resource-filler-6767d9ddc6-255gf
✅️ Running
resource-filler-6767d9ddc6-qqsks
✅️ Running
resource-filler-6767d9ddc6-txmm2
🟡 Pending

resource-filler-6767d9ddc6-qqsksが消えてその代わりにresource-filler-6767d9ddc6-txmm2が作られたが、resource-filler-6767d9ddc6-txmm2Pendingになっている。

これは、相対的にpriorityの高いimportant-cronjob-29438765-bk9b7をスケジューリングするために、resource-filler-6767d9ddc6-qqsksが退避させられたため。

$ kubectl get events --field-selector reason=Preempted
LAST SEEN   TYPE     REASON      OBJECT                                 MESSAGE
102s        Normal   Preempted   pod/resource-filler-6767d9ddc6-qqsks   Preempted by pod 52bbafdb-730a-4244-ac2c-178c7d2b71c0 on node ip-10-0-1-16.ap-northeast-1.compute.internal

Deployment のreplicas2なのですぐにresource-filler-6767d9ddc6-txmm2が作られたが、スケジューリングできる Node がなく、自身よりpriorityが低い Pod もないため、Pendingになっている。

important-cronjob-29438765-bk9b7が終了するとresources.requests.memoryに空きが生まれるため、resource-filler-6767d9ddc6-txmm2がスケジューリングされてRunningになる。

$ kubectl get pods
NAME                               READY   STATUS      RESTARTS   AGE
important-cronjob-29438765-bk9b7   0/1     Completed   0          73s
resource-filler-6767d9ddc6-255gf   1/1     Running     0          4m58s
resource-filler-6767d9ddc6-txmm2   1/1     Running     0          73s

globalDefault の挙動

PriorityClass のglobalDefaulttrueにすると、priorityClassNameを指定していない Pod のpriorityClassNameがこの PriorityClass になる。

これを確認するため、まず$ kubectl delete -f manifest.yamlを実行して現状のリソースを全て消す。
その後manifest.yamlに以下を追記して$ kubectl apply -f manifest.yamlする。

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 100
globalDefault: true

そうすると、 Nginx の Pod のpriorityClassNamehigh-priorityになる。

$ kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
resource-filler-6767d9ddc6-4frjm   1/1     Running   0          5s
resource-filler-6767d9ddc6-5f28m   1/1     Running   0          5s

$ kubectl get pod resource-filler-6767d9ddc6-4frjm -o jsonpath='{.spec.priorityClassName}'; echo
high-priority

$ kubectl get pod resource-filler-6767d9ddc6-4frjm -o jsonpath='{.spec.priority}'; echo
100

$ kubectl get pod resource-filler-6767d9ddc6-5f28m -o jsonpath='{.spec.priorityClassName}'; echo
high-priority

$ kubectl get pod resource-filler-6767d9ddc6-5f28m -o jsonpath='{.spec.priority}'; echo
100

Nginx の Pod のpriority100)が Job の Pod のそれ(10)よりも大きくなったので、リソースが足りなくなっても Nginx の Pod が退避させられることはなくなり、 Job の Pod がPendingのままになる。

$ kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
important-cronjob-29438780-q8kkb   0/1     Pending   0          29s
resource-filler-6767d9ddc6-4frjm   1/1     Running   0          89s
resource-filler-6767d9ddc6-5f28m   1/1     Running   0          89s

$ kubectl get events --field-selector involvedObject.name=important-cronjob-29438780-q8kkb
LAST SEEN   TYPE      REASON             OBJECT                                 MESSAGE
78s         Warning   FailedScheduling   pod/important-cronjob-29438780-q8kkb   0/1 nodes are available: 1 Insufficient memory. no new claims to deallocate, preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.