Workloads: Deployment, rolling update, StatefulSet, DaemonSet, Job
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 đề
- Pod tự tạo chết là mất (bài 17.1). Ai giữ 3 Pod?
- Deploy kiểu ngây thơ: kill v1 v1 v1 → 0 Pod → start v2. Khoảng giữa là downtime.
- Postgres chạy bằng Deployment: Pod mới tên random, có thể gắn nhầm ổ đĩa, ba bản cùng ghi một disk.
- Batch chạy bằng Deployment: xong việc thì exit, Deployment lại restart, chạy mãi.
3. Giải pháp: chọn đúng controller
| Workload | Dùng cho | Đặc điểm |
|---|---|---|
| Deployment | order, payment (stateless) | Pod giống hệt nhau, tên random, rolling update, rollback |
| StatefulSet | postgres, Kafka, Redis | Tên ổn định postgres-0, PVC riêng từng Pod, start/stop theo thứ tự |
| DaemonSet | log agent, node exporter, CNI | Mỗi node 1 Pod, node mới vào tự có |
| Job | migration, import một lần | Chạy tới khi thành công, có retry |
| CronJob | đối soát mỗi đêm | Tạ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✅
- maxSurge: được tạo thêm tối đa bao nhiêu Pod vượt replicas trong lúc rollout.
- maxUnavailable: được thiếu tối đa bao nhiêu Pod available. Có thể dùng %, mặc định 25%/25%.
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
| Deployment | StatefulSet | DaemonSet | Job/CronJob | |
|---|---|---|---|---|
| Tên Pod | random | name-0, name-1 | random | random |
| Storage | chung hoặc không | PVC riêng từng Pod | thường hostPath | tùy |
| Số Pod | replicas | replicas | = số node | chạy tới khi xong |
| Thứ tự | không | có | không | không |
| restartPolicy | Always | Always | Always | OnFailure/Never |
| Strategy | Cách chạy | Downtime | Khi nào |
|---|---|---|---|
| RollingUpdate | thay dần, v1 và v2 cùng chạy một lúc | không | API stateless (mặc định) |
| Recreate | kill hết v1 rồi mới start v2 | có | 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
- Deployment cho mọi service stateless. Đây là lý do app nên stateless: session để Redis, file để object storage.
- StatefulSet khi cần danh tính và ổ đĩa ổn định. Nhưng database production thường dùng managed DB (RDS, Cloud SQL) hoặc operator (CloudNativePG), vì StatefulSet không lo replication và failover.
- Không dùng Deployment cho batch, không dùng CronJob cho việc cần chạy liên tục. Job phải idempotent vì có retry.
order-service gọi Pod nào? Người dùng ngoài Internet vào bằng đường nào? Bài sau: Service và Ingress.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:
- tên Pod ổn định (
postgres-0, -1, -2), - mỗi Pod một PVC riêng, tạo lại vẫn gắn đúng PVC,
- start/stop theo thứ tự.
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
maxSurgevà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
maxSurgeandmaxUnavailable. 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 runkubectl 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, abackoffLimitfor retries,activeDeadlineSecondsso it can't run forever, andconcurrencyPolicy: Forbidto 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
- Deployment → ReplicaSet → Pod; gắn với nhau bằng label + selector.
- maxSurge = thêm bao nhiêu; maxUnavailable = thiếu bao nhiêu. Readiness là cổng của rolling update.
- Rollback app ≠ rollback DB → expand/contract.
- StatefulSet = tên + ổ đĩa ổn định, không phải HA database.
- DaemonSet = mỗi node một Pod; Job = chạy tới khi xong; CronJob = Job theo lịch.
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.