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