はじめに
今回はPodデプロイ時のスケジューリングの動作について確認したいと思います。Podをどのノードにデプロイするかを制御するためには、以下の方法があります。それぞれの設定方法と動作について確認していきます。
- nodeSelector
 - Node Affinity
 - node Anti-Affinity
 - Inter-Pod Affinity
 - Inter-Pod Anti-Affinity
 
nodeSelector
nodeSelectorでデプロイするノードを制御するには、ノードに付与されているラベルを利用します。
ラベルの確認と設定
まずは各ノードにデフォルトで設定されているラベルを確認します。
$ kubectl get nodes --show-labels
NAME           STATUS   ROLES    AGE    VERSION   LABELS
k8s-master     Ready    master   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master,kubernetes.io/os=linux,node-role.kubernetes.io/master=
k8s-worker01   Ready    <none>   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-worker01,kubernetes.io/os=linux
k8s-worker02   Ready    <none>   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-worker02,kubernetes.io/os=linux
上記コマンドの他に、kubectl describeコマンドなどでも確認できます。
各Nodeにラベルを設定します。ここでは、「disktype」ラベルを設定して、k8s-worker01は「ssd」、k8s-worker02は「hdd」とします。
実際はどちらも同じディスクですけどね。検証ですので。
$ kubectl label node k8s-worker01 disktype=ssd
node/k8s-worker01 labeled
$ kubectl label node k8s-worker02 disktype=hdd
node/k8s-worker02 labeled
設定したラベルを確認します。
$ kubectl get nodes --show-labels
NAME           STATUS   ROLES    AGE    VERSION   LABELS
k8s-master     Ready    master   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master,kubernetes.io/os=linux,node-role.kubernetes.io/master=
k8s-worker01   Ready    <none>   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-worker01,kubernetes.io/os=linux
k8s-worker02   Ready    <none>   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=hdd,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-worker02,kubernetes.io/os=linux
これだとちょっと見づらいので、disktypeラベルのみ確認します。
$ kubectl get node -L disktype
NAME           STATUS   ROLES    AGE    VERSION   DISKTYPE
k8s-master     Ready    master   104d   v1.17.3
k8s-worker01   Ready    <none>   104d   v1.17.3   ssd
k8s-worker02   Ready    <none>   104d   v1.17.3   hdd
nodeSelecterの動作確認
該当のPodのマニフェストに、nodeSelectorのセクションを追加し、「disktype: ssd」のNodeにデプロイするようにします。今回はDeploymentにしています。
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-dep
spec:
  replicas: 4
  selector:
    matchLabels:
      app: app1
  template:
    metadata:
      labels:
        app: app1
    spec:
      containers:
        - name: nginx
          image: nginx:latest
      nodeSelector:
        disktype: ssd
applyして、PodがデプロイされているNodeを確認します。
$ kubectl apply -f nginx-dep.yaml
deployment.apps/nginx-dep created
$ kubectl get pod -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
nginx-dep-8f97c98cd-b4z88   1/1     Running   0          21s   192.168.79.118   k8s-worker01   <none>           <none>
nginx-dep-8f97c98cd-bfg46   1/1     Running   0          21s   192.168.79.110   k8s-worker01   <none>           <none>
nginx-dep-8f97c98cd-jm22p   1/1     Running   0          21s   192.168.79.126   k8s-worker01   <none>           <none>
nginx-dep-8f97c98cd-vk8nj   1/1     Running   0          21s   192.168.79.117   k8s-worker01   <none>           <none>
4つのPodがすべてssdのNode(=k8s-worker01)にデプロイされていますね。
Node Affinity
Node AffinityもラベルでデプロイするNodeを制御しますが、nodeSelectorよりも細かな設定ができます。
設定
spec.affinityフィールドに設定を記載します。必須のスケジューリングポリシー(requiredDuringSchedulingIgnoredDuringExecution)と優先的に考慮されるスケジューリングポリシー(preferredDuringSchedulingIgnoredDuringExecution)があり、必須のポリシーを満たしたNodeのうち、優先的なポリシーに基づいてデプロイされるNodeが決定します。
apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution: #必須のスケジューリングポリシー
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype #ラベル名
            operator: In #In、NotIn、Exists、DoesNotExist、Gt、Ltを指定可能
            values: #複数設定可能
            - hdd
      preferredDuringSchedulingIgnoredDuringExecution: #優先的に考慮されるスケジューリングポリシー
      - weight: 1 #1~100で指定。条件を満たすノードのWeight値を合計し、一番スコアが高いノードが優先される
        preference:
          matchExpressions:
          - key: kubernetes.io/hostname
            operator: In
            values:
            - k8s-worker01
  containers:
  - name: nginx-na
    image: nginx:latest
