iT邦幫忙

2025 iThome 鐵人賽

DAY 13
2
DevOps

賢者大叔的容器修煉手札系列 第 13

StatefulSet - 有狀態應用的專屬管家 👑

  • 分享至 

  • xImage
  •  

賢者大叔的容器修煉手札系列 第 13 篇

StatefulSet - 有狀態應用的專屬管家 👑

前面我們學會了 Deployment 管理無狀態應用,就像管理一群「替身演員」,誰上場都一樣。但現實中有些應用是「主角級」的,每個都有獨特身份和專屬道具,比如資料庫的主從架構、分散式系統的節點編號等。今天我們要學習 StatefulSet,它就是專門管理這些「有個性」應用的管家!
想像一下交響樂團:小提琴手可以互相替換(無狀態),但首席小提琴、指揮、鋼琴家都有固定位置和專屬樂器(有狀態)。如果指揮突然換人,整個樂團都會亂套!StatefulSet 就是確保每個「音樂家」都能保持自己身份和樂器的專業管家。

今日學習目標 🎯

✅ 理解 StatefulSet 與 Deployment 的核心差異

✅ 掌握穩定網路身份與持久化儲存機制

✅ 學會有序部署、擴縮容與滾動更新

✅ 實作 PostgreSQL 佈署

StatefulSet 核心概念 🧠

什麼是 StatefulSet?

StatefulSet 是 K8s 中專門管理有狀態應用的控制器,提供:

  • 穩定的網路身份 - 每個 Pod 都有固定的主機名稱
  • 持久化儲存 - 每個 Pod 都有專屬的儲存卷
  • 有序部署和擴縮容 - 按照編號順序進行操作
  • 有序滾動更新 - 確保更新過程的穩定性

💡 生活化比喻:如果說 Deployment 像是管理「便利商店店員」(誰都能做相同工作),那 StatefulSet 就像是管理「醫院科室」(心臟科醫生、腦外科醫生各有專精,不能隨意替換)。

有關有狀態的應用,能參考小弟以前的文章微服務瞎談(3) 微服務的拆分

StatefulSet vs Deployment 對比 📊

我們在賢者大叔第四天介紹 Deployment,其中有提到 Deployment 適用於管理 stateless application 的生命週期。

所以我們就能兩者相互對比

特性 Deployment StatefulSet
Pod 身份 隨機名稱 (nginx-abc123) 有序名稱 (mysql-0, mysql-1)
網路身份 不穩定,重啟後變化 穩定,重啟後保持
儲存 共享或無狀態 每個 Pod 專屬 PVC
啟動順序 並行啟動 順序啟動 (0→1→2)
擴縮容 並行操作 順序操作
更新策略 並行滾動更新 有序滾動更新
適用場景 Web 服務、API 資料庫、分散式系統
DNS 記錄 Service 級別 Pod 級別 + Service 級別

Deployment Pod 命名

nginx-deployment-7d4b8f9c8-abc12  ← 隨機後綴
nginx-deployment-7d4b8f9c8-def34  ← 隨機後綴
nginx-deployment-7d4b8f9c8-ghi56  ← 隨機後綴

StatefulSet Pod 命名

postgres-0  ← 固定編號,永遠是 0
postgres-1  ← 固定編號,永遠是 1
postgres-2  ← 固定編號,永遠是 2

DNS 解析差異
Deployment

# 只能解析到 Service,無法指定特定 Pod
curl http://nginx-service/

StatefulSet

# 可以解析到特定 Pod
curl http://postgres-0.postgres-service/
curl http://postgres-1.postgres-service/
curl http://postgres-2.postgres-service/

StatefulSet 的三大保證 🛡️

  1. 穩定的網路身份
    從剛剛的命名不難理解
# Pod 名稱格式:<statefulset-name>-<ordinal>
postgres-0  # 主節點,編號永遠是 0
postgres-1  # 從節點 1
postgres-2  # 從節點 2

# DNS 名稱格式:<pod-name>.<service-name>.<namespace>.svc.cluster.local
postgres-0.postgres-service.database.svc.cluster.local
postgres-1.postgres-service.database.svc.cluster.local
  1. 持久化儲存
# 每個 Pod 都有專屬的 PVC
postgres-data-postgres-0  # postgres-0 的專屬儲存
postgres-data-postgres-1  # postgres-1 的專屬儲存
postgres-data-postgres-2  # postgres-2 的專屬儲存
  1. 有序操作
