iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

今天學 StatefulSet,說明為什麼資料庫不能用 Deployment 跑,以及 K8s 怎麼處理有狀態的服務 !


Deployment 的問題:無狀態假設

Deployment 的設計假設每個 Pod 都是無狀態、可替換的。任何一個 Pod 掛掉,Deployment 重建一個新的,服務繼續跑,完全沒有影響——因為每個 Pod 都一樣,沒有哪個比較特別。

但資料庫不是這樣的。如果用 Deployment 跑 MySQL:

問題一:Pod 名稱不固定

Deployment 的 Pod 名稱是隨機生成的(mysql-6d4b9f8c7-k2xpq),每次重建名稱都不同。如果資料庫叢集(例如 MySQL 主從複寫)需要每個節點有固定的身份,這樣行不通。

問題二:儲存不固定

Deployment 的多個副本通常共用同一份設定,但資料庫的每個副本需要各自獨立的儲存空間。主節點和從節點的資料不能混在一起。

問題三:啟動和關閉順序不保證

資料庫叢集通常需要按照特定順序啟動(主節點先、從節點後),Deployment 不保證這個順序。StatefulSet 就是為了這些有狀態的服務設計的。


StatefulSet 是什麼

StatefulSet 跟 Deployment 很像,也是管理一組 Pod,但它給每個 Pod 提供:

  • 穩定的 Pod 名稱:mysql-0、mysql-1、mysql-2,固定有序,不會隨機變
  • 穩定的網路身份:每個 Pod 有固定的 DNS 名稱
  • 獨立的儲存:每個 Pod 有自己的 PersistentVolumeClaim,不共用

StatefulSet vs Deployment

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 資料庫存在。

Step1: 移除舊的 mysql-deployment

  1. 前幾天用 Deployment 建立的 mysql-deployment.yaml 到這邊就沒用了,換成 StatefulSet 之前先刪掉它,避免兩個 MySQL 同時跑:
kubectl delete -f mysql-deployment.yaml
  1. 刪掉 cluster 上的資源之後,也刪除本機的 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 真正持久化)。

Step2: 建立 MySQL Secret

kubectl create secret generic mysql-secret \
  --from-literal=MYSQL_ROOT_PASSWORD=rootpassword \
  --from-literal=MYSQL_PASSWORD=password
kubectl get secrets

可以看到有多一筆我們剛剛建立的mysql Secret :
https://ithelp.ithome.com.tw/upload/images/20260921/20183863Icj94SRM7s.png

Step3: 建立 StatefulSet

  1. 在 /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
  1. 確認狀態:
kubectl get statefulsets

https://ithelp.ithome.com.tw/upload/images/20260921/20183863Nta0wUdOFI.png

  1. 看 Pod 名稱:
kubectl get pods

https://ithelp.ithome.com.tw/upload/images/20260921/20183863dR3DmzYuKX.png

MySQL Pod 名稱是 mysql-0,固定有序,不是隨機生成的。

Step3: 建立 Headless Service

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。

Step4: 建立對外的 ClusterIP Service

除了 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。

Step5: 確認 MySQL 正常運作

kubectl exec -it mysql-0 -- mysql -u root -prootpassword -e "SHOW DATABASES;"

https://ithelp.ithome.com.tw/upload/images/20260921/20183863J2ctMtSGC1.png

tododb 有出現就代表 MySQL 正常跑起來了,帳號密碼也跟 todo-api-secret 的 DATABASE_URL 一致,todo-api 可以順利連線。


延伸筆記:Headless Service

一般的 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,處理有狀態的服務:

  • Deployment 假設 Pod 無狀態可替換,不適合資料庫
  • StatefulSet 提供穩定的 Pod 名稱、網路身份和獨立儲存
  • 啟動順序有保證:mysql-0 一定比 mysql-1 先跑起來
  • 搭配 Headless Service:讓每個 Pod 有固定的 DNS 名稱

今天用 emptyDir 暫代儲存,Pod 重啟後資料還是會消失。明天會學 PV 和 PVC,把儲存真正持久化 !


上一篇
Day 11|Secret
下一篇
Day 13|PV 和 PVC
系列文
從零學 K8s|30 天核心概念 × 實作,新手也能真正掌握 Kubernetes 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言