Deployment 是 K8s 裡最常用的 Workload,但它不是唯一的。你的 PostgreSQL 不該用 Deployment 跑、你的 node-exporter 不該用 Deployment 跑、你的每日報表產生器也不該用 Deployment 跑。K8s 針對不同用途設計了不同的 Workload 類型,選對了省事,選錯了踩坑。

先講結論

Deployment 處理 80% 的無狀態服務場景。但資料庫要用 StatefulSet(穩定 hostname + 有序部署 + 綁定 PVC)、每個 Node 都要跑一份的 agent 用 DaemonSet、一次性任務用 Job、排程任務用 CronJob。然後不管用哪種 Workload,都要設好 Probe 三兄弟——不然 K8s 不知道你的服務到底活著還是死了。


Probe 三兄弟:liveness / readiness / startup

在講 Workload 類型之前,先搞懂 Probe。因為不管你用哪種 Workload,Probe 設錯都會出事。

三個 Probe 各自的職責

Probe問題失敗後果
livenessProbe你還活著嗎?殺掉 Pod 重建
readinessProbe你能接流量嗎?從 Service 的 endpoint 移除(不殺 Pod)
startupProbe你啟動完了嗎?啟動期間不跑 liveness/readiness

最常見的錯誤:liveness probe 打 /health/health 會查 DB。DB 慢了一下,liveness 失敗,K8s 把 Pod 殺掉重啟,所有 Pod 一起重啟,DB 壓力更大,惡性循環。

正確做法:liveness 只檢查 process 還活著(簡單的 HTTP 200 或 TCP port),readiness 才檢查依賴服務。

完整 YAML 範例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api
          image: myapp/api:1.2.0
          ports:
            - containerPort: 8080
          # startup probe:給你 30 次 * 10 秒 = 5 分鐘啟動
          startupProbe:
            httpGet:
              path: /healthz
              port: 8080
            failureThreshold: 30
            periodSeconds: 10
          # liveness:只檢查 process 活著
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 0
            periodSeconds: 15
            timeoutSeconds: 3
            failureThreshold: 3
          # readiness:檢查依賴都 OK
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 0
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi

三種檢查方式

# 1. HTTP GET — 最常用,適合有 HTTP endpoint 的服務
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
 
# 2. TCP Socket — 適合非 HTTP 服務(DB、Redis)
livenessProbe:
  tcpSocket:
    port: 5432
 
# 3. Exec — 跑一個指令,exit code 0 = 健康
livenessProbe:
  exec:
    command:
      - pg_isready
      - -U
      - postgres

StatefulSet:有狀態服務的家

Deployment 把 Pod 當成可以隨便替換的工人——殺掉一個再開一個,名字隨機、PVC 不綁定。但資料庫不能這樣搞。

StatefulSet 跟 Deployment 的差異

