今天學 StatefulSet,說明為什麼資料庫不能用 Deployment 跑,以及 K8s 怎麼處理有狀態的服務 !
Deployment 的設計假設每個 Pod 都是無狀態、可替換的。任何一個 Pod 掛掉,Deployment 重建一個新的,服務繼續跑,完全沒有影響——因為每個 Pod 都一樣,沒有哪個比較特別。
但資料庫不是這樣的。如果用 Deployment 跑 MySQL:
問題一:Pod 名稱不固定
Deployment 的 Pod 名稱是隨機生成的(mysql-6d4b9f8c7-k2xpq),每次重建名稱都不同。如果資料庫叢集(例如 MySQL 主從複寫)需要每個節點有固定的身份,這樣行不通。
問題二:儲存不固定
Deployment 的多個副本通常共用同一份設定,但資料庫的每個副本需要各自獨立的儲存空間。主節點和從節點的資料不能混在一起。
問題三:啟動和關閉順序不保證
資料庫叢集通常需要按照特定順序啟動(主節點先、從節點後),Deployment 不保證這個順序。StatefulSet 就是為了這些有狀態的服務設計的。
StatefulSet 跟 Deployment 很像,也是管理一組 Pod,但它給每個 Pod 提供:
mysql-0、mysql-1、mysql-2,固定有序,不會隨機變| Deployment | StatefulSet | |
|---|---|---|
| Pod 名稱 | 隨機(api-6d4b9f8c7-k2xpq) |
有序固定(mysql-0、mysql-1) |
| 儲存 | 所有副本共用 | 每個副本獨立 |
| 啟動順序 | 不保證 | 按序啟動(0 → 1 → 2) |
| 關閉順序 | 不保證 | 反序關閉(2 → 1 → 0) |
| 適合情境 | 無狀態服務(API、前端) | 有狀態服務(資料庫、訊息佇列) |
今天目標:刪除舊的 mysql Deployment,改用 StatefulSet 跑 MySQL,搭配 Headless Service 讓每個 Pod 有固定 DNS 名稱,並確認 MySQL 正常啟動、tododb 資料庫存在。
mysql-deployment.yaml 到這邊就沒用了,換成 StatefulSet 之前先刪掉它,避免兩個 MySQL 同時跑:kubectl delete -f mysql-deployment.yaml
mysql-deployment.yaml :rm mysql-deployment.yaml
補充一下,
mysql-deployment.yaml原本就有幾個問題,正好也是今天要換掉它的原因:
- 用 Deployment 管理資料庫,Pod 名稱隨機、沒有順序保證(就是上一節說的那些問題)
- 完全沒有掛載 volume,Pod 重建資料就沒了
- 密碼是明文寫在 YAML 裡的
env.value,沒有用 Secret換成 StatefulSet 之後,這幾個問題會逐步解決:密碼改用 Secret,Pod 名稱固定有序,儲存也在今天先用
emptyDir頂著(Day 13 再換成 PVC 真正持久化)。
kubectl create secret generic mysql-secret \
--from-literal=MYSQL_ROOT_PASSWORD=rootpassword \
--from-literal=MYSQL_PASSWORD=password
kubectl get secrets
可以看到有多一筆我們剛剛建立的mysql Secret :
/Todo-App/k8s 目錄下,建立 mysql-statefulset.yaml:apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-headless # 對應的 Headless Service 名稱
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
tier: database
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: MYSQL_ROOT_PASSWORD
- name: MYSQL_DATABASE
value: "tododb"
- name: MYSQL_USER
value: "user"
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: MYSQL_PASSWORD
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumes:
- name: mysql-data
emptyDir: {} # 暫時用 emptyDir,Day 13 換成 PVC
kubectl apply -f mysql-statefulset.yaml
kubectl get statefulsets

kubectl get pods

MySQL Pod 名稱是 mysql-0,固定有序,不是隨機生成的。
StatefulSet 需要搭配一個 Headless Service,讓每個 Pod 有固定的 DNS 名稱。
在 /Todo-App/k8s 目錄下,建立 mysql-headless-service.yaml :
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None # clusterIP 設成 None 就是 Headless Service
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
kubectl apply -f mysql-headless-service.yaml
有了 Headless Service,每個 Pod 就有固定的 DNS 名稱:
mysql-0.mysql-headless.default.svc.cluster.local
格式是 <pod名稱>.<headless service名稱>.<namespace>.svc.cluster.local。
除了 Headless Service,還需要一個一般的 ClusterIP Service 讓 todo-api 連進來。
調整 /Todo-App/k8s/mysql-service.yaml :
apiVersion: v1
kind: Service
metadata:
name: mysql-service
spec:
type: ClusterIP
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
kubectl apply -f mysql-service.yaml
todo-api 之後連資料庫就用 mysql-service:3306。
kubectl exec -it mysql-0 -- mysql -u root -prootpassword -e "SHOW DATABASES;"

tododb 有出現就代表 MySQL 正常跑起來了,帳號密碼也跟 todo-api-secret 的 DATABASE_URL 一致,todo-api 可以順利連線。
一般的 Service 有一個 ClusterIP,DNS 解析到的是這個虛擬 IP,再由 kube-proxy 做負載平衡分配到後面的 Pod。
Headless Service 把 clusterIP 設成 None,沒有虛擬 IP。DNS 解析直接回傳所有 Pod 的實際 IP,讓呼叫方自己決定要連哪個 Pod。
這對資料庫叢集很重要——主節點和從節點不能隨機分配,應用程式需要明確指定連哪個節點(例如寫入要連主節點 mysql-0,讀取可以連從節點 mysql-1)。Headless Service 讓每個 Pod 有固定的 DNS 名稱,應用程式可以精確定址。
今天學了 StatefulSet,處理有狀態的服務:
mysql-0 一定比 mysql-1 先跑起來今天用 emptyDir 暫代儲存,Pod 重啟後資料還是會消失。明天會學 PV 和 PVC,把儲存真正持久化 !