KubernetesのNode Affinity, Inter-Pod Affinityについて

# Node Affinity

taints/tolerations([参考](https://qiita.com/sheepland/items/8fedae15e157c102757f))は特定のNodeにPodをscheduleされるのを避けるための仕組み。
それに対して`Node Affinity`という仕組みは、Podを特定のNode集合へscheduleするための仕組みだ。
特定のNodeにPodをscheduleするための仕組みとしては`node selector`がある。`Node Affinity`は`node selector`よりももっとパワフル。

`Node Affinity`はNodeのlabelとして設定する。
そして(`node selector`はpodにselectorを設定したが、同様に)`Node Affinity rule`はpodに設定する。
podの`Node Affinity rule`にnodeのlabelの`Node Affinity`がマッチした場合、そのnodeへpodがscheduleされる。

まずnodeのlabelに設定されているNode Affinityを試しに見てみよう。
以下はGKEのnodeに設定されているlabelだ。

```
$ kubectl describe node gke-kubia-default-pool-db274c5a-mjnf
Name:     gke-kubia-default-pool-db274c5a-mjnf
Role:
Labels: beta.kubernetes.io/arch=amd64
        beta.kubernetes.io/fluentd-ds-ready=true
        beta.kubernetes.io/instance-type=f1-micro
        beta.kubernetes.io/os=linux
        cloud.google.com/gke-nodepool=default-pool
        failure-domain.beta.kubernetes.io/region=europe-west1
        failure-domain.beta.kubernetes.io/zone=europe-west1-d
        kubernetes.io/hostname=gke-kubia-default-pool-db274c5a-mjnf
```

沢山labelが設定されているがaffiniyとして重要なのが下の3つのlabel。詳細はあとで説明するが、この3つが意味するのは以下だ。

* `failure-domain.beta.kubernetes.io/region` はnodeが位置するregionを指す
* `failure-domain.beta.kubernetes.io/zone` はnodeが属するavailability zoneを指す
* `kubernetes.io/hostname` はnodeのhostnameを指す


podには以下のようにnodeAffinityを設定する。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-pod
spec:
  containers:
  - name: main
    image: alpine
    command: ["sleep", "9999999"]
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: failure-domain.beta.kubernetes.io/zone
            operator: In
            values:
            - "europe-west1-d"
            - "asia-northeast1-a"
```

matchExpressionsを使って柔軟に条件を設定することができる。
`requiredDuringSchedulingIgnoredDuringExecution`は2つの意味に分解できる。

前半の`requiredDuringScheduling`はnodeをscheduleする際、このaffinity ruleに合致するnodeにしかscheduleされないことを意味する。
後半の`IgnoredDuringExecution`はこのaffinity ruleはnodeで実行中のpodには影響を与えないことを意味する。

`preferredDuringSchedulingIgnoredDuringExecution`というのもある。
前半の`preferredDuringScheduling`はnodeをscheduleする際、このaffinity ruleに合致するnodeに優先的にscheduleするが合致するnodeがなければ他のnodeへscheduleすることを意味する。
また`IgnoredDuringExecution`とは対象的な`RequiredDuringExecution`もある。これはnod上で実行中のpodに影響を与えることを意味するが、まだKubernetesで実装されていなく、今後実装される予定。

`operator: In`という設定が上記でされているが、operatorには`In`以外にも`NotIn`、`Exists`、`DoesNotExists`、`Gt`、`Lt`が使用できる

またNode Affinityの設定は複数定義することができ、それぞれにweightを設定することができる。

## 試してみる


```yaml:node-affinity-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-pod
spec:
  containers:
  - name: main
    image: alpine
    command: ["sleep", "9999999"]
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/hostname
            operator: In
            values:
            - gke-wdpress-default-pool-ee3b6592-6n3t
```

```
$ kubectl create -f node-affinity-pod.yaml
pod "node-affinity-pod" created
```

nodeAffinityで指定したノードに配置されていることが分かる

```
$ kubectl get pod -o wide
NAME                READY     STATUS    RESTARTS   AGE       IP           NODE
node-affinity-pod   1/1       Running   0          3s        10.24.8.10   gke-wdpress-default-pool-ee3b6592-6n3t
```

ちなみにaffinity定義にマッチしない場合、podを作成してもPending状態になる。

```shell_session
$ kubectl get pod -o wide
NAME                READY     STATUS    RESTARTS   AGE       IP        NODE
node-affinity-pod   0/1       Pending   0          2s        <none>    <none>

# Eventsの欄に「No nodes are available that match all of the predicates: MatchNodeSelector (5).」と出ているのが分かる。
$ kubectl describe pod node-affinity-pod | tail
    SecretName:  default-token-2hc7t
    Optional:    false
QoS Class:       Burstable
Node-Selectors:  <none>
Tolerations:     node.alpha.kubernetes.io/notReady:NoExecute for 300s
                 node.alpha.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason            Age              From               Message
  ----     ------            ----             ----               -------
  Warning  FailedScheduling  2s (x4 over 5s)  default-scheduler  No nodes are available that match all of the predicates: MatchNodeSelector (5).
```

# Node Anti-Affinity

Nod Anti-Affinityは Nod Affinityの反対で、Podを特定のNode以外へスケジューリングすることができます。

厳密にはNod Anti-Affinityは存在せず、Node Affinityのspec.affinity.nodeAffinityの条件部分に否定形のオペレータを指定することで実現する。


# Inter-Pod Affinity

`Node Selector`と `Node Affinity`はpodとnodeの影響を与えるものだった。
`Inter-Pod Affinity`はpod間に影響を与える仕組み。
`Inter-Pod Affinity`を使うと、特定のPodが実行されているドメイン(Node、ラック、ゾーン、データセンター、etc)上へPodをschedulingさせることができる。
たとえばfrontend podとbackend podがあった場合それらのpodが近いほうがlatencyが減らせるので、それぞれのpodが近いほうがいい。そういうときに`Inter-Pod Affinity`が使える。

以下のような定義。

```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 5
  template:
... 
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - topologyKey: rack
            labelSelector:
              matchLabels:
                app: backend
```

`topologyKey: rack`だが、こうすると同じrackにあるnodeにpodがscheduleされる。
上記でいうと、`app: backend`というlabelをもったpodがすでに`rack=a`というlabelを持ったnodeにscheduleされていたら、このfrontend podは`rack=a`というlabelをもったnodeにscheduleされる。

つまり、例えばまず2つのnodeにそれぞれ`rack=a`, `rack=b`のようにLabelをつけたとする。
すると、`app=backend`というlabelをもつpodが先に`rack=a`のnodeにscheduleされている場合は、上記と同じInter-Pod Affinity ruleをもつ他のpodは`rack=a`のnodeにscheduleされる。


## 試してみる

nodeを5つ作る

```shell-session
$ kubectl get node
NAME                                     STATUS    ROLES     AGE       VERSION
gke-wdpress-default-pool-ee3b6592-6n3t   Ready     <none>    59m       v1.8.8-gke.0
gke-wdpress-default-pool-ee3b6592-mj3x   Ready     <none>    59m       v1.8.8-gke.0
gke-wdpress-default-pool-ee3b6592-pbrq   Ready     <none>    59m       v1.8.8-gke.0
gke-wdpress-default-pool-ee3b6592-q033   Ready     <none>    59m       v1.8.8-gke.0
gke-wdpress-default-pool-ee3b6592-th89   Ready     <none>    59m       v1.8.8-gke.0
```

`app: backend` labelを設定したbackend-pod.yamlを作り、

```yaml:backend-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: backend-pod
  labels:
    app: backend
spec:
  containers:
  - name: main
    image: alpine
    command: ["sleep", "9999999"]
```

backend-podを作成する。

```shell-session
$ kubectl create -f backend-pod.yaml
pod "backend-pod" created

$ kubectl get pod -o wide
NAME                      READY     STATUS        RESTARTS   AGE       IP           NODE
backend-pod               1/1       Running       0          4s        10.24.5.16   gke-wdpress-default-pool-ee3b6592-mj3x
```

次にInter-Pod Affinityを設定したpodを作成する

```yaml:pod-affinity-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-affinity-pod1
spec:
  containers:
  - name: main
    image: alpine
    command: ["sleep", "9999999"]
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - topologyKey: "kubernetes.io/hostname"
        labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - backend        
```

そしてpod-affinity-pod1を作成

```shell-session
$ kubectl create -f  pod-affinity-pod.yaml
pod "pod-affinity-pod1" created
```

同じノードに配置されていることが分かる(topologyKey: "kubernetes.io/hostname"なので同じホスト(=ノード)に配置されるということ)

```
$ kubectl get pod -o wide
NAME                READY     STATUS    RESTARTS   AGE       IP           NODE
backend-pod         1/1       Running   0          56s       10.24.5.16   gke-wdpress-default-pool-ee3b6592-mj3x
pod-affinity-pod1   1/1       Running   0          4s        10.24.5.17   gke-wdpress-default-pool-ee3b6592-mj3x
```

pod-affinity-podを増やして見ても同じノードに配置されることが分かる。

```
$ kubectl get pod -o wide
NAME                READY     STATUS    RESTARTS   AGE       IP           NODE
backend-pod         1/1       Running   0          1m        10.24.5.16   gke-wdpress-default-pool-ee3b6592-mj3x
pod-affinity-pod1   1/1       Running   0          19s       10.24.5.17   gke-wdpress-default-pool-ee3b6592-mj3x
pod-affinity-pod2   1/1       Running   0          8s        10.24.5.18   gke-wdpress-default-pool-ee3b6592-mj3x
pod-affinity-pod3   1/1       Running   0          2s        10.24.5.19   gke-wdpress-default-pool-ee3b6592-mj3x
```

# Inter-Pod Anti-Affinity

Inter-Pod Anti-AffinityはInter-Pod Affinityと反対で、特定のPodがいないドメイン(Node、ラック、ゾーン、データセンター、etc)上へPodをschedulingさせることができる。

また[このように](https://github.com/kubernetes/website/blob/master/content/en/docs/concepts/configuration/pod-with-pod-affinity.yaml)、`Inter-Pod Affinity`と`Inter-Pod Anti-Affinity`を合わせて指定することもできる。

# 参考

* [Kubernetes完全ガイド](https://www.amazon.co.jp/dp/B07HFS7TDT/)(最高に分かりやすく書かれているのでぜひ買いましょう)
* [Kubernetes in Action](http://amzn.asia/80rSDOx) 
* [Affinity and anti-affinity](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity)