OpenShiftにおけるPodの分散配置

# はじめに
OpenShiftやKubernetesではスケジューラーがどのノードにPodを配置するか決定します。複数ノードに分散配置されることが望ましいですが、単純にデプロイした場合、すべてのPodが1ノードに配置されることもあります。この記事では「トポロジー分散制約」と「Deschedulearを使用したPodのエビクト」を試します。

https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.8/html/nodes/nodes-scheduler-pod-topology-spread-constraints

https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.8/html/nodes/nodes-descheduler

環境やアプリケーションについては以下の記事を参照ください。

■ OpenShift環境

https://qiita.com/y-akio/items/ad0093daa504be746573

■ Spring Bootアプリケーション

https://qiita.com/y-akio/items/155ae44188a6df02138c

#1. トポロジー分散制約
ラベル「topology.kubernetes.io/zone」が異なるノードにPodが分散してスケジュールされるように、マニフェストに「topologySpreadConstraints」を追加してデプロイしました。workerノード0～2に1Podずつスケジュールされたことを確認できます。

```bash
oc get nodes
NAME       STATUS   ROLES    AGE   VERSION
master-0   Ready    master   14d   v1.21.1+051ac4f
master-1   Ready    master   14d   v1.21.1+051ac4f
master-2   Ready    master   14d   v1.21.1+051ac4f
worker-0   Ready    worker   14d   v1.21.1+051ac4f
worker-1   Ready    worker   14d   v1.21.1+051ac4f
worker-2   Ready    worker   14d   v1.21.1+051ac4f

oc label node worker-0 topology.kubernetes.io/zone=tok0
oc label node worker-1 topology.kubernetes.io/zone=tok1
oc label node worker-2 topology.kubernetes.io/zone=tok2

oc apply -f spring-liberty.yaml

oc get pod -o wide
### 標準出力↓
NAME                              READY   STATUS    RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
spring-liberty-85469f6ffd-7j6gq   1/1     Running   0          24s   10.128.3.165   worker-0   <none>           <none>
spring-liberty-85469f6ffd-c6sms   1/1     Running   0          24s   10.129.2.60    worker-1   <none>           <none>
spring-liberty-85469f6ffd-fl67k   1/1     Running   0          24s   10.131.0.34    worker-2   <none>           <none>
```

```yaml:spring-liberty.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-liberty
  labels:
    app: spring-liberty
spec:
  replicas: 3
  selector:
    matchLabels:
      app: spring-liberty
  template:
    metadata:
      labels:
        app: spring-liberty
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: ScheduleAnyway
        labelSelector:
          matchLabels:
            app: spring-liberty
      containers:
        - name: spring-liberty
          image: image-registry.openshift-image-registry.svc:5000/spring-liberty/spring-liberty
          ports:
            - containerPort: 9080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 9080
```

また、レプリカ数を3～6に変更したとき、各ノードのPod数は下表のようになりました。
maxSkewで指定したPod数の最大差『1』でスケジュールされています。

| レプリカ数 | ノード<br>worker-0 | ノード<br>worker-1 | ノード<br>worker-2 |
|---|---|---|---|
| 3 | 1 | 1 | 1 |
| 4 | 1 | 2 | 1 |
| 5 | 1 | 2 | 2 |
| 6 | 2 | 2 | 2 |

```bash:レプリカ数変更方法
oc scale --replicas=<レプリカ数> deploy/spring-liberty 
```

#2. Kube DeschedulerオペレーターによるPodのエビクト
「Kube Descheduler」オペレーターにより、ポリシーに違反するPodが削除(エビクト)されることを確認します。削除後、Podがどのノードに作成されるかはスケジューラーが決定します。

##2.1. ノード停止後の挙動確認
ノード「worker-2」停止後、同ノードのスタータスは『NotReady』になりました。また、worker-2のPod「spring-liberty-85469f6ffd-fl67k」は終了中となり、worker-1にPod「spring-liberty-85469f6ffd-fl67k」が作成されました。マニフェストに記載したレプリカ数『3』を維持しようとしていることが分かります。