特性DeploymentStatefulSet
Pod 名稱隨機(api-7d4f8b-x9k2z有序(postgres-0, postgres-1
啟動順序同時啟動按順序(0 → 1 → 2)
PVC共享或不綁定每個 Pod 有自己的 PVC
刪除行為PVC 跟 Pod 一起刪PVC 保留(資料不丟)
網路隨機 IP穩定的 DNS(postgres-0.postgres-svc

PostgreSQL on K8s 範例

apiVersion: v1
kind: Service
metadata:
  name: postgres-svc
  labels:
    app: postgres
spec:
  # Headless Service:不分配 ClusterIP,讓每個 Pod 有自己的 DNS
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-svc
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_DB
              value: myapp
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
          livenessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 1Gi
  # 每個 Pod 自動建立一個 PVC
  volumeClaimTemplates:
    - metadata:
        name: postgres-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: local-path
        resources:
          requests:
            storage: 10Gi

Pod 名稱會是 postgres-0,對應的 PVC 是 postgres-data-postgres-0。就算 Pod 被砍掉重建,PVC 還在,資料不會丟。

實務建議:生產環境的資料庫建議用 Operator(CloudNativePG、Zalando Postgres Operator),不要自己手寫 StatefulSet。Operator 幫你處理 failover、backup、replica 的邏輯,你只需要寫一個 CR(Custom Resource)。


DaemonSet:每個 Node 跑一份

有些服務不是「跑 N 個副本」的概念,而是「每個 Node 上都要有一份」。典型場景:

  • node-exporter:收集每個 Node 的 CPU/RAM/Disk 指標
  • fluentd / fluent-bit:收集每個 Node 上所有 Pod 的 log
  • kube-proxy:K8s 內建的網路組件(本身就是 DaemonSet)

node-exporter DaemonSet YAML

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
  labels:
    app: node-exporter
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      hostNetwork: true
      hostPID: true
      containers:
        - name: node-exporter
          image: prom/node-exporter:v1.8.1
          args:
            - --path.rootfs=/host
            - --path.procfs=/host/proc
            - --path.sysfs=/host/sys
            - --collector.filesystem.mount-points-exclude
            - ^/(dev|proc|sys|var/lib/docker/.+|var/lib/kubelet/.+)($|/)
          ports:
            - containerPort: 9100
              hostPort: 9100
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi
          volumeMounts:
            - name: host-root
              mountPath: /host
              readOnly: true
      volumes:
        - name: host-root
          hostPath:
            path: /
      tolerations:
        # 讓 DaemonSet 也能跑在 master/control-plane node 上
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule

注意 tolerations:K8s 的 control-plane node 預設有 taint,一般 Pod 不會被排上去。但 node-exporter 需要跑在所有 Node 上,所以要加 toleration。


Job:跑完就結束的任務

不是所有任務都是「一直跑著」的。資料庫 migration、批次報表、資料清理——這些跑完就該結束。

一次性 Job

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  backoffLimit: 3    # 失敗最多重試 3 次
  activeDeadlineSeconds: 600  # 最多跑 10 分鐘
  template:
    spec:
      containers:
        - name: migrate
          image: myapp/api:1.2.0
          command: ["python", "manage.py", "migrate"]
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: url
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
      restartPolicy: Never   # Job 只能用 Never 或 OnFailure

CronJob:排程任務

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report
spec:
  schedule: "0 2 * * *"   # 每天凌晨 2 點
  timeZone: "Asia/Taipei"
  concurrencyPolicy: Forbid  # 上一個還沒跑完就不要再開新的
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 1800   # 最多跑 30 分鐘
      template:
        spec:
          containers:
            - name: report
              image: myapp/report-generator:1.0.0
              command: ["python", "generate_report.py"]
              env:
                - name: DATABASE_URL
                  valueFrom:
                    secretKeyRef:
                      name: db-secret
                      key: url
                - name: S3_BUCKET
                  value: my-reports-bucket
              resources:
                requests:
                  cpu: 200m
                  memory: 512Mi
                limits:
                  cpu: "1"
                  memory: 1Gi
          restartPolicy: OnFailure

CronJob 的 concurrencyPolicy

行為
Allow預設。可以同時跑多個 Job
Forbid上一個還在跑就跳過這次
Replace上一個還在跑就砍掉,開新的

我的建議:除非你很確定任務可以平行跑,否則用 Forbid。一個報表跑兩份,不是效率加倍,是 DB 壓力加倍。


Workload 選擇指南

場景用什麼原因
Web API / 微服務Deployment無狀態,可以隨便殺
資料庫 / Redis / KafkaStatefulSet需要穩定 hostname 和持久化儲存
監控 agent / log collectorDaemonSet每個 Node 都要有
DB migration / 資料清理Job跑完就結束
每日報表 / 定時清理CronJob排程觸發

不確定的時候先用 Deployment。等你發現 Deployment 滿足不了需求(需要穩定 hostname、需要有序部署、需要每個 Node 一份),再換對應的 Workload 類型。


常見踩坑

StatefulSet 縮容很慢:它是有序的,scale down 從最大編號開始一個一個砍,不像 Deployment 可以同時砍多個。

Job 忘記設 activeDeadlineSeconds:某個 bug 讓 Job 卡住永遠跑不完,沒有 deadline 就會一直佔著資源。

CronJob 的 timezonetimeZone 是 K8s 1.27+ 才穩定的功能。舊版本的 schedule 是 UTC,台灣時間要自己換算。

liveness probe 太敏感periodSeconds: 3 + failureThreshold: 1 = 3 秒沒回應就殺 Pod。GC pause 一下就被殺了。建議 periodSeconds: 15 + failureThreshold: 3 = 45 秒容忍時間。


Deployment 是 K8s 的 “Hello World”,但真正的生產環境不會只有 Deployment。搞懂每種 Workload 的適用場景,才不會拿著鐵鎚看什麼都像釘子。


延伸閱讀


本系列文章

  • 本篇:Workloads:Deployment 之外的世界
  • 下一篇: DNS