Workloads: Deployment, rolling update, StatefulSet, DaemonSet, Job

5 câu hỏi phỏng vấn~30 phútTừ khóa: Deployment, ReplicaSet, label, selector, self-healing, RollingUpdate, maxSurge, maxUnavailable, rollout undo, Recreate, StatefulSet, volumeClaimTemplates, DaemonSet, Job, CronJob

Tiếp đi: Deployment, rolling update và các loại workload.

Điểm quan trọng nhất bài này: Deployment → ReplicaSet → Pod → Container, và mỗi kiểu ứng dụng có một controller riêng.

1. Bài toán

Hệ thống của ta cần bốn kiểu chạy khác nhau:

order-service, payment-service  → stateless, luôn 3 bản, deploy v2 không downtime
postgres                        → có data, cần tên ổn định và ổ đĩa riêng
log agent                       → mỗi node đúng 1 bản
đối soát thanh toán             → chạy 2h sáng mỗi ngày, xong thì dừng

2. Vấn đề

3. Giải pháp: chọn đúng controller

WorkloadDùng choĐặc điểm
Deploymentorder, payment (stateless)Pod giống hệt nhau, tên random, rolling update, rollback
StatefulSetpostgres, Kafka, RedisTên ổn định postgres-0, PVC riêng từng Pod, start/stop theo thứ tự
DaemonSetlog agent, node exporter, CNIMỗi node 1 Pod, node mới vào tự có
Jobmigration, import một lầnChạy tới khi thành công, có retry
CronJobđối soát mỗi đêmTạo Job theo lịch cron

4. Code từng bước

Bước 1: Deployment cho payment-service

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service
spec:
  replicas: 3
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: payment-service        # phải khớp label của template
  template:                       # khuôn để tạo Pod
    metadata:
      labels:
        app: payment-service
    spec:
      containers:
        - name: payment
          image: myregistry/payment-service:1.0
          ports: [ { containerPort: 8080 } ]
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: 8080 }

Label + selector là cách K8s ghép đồ vật. Deployment không quản lý Pod theo tên (tên Pod thay đổi mỗi lần tạo lại) mà theo câu "Pod nào có app=payment-service":

selector app=payment-service
  Pod payment-...-abc12   app=payment-service   ✅
  Pod payment-...-def34   app=payment-service   ✅
  Pod order-...-x1y2z     app=order-service     ❌
  Pod postgres-0          app=postgres          ❌

Bước 2: Apply, xem cây resource, test self-healing

kubectl apply -f payment-deployment.yaml
kubectl get deploy,rs,pods -l app=payment-service
# deployment/payment-service   3/3
# replicaset/payment-service-7c84c6f789   3   3   3
# pod/payment-service-7c84c6f789-abc12   1/1 Running
# ...
kubectl delete pod payment-service-7c84c6f789-abc12
kubectl get pods -l app=payment-service     # có Pod mới thay thế ngay
kubectl scale deployment payment-service --replicas=5

Bước 3: Rolling update và rollback

# cách declarative: sửa image trong YAML rồi apply
kubectl apply -f payment-deployment.yaml
# cách nhanh (nhớ cập nhật lại file cho khớp)
kubectl set image deployment/payment-service payment=myregistry/payment-service:2.0
kubectl annotate deployment/payment-service kubernetes.io/change-cause="payment 2.0"

kubectl rollout status deployment/payment-service
kubectl get pods -w                              # xem Pod thay dần
kubectl rollout history deployment/payment-service
kubectl rollout undo deployment/payment-service  # về revision trước
kubectl rollout undo deployment/payment-service --to-revision=1
kubectl rollout restart deployment/payment-service  # tạo lại Pod, cùng image

Bước 4: StatefulSet cho postgres, CronJob cho đối soát

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres          # headless Service cho DNS postgres-0.postgres
  replicas: 1
  selector:
    matchLabels: { app: postgres }
  template:
    metadata:
      labels: { app: postgres }
    spec:
      containers:
        - name: postgres
          image: postgres:17
          env:
            - name: POSTGRES_DB
              value: shop
            - name: POSTGRES_USER
              valueFrom: { secretKeyRef: { name: postgres-secret, key: username } }
            - name: POSTGRES_PASSWORD
              valueFrom: { secretKeyRef: { name: postgres-secret, key: password } }
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:          # mỗi Pod một PVC riêng: data-postgres-0, data-postgres-1...
    - metadata:
        name: data
      spec:
        accessModes: [ "ReadWriteOnce" ]
        resources:
          requests:
            storage: 10Gi
apiVersion: batch/v1
kind: CronJob
metadata:
  name: payment-reconcile
spec:
  schedule: "0 2 * * *"          # 2h sáng mỗi ngày
  timeZone: "Asia/Ho_Chi_Minh"
  concurrencyPolicy: Forbid      # lần trước chưa xong thì không chạy chồng
  jobTemplate:
    spec:
      backoffLimit: 3            # retry tối đa 3 lần
      activeDeadlineSeconds: 3600
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: reconcile
              image: myregistry/payment-service:2.0
              args: ["--spring.profiles.active=reconcile-job"]
