前面我們學會了 Deployment 管理無狀態應用,就像管理一群「替身演員」,誰上場都一樣。但現實中有些應用是「主角級」的,每個都有獨特身份和專屬道具,比如資料庫的主從架構、分散式系統的節點編號等。今天我們要學習 StatefulSet,它就是專門管理這些「有個性」應用的管家!
想像一下交響樂團:小提琴手可以互相替換(無狀態),但首席小提琴、指揮、鋼琴家都有固定位置和專屬樂器(有狀態)。如果指揮突然換人,整個樂團都會亂套!StatefulSet 就是確保每個「音樂家」都能保持自己身份和樂器的專業管家。
✅ 理解 StatefulSet 與 Deployment 的核心差異
✅ 掌握穩定網路身份與持久化儲存機制
✅ 學會有序部署、擴縮容與滾動更新
✅ 實作 PostgreSQL 佈署
StatefulSet
是 K8s 中專門管理有狀態應用的控制器,提供:
💡 生活化比喻:如果說 Deployment 像是管理「便利商店店員」(誰都能做相同工作),那 StatefulSet 就像是管理「醫院科室」(心臟科醫生、腦外科醫生各有專精,不能隨意替換)。
有關
有狀態的應用
,能參考小弟以前的文章微服務瞎談(3) 微服務的拆分
我們在賢者大叔第四天介紹 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/
# 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
# 每個 Pod 都有專屬的 PVC
postgres-data-postgres-0 # postgres-0 的專屬儲存
postgres-data-postgres-1 # postgres-1 的專屬儲存
postgres-data-postgres-2 # postgres-2 的專屬儲存
# 啟動順序:0 → 1 → 2
# 停止順序:2 → 1 → 0
# 更新順序:2 → 1 → 0 (預設)
首先建立 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"
postgres-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: database
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)
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 記錄。
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
# 執行部署
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
# 檢查 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
關鍵觀察點:
# 檢查 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)
預期結果: 資料完整保留!
# 擴展到 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
# 更新 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,我們就能在 Kubernetes 中優雅地管理各種複雜的有狀態應用了!