# 啟動順序:0 → 1 → 2
# 停止順序:2 → 1 → 0
# 更新順序:2 → 1 → 0 (預設)

StatefulSet 工作流程圖 📊

https://ithelp.ithome.com.tw/upload/images/20250828/201049303c0utpksvr.png

實戰演練:PostgreSQL 部署 🗄️

環境準備 🛠️

首先建立 Kind 集群配置:
kind-config.yaml

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: postgres-cluster
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true"
  extraPortMappings:
  - containerPort: 31432  # 匹配 NodePort
    hostPort: 5432        # 本機 5432 端口
    protocol: TCP
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
- role: worker
  labels:
    storage-node: "true"
- role: worker
  labels:
    storage-node: "true"

完整配置文件 📝

  1. 命名空間
    postgres-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: database
  1. 密鑰配置
    postgres-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  namespace: database
type: Opaque
data:
  postgres-user: cG9zdGdyZXM=        # postgres (base64)
  postgres-password: YWRtaW4xMjM=    # admin123 (base64)
  postgres-db: bXlhcHA=              # myapp (base64)
  1. 網路配置
    postgres-services.yaml
# Headless Service for StatefulSet (required)
apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
  namespace: database
  labels:
    app: postgres
spec:
  ports:
  - port: 5432
    name: postgres
  clusterIP: None
  selector:
    app: postgres
---
# External access service
apiVersion: v1
kind: Service
metadata:
  name: postgres-external
  namespace: database
  labels:
    app: postgres
spec:
  type: NodePort
  ports:
  - port: 5432
    targetPort: 5432
    nodePort: 31432
    name: postgres
  selector:
    app: postgres

💡 重點說明:Headless Service (clusterIP: None) 是 StatefulSet 的必要條件,它讓每個 Pod 都能有獨立的 DNS 記錄。

  1. StatefulSet 主配置
    postgres-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: database
spec:
  serviceName: postgres-headless
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16-alpine
        ports:
        - containerPort: 5432
          name: postgres
        env:
        - name: POSTGRES_USER
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: postgres-user
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: postgres-password
        - name: POSTGRES_DB
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: postgres-db
        - name: PGDATA
          value: "/var/lib/postgresql/data/pgdata"
        volumeMounts:
        - name: postgres-data
          mountPath: /var/lib/postgresql/data
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          exec:
            command:
            - sh
            - -c
            - "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"
          initialDelaySeconds: 60
          periodSeconds: 30
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
  volumeClaimTemplates:
  - metadata:
      name: postgres-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: standard
      resources:
        requests:
          storage: 2Gi

部署腳本 🚀
deploy.sh

#!/bin/bash
set -e

echo "🧹 清理現有資源..."
pkill -f "kubectl port-forward" || true
kind delete cluster --name postgres-cluster || true

echo "🚀 創建 Kind 集群..."
kind create cluster --config kind-config.yaml

echo "📦 部署 PostgreSQL..."
kubectl apply -f postgres-namespace.yaml
kubectl apply -f postgres-secret.yaml
kubectl apply -f postgres-services.yaml
kubectl apply -f postgres-statefulset.yaml

echo "⏳ 等待 Pod 就緒..."
kubectl wait --for=condition=ready pod -l app=postgres -n database --timeout=300s

echo "✅ 部署完成!"
echo "📊 資源狀態:"
kubectl get all -n database

echo "🔌 連接信息:"
echo "Host: localhost"
echo "Port: 5432"
echo "Database: myapp"
echo "User: postgres"
echo "Password: admin123"

echo "🧪 測試連接..."
kubectl exec -n database postgres-0 -- psql -U postgres -d myapp -c "SELECT 'Connection successful!' as status;"

我還能用 DataGrip 來連接到剛剛佈署的 database,
因為有設定了 NodePort
https://ithelp.ithome.com.tw/upload/images/20250828/20104930SzY9Aqv62g.png

https://ithelp.ithome.com.tw/upload/images/20250828/20104930FjATuZMKAI.png

StatefulSet 操作 🚀

  1. 部署
# 執行部署
chmod +x deploy.sh
./deploy.sh

# 觀察 Pod 啟動過程
kubectl get pods -n database -w

# 檢查 StatefulSet 狀態
kubectl get statefulset -n database
# NAME       READY   AGE
# postgres   1/1     9m22s

kubectl describe statefulset postgres -n database
  1. 驗證穩定網路身份
# 檢查 Pod 名稱(固定編號)
kubectl get pods -n database
# NAME         READY   STATUS    RESTARTS   AGE
# postgres-0   1/1     Running   0          10m