kubectl get sts,pvc
kubectl get cronjob,job
kubectl create job --from=cronjob/payment-reconcile reconcile-manual   # chạy thử ngay

5. Cách hoạt động

Rolling update: Deployment quản lý hai ReplicaSet

Deployment payment-service
    ├── ReplicaSet v1 (image 1.0)   replicas 3 → 2 → 1 → 0  (giữ lại để rollback)
    └── ReplicaSet v2 (image 2.0)   replicas 0 → 1 → 2 → 3

Với replicas: 3, maxSurge: 1, maxUnavailable: 0:

STEP 0   v1✅ v1✅ v1✅              total 3, available 3
STEP 1   v1✅ v1✅ v1✅ v2⏳          total 4 (surge +1), v2 chưa Ready → chưa nhận traffic
STEP 2   v2 Ready → xóa 1 v1
         v1✅ v1✅ v2✅              available 3
STEP 3   v1✅ v1✅ v2✅ v2⏳
 ...
END      v2✅ v2✅ v2✅

Nếu v2 lỗi (readiness không pass), với maxUnavailable: 0 rollout bị kẹt ở STEP 1, ba Pod v1 vẫn phục vụ. Kẹt còn hơn sập. Sau progressDeadlineSeconds (mặc định 600s) Deployment đánh dấu ProgressDeadlineExceeded; K8s không tự rollback, bạn phải rollout undo hoặc để pipeline làm.

StatefulSet: thứ tự và danh tính

Tạo:   postgres-0 Ready → postgres-1 Ready → postgres-2
Xóa:   postgres-2 → postgres-1 → postgres-0
postgres-1 chết → Pod mới vẫn tên postgres-1, gắn lại đúng PVC data-postgres-1
DNS:   postgres-0.postgres.shop.svc.cluster.local

6. So sánh

DeploymentStatefulSetDaemonSetJob/CronJob
Tên Podrandomname-0, name-1randomrandom
Storagechung hoặc khôngPVC riêng từng Podthường hostPathtùy
Số Podreplicasreplicas= số nodechạy tới khi xong
Thứ tựkhôngcókhôngkhông
restartPolicyAlwaysAlwaysAlwaysOnFailure/Never
StrategyCách chạyDowntimeKhi nào
RollingUpdatethay dần, v1 và v2 cùng chạy một lúckhôngAPI stateless (mặc định)
Recreatekill hết v1 rồi mới start v2cóv1, v2 không được chạy cùng lúc (schema không tương thích, file lock)

7. Khi nào nên dùng / không nên dùng

Còn bao nhiêu bài nữa thì tới Helm?

Không còn nhiều. Với mục tiêu backend senior và phỏng vấn, không cần học K8s ở mức admin. Lộ trình từ đây:

KUBERNETES
├── 16.1 Cluster / control plane            ✅
├── 16.2 Pod / probes                       ✅
├── 16.3 Workloads / rolling update         ✅ ← ĐANG Ở ĐÂY
├── 16.4 Service / Ingress                  ⏭️  Internet → domain/path → Service
├── 16.5 ConfigMap / Secret / RBAC          ⏭️
├── 16.6 Requests / limits / HPA / PVC      ⏭️  CPU/RAM, OOMKilled, autoscale
├── 16.7 Troubleshooting                    ⏭️  kubectl describe/logs/events
└── 16.8 HELM                               ⏭️  Chart, values, install/upgrade/rollback

Còn 4 bài K8s rồi tới Helm. Mình sẽ không đào sâu etcd internals, CNI, CRD/operator, NetworkPolicy. Quan trọng hơn là bạn nhìn một kiến trúc và giải thích được: image ở đâu, ai giữ 3 replica, Service tìm Pod thế nào, traffic từ Internet vào ra sao, config lấy ở đâu, deploy v2 không downtime thế nào, CPU cao scale thế nào, data nằm đâu.

Rollback Deployment thì database có được rollback theo không?

Không. rollout undo chỉ đổi Pod template về image cũ. Database là chuyện riêng. Ví dụ payment 2.0 chạy Flyway migration:

ALTER TABLE payments DROP COLUMN legacy_status;
payment 2.0 → BUG → kubectl rollout undo → payment 1.0
                                              │
                                    SELECT legacy_status ...
                                              │
                                     ERROR: column does not exist 💥

Rolling update còn làm v1 và v2 chạy cùng lúc, nên schema phải tương thích cả hai. Dùng pattern expand / contract:

1. Expand:   thêm cột/bảng mới, không xóa gì   (v1 và v2 đều chạy được)
2. Deploy v2: ghi cả cũ lẫn mới hoặc chỉ đọc mới
3. Migrate:  backfill dữ liệu
4. Contract: khi chắc không còn v1, release sau mới xóa cột cũ

Chi tiết migration xem bài Flyway & Liquibase.

Vậy StatefulSet postgres để replicas 3 là thành cluster có replication luôn hả?

Không. Đây là hiểu nhầm rất hay gặp. StatefulSet chỉ cho primitive của Kubernetes:

