【Kubernetes】StatefulSetについて理解する
はじめに
KubernetesのStatefulSetについて、どんなものなのか動かしながら理解していきたいと思います。
StatefulSetとは
StatefulSetとは、データベースなどステートフルなPodを管理するオブジェクトになります。
ざっくり特徴としては、下記のものが挙げられます。
- 各Podは一意なインデックスを含むホスト名がつけられ、再起動した場合でも同じホスト名のPodが起動される(pod-0, pod-1, pod2など)
- 各Podに対して一意なPVCが割り当てられ、Podが削除されてもPVCは削除されない
- 作成されるPodはインデックスが小さい順から作成され、前のPodがReadyになるまで次のPodは作成開始されない
- 削除されるPodはインデックスが大きい順から削除され、前のPodが削除されるまで次のPodは削除されない
- Headless Serviceを作成する必要がある
Headless Service
Headless Serviceとは、ClusterIPタイプのServiceでClusterIPの値が"None"に設定されているServiceのことです。
StatefulSetのように各Podに一意な役割があり、負荷分散のためのIPアドレスが不要な場合に使われます。
1apiVersion: v1
2kind: Service
3metadata:
4 name: headless-svc
5 labels:
6 app: sts-app
7spec:
8 ports:
9 - port: 80
10 clusterIP: None # clusterIPをNoneにすることでHeadless Serviceになる
11 selector:
12 app: sts-app
試してみる
実際にStatefulSetを操作しながら、特徴を理解していきます。
作成するファイルは以下の通りです。
1.
2├── service.yml
3└── statefulset.yml
service.yml
とstatefulset.yml
の内容は下記の通りです。
1apiVersion: v1
2kind: Service
3metadata:
4 name: myapp-svc
5 labels:
6 app: myapp
7spec:
8 ports:
9 - port: 80
10 clusterIP: None
11 selector:
12 app: myapp
1apiVersion: apps/v1
2kind: StatefulSet
3metadata:
4 name: myapp
5spec:
6 serviceName: "myapp-svc"
7 replicas: 3
8 selector:
9 matchLabels:
10 app: myapp
11 template:
12 metadata:
13 labels:
14 app: myapp
15 spec:
16 containers:
17 - name: web
18 image: nginx:1.22
19 ports:
20 - containerPort: 80
21 volumeMounts:
22 - name: html
23 mountPath: /usr/share/nginx/html
24 volumeClaimTemplates:
25 - metadata:
26 name: html
27 spec:
28 accessModes: [ "ReadWriteOnce" ]
29 resources:
30 requests:
31 storage: 1Gi
作成
まずは、StatefulSetを作成してみます。
1kubectl apply -f statefulset.yml
作成される状況を確認すると、インデックスが小さいPodから作成され、前のPodが作成されてから次のPodが作成開始されているのがわかります。
1❯ kubectl get pod -w
2NAME READY STATUS RESTARTS AGE
3myapp-0 0/1 ContainerCreating 0 2s # 1つ目のPod起動開始
4myapp-0 1/1 Running 0 5s
5myapp-1 0/1 Pending 0 0s # 2つ目のPod起動開始
6myapp-1 0/1 ContainerCreating 0 0s
7myapp-1 1/1 Running 0 3s
8myapp-2 0/1 Pending 0 0s # 3つ目のPod起動開始
9myapp-2 0/1 ContainerCreating 0 0s
10myapp-2 1/1 Running 0 4s
Headless Serviceも作成してみます。
1kubectl apply -f service.yml
下記のコマンドで確認すると、ClusterIPが"None"になっていることが確認できます。
1❯ kubectl get svc
2NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
3myapp-svc ClusterIP None <none> 80/TCP 5s
名前
名前についてもう少し確認してみます。
各Podは一意な名前がついています。
1❯ kubectl get pod
2NAME READY STATUS RESTARTS AGE
3myapp-0 1/1 Running 0 9m47s
4myapp-1 1/1 Running 0 9m42s
5myapp-2 1/1 Running 0 9m39s
Podからホスト名を確認してみても、同じ名前が付けられていることが確認できます。
1❯ kubectl exec myapp-1 -- hostname
2myapp-1
例え、あるPodを削除したとしても同じ名前のPodが削除され、ホスト名も同じになっています。
1❯ kubectl delete pod myapp-1
2pod "myapp-1" deleted
3
4❯ kubectl get pod
5NAME READY STATUS RESTARTS AGE
6myapp-0 1/1 Running 0 10m
7myapp-1 1/1 Running 0 13s
8myapp-2 1/1 Running 0 10m
9
10❯ kubectl exec myapp-1 -- hostname
11myapp-1
マウントするストレージ
次はマウントされているストレージ(PVC)について確認してみます。
PVCを確認すると、volumeClaimTemplates
から作成された各Podに紐づくPVCが確認できます。
1❯ kubectl get pvc
2NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
3html-myapp-0 Bound pvc-aca73200-b48b-4617-8dd6-40832a03a812 1Gi RWO hostpath 3m39s
4html-myapp-1 Bound pvc-c7444acd-52ce-4363-9217-0e299f4c822a 1Gi RWO hostpath 3m33s
5html-myapp-2 Bound pvc-84c45fcc-c6ca-48ee-90ea-3a281be87b00 1Gi RWO hostpath 3m28s
例え、Podを削除したとしても、紐づいていたPVCは削除されずに、再起動した同じ名前のPodに再度マウントされているのが確認できます。
1❯ kubectl describe pod myapp-2 | grep -A 4 Volumes:
2Volumes:
3 html:
4 Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
5 ClaimName: html-myapp-2
6 ReadOnly: false
7
8❯ kubectl delete pod myapp-2
9pod "myapp-2" deleted
10
11❯ kubectl get pvc
12NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
13html-myapp-0 Bound pvc-aca73200-b48b-4617-8dd6-40832a03a812 1Gi RWO hostpath 46m
14html-myapp-1 Bound pvc-c7444acd-52ce-4363-9217-0e299f4c822a 1Gi RWO hostpath 46m
15html-myapp-2 Bound pvc-84c45fcc-c6ca-48ee-90ea-3a281be87b00 1Gi RWO hostpath 46m
16
17❯ kubectl describe pod myapp-2 | grep -A 4 Volumes:
18Volumes:
19 html:
20 Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
21 ClaimName: html-myapp-2
22 ReadOnly: false
Podの更新
Podに更新があった場合の動きについて確認してみます。
下記コマンドでイメージの更新を行ってみます。
1kubectl patch statefulset myapp -p '{"spec":{"template":{"spec":{"containers":[{"name":"web","image":"nginx:1.23"}]}}}}'
インデックスの大きいPodから停止され更新されているのが確認できます。
1❯ kubectl get pod -w
2NAME READY STATUS RESTARTS AGE
3myapp-0 1/1 Running 0 44s
4myapp-1 1/1 Running 0 37s
5myapp-2 1/1 Terminating 0 41s # インデックスが大きいPodから停止
6myapp-2 0/1 Pending 0 0s
7myapp-2 0/1 ContainerCreating 0 0s
8myapp-2 1/1 Running 0 1s # Podの更新が完了
9myapp-1 1/1 Terminating 0 39s # 次のPodが停止
10myapp-1 0/1 Pending 0 0s
11myapp-1 0/1 ContainerCreating 0 0s
12myapp-1 1/1 Running 0 3s # Podの更新が完了
13myapp-0 1/1 Terminating 0 50s # 次のPodが停止
14myapp-0 0/1 Pending 0 0s
15myapp-0 0/1 ContainerCreating 0 0s
16myapp-0 1/1 Running 0 2s # Podの更新が完了
スケール
スケールアウト、スケールインの場合の動きを見てみます。
まずはスケールアウトしてみます。
1kubectl scale --replicas=5 statefulset myapp
インデックスの続きからPodが作成されているのが確認できます。
1❯ kubectl get pod -w
2NAME READY STATUS RESTARTS AGE
3myapp-0 1/1 Running 0 9m28s
4myapp-1 1/1 Running 0 9m32s
5myapp-2 1/1 Running 0 9m34s
6myapp-3 0/1 Pending 0 1s
7myapp-3 0/1 ContainerCreating 0 2s
8myapp-3 1/1 Running 0 4s
9myapp-4 0/1 Pending 0 1s
10myapp-4 0/1 ContainerCreating 0 2s
11myapp-4 1/1 Running 0 4s
次にスケールインしてみます。
1kubectl scale --replicas=3 statefulset myapp
インデックスの大きいPodから終了して、スケールインしています。
1❯ kubectl get pod -w
2NAME READY STATUS RESTARTS AGE
3myapp-0 1/1 Running 0 10m
4myapp-1 1/1 Running 0 10m
5myapp-2 1/1 Running 0 10m
6myapp-3 1/1 Running 0 76s
7myapp-4 1/1 Terminating 0 72s
8myapp-4 0/1 Terminating 0 72s
9myapp-3 1/1 Terminating 0 76s
10myapp-3 0/1 Terminating 0 77s
削除
最後に削除について確認してみます。
StatefulSetを削除してみます。
1kubectl delete statefulset myapp
インデックスが大きいPodから削除されるはずですが、タイミングが早すぎるのかkubectl
の出力的には小さいものから削除されているように見えてしまっています。
1❯ kubectl get pod -w
2NAME READY STATUS RESTARTS AGE
3myapp-0 1/1 Terminating 0 21m
4myapp-1 1/1 Terminating 0 21m
5myapp-2 1/1 Terminating 0 21m
6myapp-0 0/1 Terminating 0 21m
7myapp-0 0/1 Terminating 0 21m
8myapp-1 0/1 Terminating 0 21m
9myapp-1 0/1 Terminating 0 21m
10myapp-2 0/1 Terminating 0 21m
11myapp-2 0/1 Terminating 0 21m
試しにレプリカを10にして、削除してみましたが、出力はバラバラになっていました。
1❯ kubectl get pod -w
2NAME READY STATUS RESTARTS AGE
3myapp-0 1/1 Terminating 0 43s
4myapp-1 1/1 Terminating 0 41s
5myapp-2 1/1 Terminating 0 40s
6myapp-3 1/1 Terminating 0 25s
7myapp-4 1/1 Terminating 0 24s
8myapp-5 1/1 Terminating 0 22s
9myapp-6 1/1 Terminating 0 21s
10myapp-7 1/1 Terminating 0 17s
11myapp-8 1/1 Terminating 0 15s
12myapp-9 1/1 Terminating 0 13s
13myapp-4 0/1 Terminating 0 25s
14myapp-4 0/1 Terminating 0 25s
15myapp-3 0/1 Terminating 0 26s
16myapp-3 0/1 Terminating 0 26s
17myapp-8 0/1 Terminating 0 16s
18myapp-8 0/1 Terminating 0 16s
19myapp-9 0/1 Terminating 0 15s
20myapp-9 0/1 Terminating 0 15s
21myapp-2 0/1 Terminating 0 42s
22myapp-2 0/1 Terminating 0 43s
23myapp-7 0/1 Terminating 0 20s
24myapp-7 0/1 Terminating 0 20s
25myapp-5 0/1 Terminating 0 26s
26myapp-5 0/1 Terminating 0 26s
27myapp-0 0/1 Terminating 0 48s
28myapp-0 0/1 Terminating 0 48s
29myapp-1 0/1 Terminating 0 47s
30myapp-1 0/1 Terminating 0 47s
31myapp-6 0/1 Terminating 0 27s
32myapp-6 0/1 Terminating 0 27s
公式のドキュメントでも、インデックスの逆順から削除されると文章では書いてあるのですが、出力例は逆順にはなっていないので、kubectlの出力の問題なのかもしれません。 https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/#cascading-delete
また、StatefulSetを削除したとしても、Headless ServiceやPVCは削除されずに残っています。
1❯ kubectl get svc
2NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
3myapp-svc ClusterIP None <none> 80/TCP 57m
4
5❯ kubectl get pvc
6NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
7html-myapp-0 Bound pvc-aca73200-b48b-4617-8dd6-40832a03a812 1Gi RWO hostpath 62m
8html-myapp-1 Bound pvc-c7444acd-52ce-4363-9217-0e299f4c822a 1Gi RWO hostpath 62m
9html-myapp-2 Bound pvc-84c45fcc-c6ca-48ee-90ea-3a281be87b00 1Gi RWO hostpath 62m