今回は以下の図のように必須と優先的なスケジューリングポリシーを別ノードにするようにしています。
operatorフィールドについて
以下の6つを指定できます。
| operator | 概要 | 
|---|---|
| In | keyで指定したラベルが指定したvaluesのいづれか一つ以上に一致する。 | 
| NotIn | keyで指定したラベルが指定したvaluesのいづれにも一致しない。 | 
| Exists | keyで指定したラベルが存在する。valuesは指定しません。 | 
| DoesNotExist | keyで指定したラベルが存在しない。valuesは指定しません。 | 
| Gt | keyで指定したラベルの値(values)よりも大きい。valuesには整数値を指定します。 | 
| Lt | keyで指定したラベルの値(values)よりも小さい。valuesには整数値を指定します。 | 
Node Affinityの動作確認
applyして動作を確認します。
$ kubectl apply -f nodeAffinity.yaml
pod/with-node-affinity created
$ kubectl get pod -o wide
NAME                 READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
with-node-affinity   1/1     Running   0          11s   192.168.69.210   k8s-worker02   <none>           <none>
想定通りk8s-worker02にデプロイされていますね。
requiredDuringSchedulingIgnoredDuringExecutionは満たさないといけませんが、preferredDuringSchedulingIgnoredDuringExecutionは必ずしも満たす必要はありません。
なお、requiredDuringSchedulingIgnoredDuringExecutionを満たすNodeがない場合、preferredDuringSchedulingIgnoredDuringExecutionを満たすNodeがあってもスケジューリングされません。
Podを削除して、k8s-worker02をスケジューリング対象から外してから、再度applyしてみます。
$ kubectl get node
NAME           STATUS                     ROLES    AGE    VERSION
k8s-master     Ready                      master   105d   v1.17.3
k8s-worker01   Ready                      <none>   105d   v1.17.3
k8s-worker02   Ready,SchedulingDisabled   <none>   105d   v1.17.3
$ kubectl apply -f nodeAffinity.yaml
pod/with-node-affinity created
$ kubectl get pod
NAME                 READY   STATUS    RESTARTS   AGE
with-node-affinity   0/1     Pending   0          5s
$ kubectl describe pod with-node-affinity
Name:         with-node-affinity
・・・
Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  66s (x2 over 66s)  default-scheduler  0/3 nodes are available: 1 node(s) were unschedulable, 2 node(s) didn't match node selector.
スケジューリングされず、Pendingで止まってますね。
Node Anti-Affinity
Node Anti-Affinityは、Node Affinityの条件(operator)を否定形にします。
以下の例では、「disktypeがhdd"ではない"Node」(=k8s-worker01)にデプロイされます。
apiVersion: v1
kind: Pod
metadata:
  name: node-anti-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: NotIn
            values:
            - hdd
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: kubernetes.io/hostname
            operator: In
            values:
            - k8s-worker01
  containers:
  - name: nginx-na
    image: nginx:latest
$ kubectl apply -f nodeAntiAffinity.yaml
pod/node-anti-affinity created
$ kubectl get pod -o wide
NAME                 READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
node-anti-affinity   1/1     Running   0          31s   192.168.79.116   k8s-worker01   <none>           <none>
Inter-Pod Affinity
Inter-Pod Affinityは既にデプロイされているPodのラベルをキーにして、新規にデプロイされるPodのスケジューリングを決定します。
例えば、あるサービスを構成するPodは同じノードにデプロイすることで、レイテンシーを極力小さくするなどの利用方法が考えられますね。
設定
ここでは、既に以下のPodがデプロイされています。
$ kubectl get pod -o wide -L env
NAME                         READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES   ENV
redis-prd                    1/1     Running   0          20s   192.168.79.111   k8s-worker01   <none>           <none>            prd
redis-stg                    1/1     Running   0          20s   192.168.69.225   k8s-worker02   <none>           <none>            stg
Production環境に新たにnginxをデプロイします。このとき、既にデプロイされているredisと同じノードにデプロイさせたいと思います。
マニフェストはNode Affinityと似ていますが、topologyKeyが追加となっています。ここではhostnameを指定していますので、prdラベルを持つPodと同じhostnameのNodeにデプロイされます。
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-prd
spec:
  selector:
    matchLabels:
      env: prd
  replicas: 3
  template:
    metadata:
      labels:
        env: prd
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: env
                operator: In
                values:
                - prd
            topologyKey: "kubernetes.io/hostname" #スケジューリング対象の範囲を指定
      containers:
      - name: nginx-prd
        image: nginx:latest
Inter-Pod Affinityの動作確認
このマニフェストをapplyします。
$ kubectl apply -f interPodAffinity.yaml
deployment.apps/nginx-prd created
$ kubectl get pod -o wide -L env
NAME                         READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES   ENV
nginx-prd-7d498d98ff-djzvz   1/1     Running   0          16s   192.168.79.120   k8s-worker01   <none>           <none>            prd
nginx-prd-7d498d98ff-gtbzj   1/1     Running   0          16s   192.168.79.122   k8s-worker01   <none>           <none>            prd
nginx-prd-7d498d98ff-vtmk4   1/1     Running   0          16s   192.168.79.127   k8s-worker01   <none>           <none>            prd
redis-prd                    1/1     Running   0          10m   192.168.79.111   k8s-worker01   <none>           <none>            prd
redis-stg                    1/1     Running   0          10m   192.168.69.225   k8s-worker02   <none>           <none>            stg
3つのPod全てがredis-prdと同じNodeにデプロイされていますね。
なお、今回はrequiredDuringSchedulingIgnoredDuringExecutionのみを指定しましたが、Node Affinityと同様にpreferredDuringSchedulingIgnoredDuringExecutionも指定することができます。
Inter-Pod Anti-affinity
Inter-Pod Anti-affinityは、特定のPodがデプロイされていないノード上に新たなPodをスケジューリングするポリシーです。
設定
Node Anti-Affinityとは異なり、operatorを否定形にするのではなく、spec.affinityフィールドにpodAntiAffinityをマニフェストで指定できます。
以下の例は、inter-Pod Affinityのマニフェストの「podAffinity」を「podAntiAffinity」に変更しています。
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-prd
spec:
  selector:
    matchLabels:
      env: prd
  replicas: 3
  template:
    metadata:
      labels:
        env: prd
    spec:
      affinity:
        podAntiAffinity: #podAffinityから変更
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: env
                operator: In
                values:
                - prd
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: nginx-prd
        image: nginx:latest
Inter-Pod Anti-affinityの動作確認
このマニフェストをapplyして動作を確認します。初期状態として、redisのみがデプロイされています。
$ kubectl get pod -o wide -L env
NAME        READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES   ENV
redis-prd   1/1     Running   1          22h   192.168.79.121   k8s-worker01   <none>           <none>            prd
redis-stg   1/1     Running   1          22h   192.168.69.229   k8s-worker02   <none>           <none>            stg
$ kubectl apply -f interPodAntiAffinity.yaml
deployment.apps/nginx-prd created
$ kubectl get pod -o wide -L env
NAME                         READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES   ENV
nginx-prd-7df77bb75f-69gcw   0/1     Pending   0          54s   <none>           <none>         <none>           <none>            prd
nginx-prd-7df77bb75f-7gvq6   0/1     Pending   0          54s   <none>           <none>         <none>           <none>            prd
nginx-prd-7df77bb75f-xqj5v   1/1     Running   0          54s   192.168.69.221   k8s-worker02   <none>           <none>            prd
redis-prd                    1/1     Running   1          22h   192.168.79.121   k8s-worker01   <none>           <none>            prd
redis-stg                    1/1     Running   1          22h   192.168.69.229   k8s-worker02   <none>           <none>            stg
replica数3のうち、1つだけPodがRunningになって、残りの2つはPendingで止まってますね。
詳細を確認します。
$ kubectl describe replicasets.apps nginx-prd-7df77bb75f
Name:           nginx-prd-7df77bb75f
・・・
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
  Normal  SuccessfulCreate  84s   replicaset-controller  Created pod: nginx-prd-7df77bb75f-xqj5v
  Normal  SuccessfulCreate  83s   replicaset-controller  Created pod: nginx-prd-7df77bb75f-69gcw
  Normal  SuccessfulCreate  83s   replicaset-controller  Created pod: nginx-prd-7df77bb75f-7gvq6
$ kubectl describe pod nginx-prd-7df77bb75f-69gcw
Name:           nginx-prd-7df77bb75f-69gcw
・・・
Events:
  Type     Reason            Age                 From               Message
  ----     ------            ----                ----               -------
  Warning  FailedScheduling  35s (x2 over 101s)  default-scheduler  0/3 nodes are available: 1 node(s) had taints that the pod didn't tolerate, 2 node(s) didn't match pod affinity/anti-affinity.
ReplicaSetまでは正常ですが、PodのデプロイでFailedSchedulingになっています。
あまり考えずに、「podAffinity」を「podAntiAffinity」に変更しましたが、よくよく考えると「prdラベルのPodがないノードにprdラベルのPodをデプロイする」という設定なので、1つ目のnginx-prdがデプロイされた時点で、全てのノード(2ノード)にprdラベルのPodが存在することになりますね。なので、2つ目、3つ目のPodがPendingで止まるのは正常な動作です。
まとめ
今回は5つのスケジューリングの制御方法について確認しました。環境や要件に応じて、色々な制御ができそうですね。
また、Node Affinity/Node Anti-Affinity/Inter-Pod Affinity/Inter-Pod Anti-Affinityの4つは組み合わせて利用することもできますので、さらに細かく制御することができます。
まあ、細かくすればするほど設計が大変になりそうではありますが。。。