replicas: 3 chỉ tạo ra ba postgres độc lập, ba database riêng không biết gì về nhau. Những câu sau vẫn phải tự trả lời: ai là primary, replica nhận WAL thế nào (xem bài 6.9), failover ra sao, client tìm primary bằng cách nào. Thực tế production dùng managed database hoặc operator như CloudNativePG, Zalando Postgres Operator. Các operator này dùng StatefulSet (hoặc Pod) bên dưới và thêm logic replication.

Phỏng vấn hỏi về Deployment và các workload thì trả lời sao?

Q1. Kubernetes thực hiện zero-downtime deployment thế nào?

🇻🇳 Tiếng Việt:

Deployment dùng rolling update: tạo ReplicaSet mới, tăng dần Pod version mới và giảm Pod version cũ theo maxSurge và maxUnavailable. Readiness probe đảm bảo Pod mới chỉ nhận traffic khi đã sẵn sàng, graceful shutdown đảm bảo Pod cũ xử lý xong request. Nếu lỗi thì kubectl rollout undo. Ngoài ra migration database phải tương thích ngược vì hai version chạy cùng lúc và rollback app không rollback database.

🇬🇧 English:

A Deployment performs a rolling update: it creates a new ReplicaSet, scales the new version up and the old one down according to maxSurge and maxUnavailable. Readiness probes make sure new Pods only get traffic once they're ready, and graceful shutdown lets old Pods finish their requests. If something goes wrong, I run kubectl rollout undo. Database migrations must also be backward compatible, because both versions run at the same time and rolling back the app doesn't roll back the database.

Q2. Deployment, ReplicaSet và Pod liên quan thế nào?

🇻🇳 Tiếng Việt:

Deployment quản lý ReplicaSet, ReplicaSet giữ đúng số Pod khớp selector. Mỗi lần Pod template đổi, ví dụ đổi image, Deployment tạo ReplicaSet mới và chuyển dần Pod sang đó; ReplicaSet cũ giữ lại với 0 replica để rollback. Mình gần như không bao giờ tạo ReplicaSet trực tiếp.

🇬🇧 English:

A Deployment manages ReplicaSets, and a ReplicaSet keeps the right number of Pods matching its selector. Whenever the Pod template changes, for example a new image, the Deployment creates a new ReplicaSet and gradually moves Pods to it; old ReplicaSets are kept at zero replicas for rollback. I almost never create a ReplicaSet directly.

Q3. Khi nào dùng StatefulSet thay vì Deployment?

🇻🇳 Tiếng Việt:

Khi Pod cần danh tính ổn định và ổ đĩa riêng, như database, Kafka, ZooKeeper. StatefulSet cho tên Pod cố định theo số thứ tự, mỗi Pod một PVC qua volumeClaimTemplates, DNS riêng qua headless Service, và start/stop theo thứ tự. Nhưng nó không tự lo replication hay failover; với database production em ưu tiên managed service hoặc operator.

🇬🇧 English:

When Pods need a stable identity and their own storage, like databases, Kafka or ZooKeeper. A StatefulSet gives each Pod a fixed ordinal name, its own PVC through volumeClaimTemplates, a stable DNS name through a headless Service, and ordered startup and shutdown. It does not handle replication or failover, so for production databases I prefer a managed service or an operator.

Q4. Job và CronJob dùng khi nào, cần chú ý gì?

🇻🇳 Tiếng Việt:

Job cho việc chạy tới khi xong như migration hay import; CronJob tạo Job theo lịch như đối soát mỗi đêm. Em đặt restartPolicy: OnFailure, backoffLimit để giới hạn retry, activeDeadlineSeconds để không chạy vô hạn, và concurrencyPolicy: Forbid để không chạy chồng. Logic phải idempotent vì Job có thể chạy lại, và CronJob có thể bỏ lỡ hoặc chạy trễ một lần.

🇬🇧 English:

A Job runs work to completion, such as a migration or an import; a CronJob creates Jobs on a schedule, such as nightly reconciliation. I set restartPolicy: OnFailure, a backoffLimit for retries, activeDeadlineSeconds so it can't run forever, and concurrencyPolicy: Forbid to prevent overlapping runs. The logic must be idempotent because a Job can be retried, and a CronJob can occasionally miss or delay a run.

Cách nhớ ngắn gọn


Tự kiểm tra

replicas: 4, maxSurge: 1, maxUnavailable: 0. Trong rollout tối đa có bao nhiêu Pod?

5 Pod, và luôn có ít nhất 4 Pod available.

Selector của Deployment không khớp label trong template thì sao?

API server từ chối: selector does not match template labels.

Pod v2 không bao giờ Ready, maxUnavailable: 0. Người dùng có bị ảnh hưởng không?

Không. Rollout kẹt, Pod v1 vẫn phục vụ. Cần rollout undo hoặc sửa lỗi.

Pod postgres-1 bị xóa. Pod mới tên gì, dùng PVC nào?

Vẫn postgres-1, gắn lại PVC data-postgres-1.

Muốn mỗi node có đúng một Pod thu log thì dùng gì?

DaemonSet.