```bash
oc get nodes
### 標準出力↓
NAME       STATUS     ROLES    AGE   VERSION
master-0   Ready      master   1d   v1.21.1+051ac4f
master-1   Ready      master   1d   v1.21.1+051ac4f
master-2   Ready      master   1d   v1.21.1+051ac4f
worker-0   Ready      worker   1d   v1.21.1+051ac4f
worker-1   Ready      worker   1d   v1.21.1+051ac4f
worker-2   NotReady   worker   1d   v1.21.1+051ac4f

oc get pod -o wide
### 標準出力↓
NAME                              READY   STATUS        RESTARTS   AGE     IP            NODE       NOMINATED NODE   READINESS GATES
spring-liberty-85469f6ffd-4d9d9   1/1     Running       0          116s    10.129.2.64    worker-1   <none>           <none>
spring-liberty-85469f6ffd-7j6gq   1/1     Running       0          8m36s   10.128.3.165   worker-0   <none>           <none>
spring-liberty-85469f6ffd-c6sms   1/1     Running       0          8m36s   10.129.2.60    worker-1   <none>           <none>
spring-liberty-85469f6ffd-fl67k   1/1     Terminating   0          8m36s   10.131.0.34    worker-2   <none>           <none>
```

##2.2. ノード起動後の挙動確認
ノード「worker-2」起動後、同ノードのスタータスは『Ready』になりました。worker-2のPod「spring-liberty-85469f6ffd-fl67k」の終了処理は完了しましたが、Podが再作成されることはありません。

```bash
oc get nodes
### 標準出力↓
NAME       STATUS   ROLES    AGE   VERSION
master-0   Ready    master   1d   v1.21.1+051ac4f
master-1   Ready    master   1d   v1.21.1+051ac4f
master-2   Ready    master   1d   v1.21.1+051ac4f
worker-0   Ready    worker   1d   v1.21.1+051ac4f
worker-1   Ready    worker   1d   v1.21.1+051ac4f
worker-2   Ready    worker   1d   v1.21.1+051ac4f

oc get pod -o wide
### 標準出力↓
NAME                              READY   STATUS    RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
spring-liberty-85469f6ffd-4d9d9   1/1     Running   0          7m40s   10.129.2.64    worker-1   <none>           <none>
spring-liberty-85469f6ffd-7j6gq   1/1     Running   0          14m     10.128.3.165   worker-0   <none>           <none>
spring-liberty-85469f6ffd-c6sms   1/1     Running   0          14m     10.129.2.60    worker-1   <none>           <none>
```

##2.3. KubeDeschedulerマニフェストの適用
「Kube Descheduler」オペレーターをインストールした後、「KubeDescheduler」マニフェストを適用すると、worker-1に2つ起動していたPodのうち1つが終了し、worker-2にPod「spring-liberty-85469f6ffd-pm9jw」が作成されました。結果的に各ノードに1つのPodが存在する状態になっています。

https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.8/html/nodes/nodes-descheduler-installing_nodes-descheduler

```bash
oc apply -f descheduler.yaml

oc get pod -o wide
### 標準出力↓
spring-liberty-85469f6ffd-4d9d9   1/1     Running   0          10m     10.129.2.64    worker-1   <none>           <none>
spring-liberty-85469f6ffd-7j6gq   1/1     Running   0          16m     10.128.3.165   worker-0   <none>           <none>
spring-liberty-85469f6ffd-pm9jw   1/1     Running   0          2m14s   10.131.0.35    worker-2   <none>           <none>
```

「KubeDescheduler」マニフェストでは、同じノードにPodが重複する場合は削除されるように、TopologyAndDuplicatesプロファイルを有効にしています。

```yaml:desucheduler.yaml
apiVersion: operator.openshift.io/v1
kind: KubeDescheduler
metadata:
  name: cluster
  namespace: openshift-kube-descheduler-operator
spec:
  deschedulingIntervalSeconds: 0
  logLevel: Normal
  managementState: Managed
  operatorLogLevel: Normal
  profiles:
  - TopologyAndDuplicates
```

##2.4. OpenShift Kube Deschedulerについて
OpenShiftの「Kube Descheduler」オペレーターは、GitHubに公開されている「Descheduler for Kubernetes」のアップストリーム版です。
>The Descheduler is an upstream Kubernetes subproject owned by SIG-Scheduling. 21

https://cloud.redhat.com/blog/descheduler-ga-in-openshift-4.7

https://github.com/kubernetes-sigs/descheduler

「Descheduler for Kubernetes」は、「DeschedulerPolicy」をConfigMapとして作成し、同設定を元にdeschedulerコンテナを実行することでPodの削除を行う仕組みのようです。一方、OpenShiftの「Kube Descheduler」オペレーターでは、「KubeDescheduler」マニフェスト適用時に「DeschedulerPolicy」が作成されていました。

