Kekeの日記

エンジニア、読書なんでも

KubernetesのNode AffinityとInternal Pod Affinityを使ってPodを高度スケジューリングする

f:id:bobchan1915:20180926175658p:plain

はじめに

Kubernetesには高度スケジューリングを行えるAPIが用意されていあるので、今回はまとめようと思います。

どれもlabelを使って認識するのでその事前知識が必要です。

アジェンダ

Affinity, Anti-Affinityとは

Affinity, Anti-Affinityとは

PodかNodeにスケジュールされるのに対して、どのNodeにPodを配置するかや、どのPodをNodeが配置を許可するかのルール

を示したものです。

公式記事は以下のようになっています。

Assigning Pods to Nodes - Kubernetes

これからいくつかのセクションで分けて解説します。

2つの種類

以下のようにAffinityには2つの種類があります。

  • Node Affinity, Anti-Affinity
  • Internal Pod Affinity

Node Affinity, Anti-Affinity

Node Affinityは

どのNodeにPodが配置されるかのノードのラベルで識別したもの

を示したものです。高度なnodeSelectorです。

f:id:bobchan1915:20180926180328p:plain

指定方法はnodeSelectorTermsというものがあって、以下のように設定します。

nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2

また、Node Anti-Affinityはその逆です。

名前の通り、どのNodeに!?を選ぶようなものです。

ここで出てきたoperatorについてはあとに解説します。

またこの中でも2つの種類があります。

項目 説明
requiredDuringSchedulingIgnoredDuringExecution 指定したNodeにスケジュールされることを強制する
preferredDuringSchedulingIgnoredDuringExecution 指定したNodeにスケジュールされることを優先的にする。weightによって1~100で優先度を設定する。

です。

またどちらも語尾についているIgnoredDuringExecutionは、ランタイムでラベルのノードが変更された時などはPodは終了しないことを意味しています。

以下のようにspec.affinityに設定します。

affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value

また、Node-AntiAffintiyではtopologyKeyを指定することができますが、あとで解説します。

Internal Pod Affinity, Anti-Affinity

Node Affinityの逆で、Internal Pod Affinityとは公式ドキュメントでは

NodeがPodのラベルを識別してPodを受け入れるかを指定する

ルールと書かれています。たとえば特定のPodが動いていた時は受け入れないっといったようなことができるようになります。

ですが、実質的には

Pod同士のルールを定めてPodをスケジューリングするものです。

注意点としては、大きなクラスタになるとスケジューリングコストがかかります。

また、指定の方法は二つ種類があって、これはNode Affinityと同様です。

項目 説明
requiredDuringSchedulingIgnoredDuringExecution 指定したNodeにスケジュールされることを強制する
preferredDuringSchedulingIgnoredDuringExecution 指定したNodeにスケジュールされることを優先的にする

以下のように設定します。

spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: failure-domain.beta.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: kubernetes.io/hostname
  containers:
  - name: with-pod-affinity
    image: k8s.gcr.io/pause:2.0

ここで意味するところは

  • podAffinityについては
    • 新しいPodはもしsecurity=S1に対して同じゾーンで一つ以上あればPodはそのNodeにスケジュールされる
  • podAntiAffinityについては
    • 新しいPodはもしsecurity=S2に対して同じゾーンで一つ以上あればPodはそのNodeにスケジュールされない

という意味である。

ビルトインNode Label

ノードにはあらかじめ自動的にラベル付けされるものがあって、それも指定することができます。

現在では以下のようなラベルがあります。

  • kubernetes.io/hostname
  • failure-domain.beta.kubernetes.io/zone
  • failure-domain.beta.kubernetes.io/region
  • beta.kubernetes.io/instance-type
  • beta.kubernetes.io/os
  • beta.kubernetes.io/arch

またGoogle Cloud Platformだと以下のようにすればプリエンティティブノードにスケジューリングされることを防ぐことができます。

nodeSelectorTerms:
        - matchExpressions:
          - key: cloud.google.com/gke-preemptible
            operator: DoesNotExist

operator

以下のようなoperatorの指定方法があります。

operator 意味
In ラベルの値が指定したValueのいずれかに一致する
NotIn ラベルの値が指定したValueのいずれにも一しない時
Exists ラベルが存在する時
DoesNotExist ラベルが存在しない時
Gt ラベルの値が指定した値よりも大きい時
Lt ラベルの値が指定した値よりも小さい時

topologyKey

podAffintiyなどを結果的にどのレベルで一緒に配置するのというものです。

operator 意味
failure-domain.beta.kubernetes.io/zone 同じ値をもつPodと同じゾーンにスケジュール
kubernetes.io/hostname 同じ値をもつPodの同じノードにスケジュール

Affinityの今後

現在はまだベータ版ですが、以下の機能が実装されることが予定されています。

  • requiredDuringSchedulingRequiredDuringExecution: これによってすでにあるPodは削除されて、Affinityにあうように再スケジュールされる。

検証

準備

変数設定

何回も何回も指定するのは面倒なので以下のように設定します。

$CLUSTER_NAME=hoge

クラスタ作成

以下のようにクラスタを作成します。

Nodeの数ですがデフォルトは3ですが、せっかくのスケジューリングの実証なので6にします。 Nodeのスペックは高くなくていいので、以下のg1-smallに指定します。

gcloud container clusters create- $CLUSTER_NAME --region=asia-northeast1-b --num-nodes=6 --machine-type=g1-small

正しくクラスタが構築できたか確認してください。

gcloud container clusters describe $CLUSTER_NAME --region $(gcloud config get-value compute/region)

そしてkubectlでアクセスできるようにcredentialを取得します。

gcloud container clusters get-credentials $CLUSTER_NAME --region $(gcloud config get-value compute/region)

ここでNodeを確認してください。

kubectl get nodes

NAME                                            STATUS    ROLES     AGE       VERSION
gke-affinitiy-test-default-pool-42bc3e7f-4v8r   Ready     <none>    19m       v1.9.7-gke.6
gke-affinitiy-test-default-pool-42bc3e7f-f7f9   Ready     <none>    19m       v1.9.7-gke.6
gke-affinitiy-test-default-pool-42bc3e7f-j50r   Ready     <none>    19m       v1.9.7-gke.6
gke-affinitiy-test-default-pool-42bc3e7f-n1m5   Ready     <none>    19m       v1.9.7-gke.6
gke-affinitiy-test-default-pool-42bc3e7f-n31r   Ready     <none>    19m       v1.9.7-gke.6
gke-affinitiy-test-default-pool-42bc3e7f-rh20   Ready     <none>    19m       v1.9.7-gke.6
オプション説明

Num Nodes

以下のオプションでNodeの個数を指定することができます。

--num-nodes=NUM_NODES; default=3]

Machine type

どのインスタンスのタイプ化かを指定します。

[--machine-type=MACHINE_TYPE, -m MACHINE_TYPE]

スタンダートなものと、小さなものは以下の一覧から選べます。

f:id:bobchan1915:20180926185935p:plain

f:id:bobchan1915:20180926190000p:plain

Machine Types  |  Compute Engine Documentation  |  Google Cloud

Nodeラベル

何かnode全体にラベルを付けたければ以下のオプションがあります。