# 重啟 Pod 後驗證名稱不變
kubectl delete pod postgres-0 -n database
kubectl get pods -n database -w
# 新 Pod 仍然是 postgres-0
# NAME         READY   STATUS    RESTARTS   AGE
# postgres-0   0/1     Running   0          5s
# postgres-0   1/1     Running   0          32s

關鍵觀察點:

  • 新 Pod 仍然是 postgres-0
  • DNS 名稱保持不變
  • 網路身份完全穩定
  1. 驗證持久化儲存
# 檢查 PVC
kubectl get pvc -n database
# 輸出:postgres-data-postgres-0,取 Status 是 Bound
# NAME                       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
# postgres-data-postgres-0   Bound    pvc-7dcf3fb9-2d53-453d-bc5e-a2c158168e2b   2Gi        RWO            standard       <unset>                 11m

# 在資料庫中創建測試資料
kubectl exec -n database postgres-0 -- psql -U postgres -d myapp -c "
CREATE TABLE test_table (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO test_table (name) VALUES ('StatefulSet Test');
"

# CREATE TABLE
# INSERT 0 1

# 刪除 Pod 測試資料持久性
kubectl delete pod postgres-0 -n database

# 等待新 Pod 啟動後檢查資料
kubectl wait --for=condition=ready pod postgres-0 -n database --timeout=120s
kubectl exec -n database postgres-0 -- psql -U postgres -d myapp -c "SELECT * FROM test_table;"
# 資料應該還在!
#  id |       name       |         created_at         
# ----+------------------+----------------------------
#  1 | StatefulSet Test | 2025-08-27 16:41:08.184034
# (1 row)

預期結果: 資料完整保留!

  1. 擴縮容操作
# 擴展到 3 個副本
kubectl scale statefulset postgres --replicas=3 -n database

# 觀察有序啟動:postgres-0 → postgres-1 → postgres-2
kubectl get pods -n database -w

# NAME         READY   STATUS    RESTARTS   AGE
# postgres-0   1/1     Running   0          74s
# postgres-1   0/1     Pending   0          3s
# postgres-1   0/1     Pending   0          4s
# postgres-1   0/1     ContainerCreating   0          4s
# postgres-1   0/1     Running             0          15s
# postgres-1   1/1     Running             0          46s
# postgres-2   0/1     Pending             0          0s
# postgres-2   0/1     Pending             0          3s
# postgres-2   0/1     ContainerCreating   0          3s
# postgres-2   0/1     Running             0          4s


# 檢查每個 Pod 的專屬 PVC
kubectl get pvc -n database
# NAME                       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
# postgres-data-postgres-0   Bound    pvc-7dcf3fb9-2d53-453d-bc5e-a2c158168e2b   2Gi        RWO            standard       <unset>                 17m
# postgres-data-postgres-1   Bound    pvc-a0b2c29a-99c8-4524-806f-f04f977a0cdc   2Gi        RWO            standard       <unset>                 86s
# postgres-data-postgres-2   Bound    pvc-2e03423b-f43a-444c-b8c2-da507174c8f7   2Gi        RWO            standard       <unset>                 40s

# 縮容到 1 個副本
kubectl scale statefulset postgres --replicas=1 -n database

# 觀察有序停止:postgres-2 → postgres-1 (postgres-0 保留)
kubectl get pods -n database -w

# NAME         READY   STATUS    RESTARTS   AGE
# postgres-0   1/1     Running   0          3m14s
  1. 滾動更新
# 更新 PostgreSQL 版本
kubectl patch statefulset postgres -n database -p='{"spec":{"template":{"spec":{"containers":[{"name":"postgres","image":"postgres:15-alpine"}]}}}}'

# 觀察有序更新過程
kubectl rollout status statefulset/postgres -n database
kubectl get pods -n database -w

總結:StatefulSet 完整生命週期 🔄

StatefulSet 就像是有狀態應用的專屬管家,確保每個「貴賓」都能保持自己的身份和專屬服務!🎭 掌握了 StatefulSet,我們就能在 Kubernetes 中優雅地管理各種複雜的有狀態應用了!

https://ithelp.ithome.com.tw/upload/images/20250828/201049306lME4gdNrr.png


上一篇
DNS 服務發現 - 微服務的電話簿 📞
下一篇
DevSpace - 雲原生開發的好幫手 ⚡
系列文
賢者大叔的容器修煉手札17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言