```bash
oc describe cm cluster -n openshift-kube-descheduler-operator
### 標準出力↓
Name:         cluster
Namespace:    openshift-kube-descheduler-operator
Labels:       <none>
Annotations:  <none>

Data
====
policy.yaml:
----
apiVersion: descheduler/v1alpha1
ignorePvcPods: true
kind: DeschedulerPolicy
strategies:
  RemoveDuplicates:
    enabled: true
    params:
      includeSoftConstraints: false
      namespaces:
        exclude:
        - kube-system
        - openshift-apiserver
        - openshift-apiserver-operator
…
```

前項の例でworker-1で実行されていたPod「spring-liberty-85469f6ffd-c6sms」を削除したのは、ネームスペース『openshift-kube-descheduler-operator』のPod「cluster-69ccfc675b-6bpm2」です。下記のログが出力されていました。
> "Evicted pod" pod="spring-liberty/spring-liberty-85469f6ffd-c6sms" reason="RemoveDuplicatePods"

```bash
oc get pod -n openshift-kube-descheduler-operator
### 標準出力↓　
NAME                                    READY   STATUS      RESTARTS   AGE
cluster-69ccfc675b-6bpm2                0/1     Completed   3          63s
descheduler-operator-858465f765-kxsgj   1/1     Running     0          10m

oc logs cluster-69ccfc675b-6bpm2 -n openshift-kube-descheduler-operator
### 標準出力↓
I0826 03:18:54.636239       1 dynamic_serving_content.go:111] Loaded a new cert/key pair for "serving-cert::/certs-dir/tls.crt::/certs-dir/tls.key"
I0826 03:18:59.430209       1 dynamic_serving_content.go:130] Starting serving-cert::/certs-dir/tls.crt::/certs-dir/tls.key
I0826 03:18:59.430283       1 tlsconfig.go:200] loaded serving cert ["serving-cert::/certs-dir/tls.crt::/certs-dir/tls.key"]: "metrics.openshift-kube-descheduler-operator.svc" [ser
ving] validServingFor=[metrics.openshift-kube-descheduler-operator.svc,metrics.openshift-kube-descheduler-operator.svc.cluster.local] issuer="openshift-service-serving-signer@16285
84568" (2021-08-26 01:53:35 +0000 UTC to 2023-08-26 01:53:36 +0000 UTC (now=2021-08-26 03:18:59.430262245 +0000 UTC))
I0826 03:18:59.430471       1 named_certificates.go:53] loaded SNI cert [0/"self-signed loopback"]: "apiserver-loopback-client@1629947939" [serving] validServingFor=[apiserver-loop
back-client] issuer="apiserver-loopback-client-ca@1629947936" (2021-08-26 02:18:54 +0000 UTC to 2022-08-26 02:18:54 +0000 UTC (now=2021-08-26 03:18:59.430463658 +0000 UTC))
I0826 03:18:59.430522       1 secure_serving.go:197] Serving securely on [::]:10258
I0826 03:18:59.430612       1 tlsconfig.go:240] Starting DynamicServingCertificateController
I0826 03:18:59.537841       1 node.go:46] "Node lister returned empty list, now fetch directly"
I0826 03:18:59.629163       1 duplicates.go:99] "Processing node" node="master-0"
I0826 03:18:59.727021       1 duplicates.go:99] "Processing node" node="master-1"
I0826 03:18:59.827317       1 duplicates.go:99] "Processing node" node="master-2"
I0826 03:18:59.854129       1 duplicates.go:99] "Processing node" node="worker-0"
I0826 03:18:59.878843       1 duplicates.go:99] "Processing node" node="worker-1"
I0826 03:18:59.913138       1 duplicates.go:99] "Processing node" node="worker-2"
I0826 03:18:59.945896       1 duplicates.go:194] "Adjusting feasible nodes" owner={namespace:spring-liberty kind:ReplicaSet name:spring-liberty-85469f6ffd imagesHash:image-registry
.openshift-image-registry.svc:5000/spring-liberty/spring-liberty} from=6 to=3
I0826 03:18:59.945925       1 duplicates.go:202] "Average occurrence per node" node="worker-1" ownerKey={namespace:spring-liberty kind:ReplicaSet name:spring-liberty-85469f6ffd ima
gesHash:image-registry.openshift-image-registry.svc:5000/spring-liberty/spring-liberty} avg=1
I0826 03:18:59.967505       1 evictions.go:130] "Evicted pod" pod="spring-liberty/spring-liberty-85469f6ffd-c6sms" reason="RemoveDuplicatePods"
I0826 03:18:59.984554       1 topologyspreadconstraint.go:139] "Processing namespaces for topology spread constraints"
I0826 03:19:00.427877       1 descheduler.go:151] "Number of evicted pods" totalEvicted=1
```