[--node-labels=[NODE_LABEL,…]

Podに使うコンテナイメージ用意

今回はスケジューリングを見るので特に作らなくて大丈夫です。

以下の公式DockerImageを使おうと思います。

  • nginx:1.15.4-alpine
  • node:8.12.0-alpine

実際にやっていく

何も指定なし

以下のように何も指定しません。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 8
  selector:
    matchLabels:
      app: nginx-pod
  template:
    metadata:
      labels:
        app: nginx-pod
    spec:
      containers:
      - name: nginx
        image: nginx-1.15.4-alpine
        ports:
        - containerPort: 80

以下のようになっています。

kubectl get pods

nginx-566d7d7fb9-2tdtx   1/1       Running   0          1m
nginx-566d7d7fb9-99wrx   1/1       Running   0          24m
nginx-566d7d7fb9-bptf5   1/1       Running   0          1m
nginx-566d7d7fb9-dl6tt   1/1       Running   0          1m
nginx-566d7d7fb9-md988   1/1       Running   0          24m
nginx-566d7d7fb9-xcvnp   1/1       Running   0          24m
nginx-566d7d7fb9-xh2jp   1/1       Running   0          24m
nginx-566d7d7fb9-xz6rj   1/1       Running   0          24m

どのようにNodeにスケジューリングされたかというと以下のようになっています。

f:id:bobchan1915:20180926201319p:plain

以下のコマンドでスケールされることができます。

kubectl scale --replicas=20 deploy/nginx
結果

1つはどこも配置されて、あるところは2つになっている

ここで確率的に一つも配置されないケースもありえるので、再度試してみてください。

nodeSelector

次にspec.nodeSelectorでビルドインラベルが以下のようなものを指定しようと思います。

kubernetes.io/hostnamegke-affinitiy-test-default-pool-42bc3e7f-4v8rのもの。

よって以下をdeploymentに付け足します。

nodeSelector:
        kubernetes.io/hostname: gke-affinitiy-test-default-pool-42bc3e7f-4v8r

そしてデプロイします。

結果

f:id:bobchan1915:20180926203437p:plain

リソースが限界に対しているのでひとつはスケジューリングできなかったですが他はうまくいってます。

Node Affinity

新しいラベルをNodeごとに割り当てるのは面倒なので、以下のようにします。

affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/hostname
            operator: In
            values:
            - gke-affinitiy-test-default-pool-42bc3e7f-4v8r
            - gke-affinitiy-test-default-pool-42bc3e7f-f7f9
結果

f:id:bobchan1915:20180926204215p:plain

狙った通り、二つに入れられています。

Internal Pod Affinity

これは2つ目のイメージも使います。

一つ目のreplica数を以下の用に変更します。

replica: 1

また、ラベルも付与しておきます。

nginx: prod

2つめのdeploymentは以下のようにします。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: node
  labels:
    app: node
spec:
  replicas: 1
  selector:
    matchLabels:
      app: node-pod
  template:
    metadata:
      labels:
        app: node-pod
    spec:
      containers:
      - name: node
        image: node:8.12.0-alpine
        ports:
        - containerPort: 5000
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: nginx
                operator: In
                values:
                - prod
            topologyKey: kubernetes.io/hostname

ユースケースとしてはnginx-podと一緒のnodeにnode-podを入れたい時です。

結果

以下のように一致しています。

f:id:bobchan1915:20180926213319p:plain

クリーンアップ

検証用に使ったリソースを解放します。

まず、deploymentを消してください。

kubectl delete deploy nginx
kubectl delete deploy node

そしてクラスタを消してください。

gcloud container cluster $CLUSTER_NAME -- region $(gcloud config get-value computer/region)

以上です。

まとめ

Pod同士の配置は意識したことがなかったのでいい勉強になりました。

付録

NodeSelectorとは

NodeSelectorとは、もっとも簡単なNode Affinityのことです。

ラベルによってPodを配置するNodeを選ぶことができる。

以下のようにspec.nodeSelectorにつけるだけです。

spec:
     nodeSelector:
         disktype: ssd

それで一致するラベルがあればスケジュールしてくれます。

ラベルをCLIから付ける方法

以下のコマンドでノードも付けることができます。

kubectl label nodes <node-name> <label-key>=<label-value>

Podも同様に付けることはできますが、.yamlなどのファイルにつけて宣言的であることを保証するためにCLIで付けることは推奨しません。

Resource Quota

たとえばクラスタ作成のときに--num-nodes10とするとクラスタを作成することができなく、以下のエラーがでます。

ERROR: (gcloud.container.clusters.create) ResponseError: code=403, message=
    (1) insufficient regional quota to satisfy request: resource "CPUS": request requires '10.0' and is short '2.0'. project has a quota of '8.0' with '8.0' available
    (2) insufficient regional quota to satisfy request: resource "IN_USE_ADDRESSES": request requires '10.0' and is short '2.0'. project has a quota of '8.0' with '8.0' available.

これはResource Quota=リソースの分け前と呼ばれるもので、この場合だと作成できるノード数はリージョンによって指定されています。

追加で要求するときはIAMと管理 -> 割り当て から編集することができます。

事前に知りたい時は以下のコマンドを走らせてください。

gcloud compute project-info describe --project $(gcloud config get-value project)