Bài 03
Bài 03 — ReplicaSet & Deployment: self-healing và scale thật sự
Cuối Bài 02 ta để lại một vết nứt: bare Pod chết là chết luôn. Bạn kubectl delete pod web — nó biến mất, không ai hồi sinh. Một bản copy duy nhất, không scale, không có cách cập nhật version êm. Đúng kiểu single-point-of-failure mà chặng trước ta sợ.
Bài này vá vết nứt đó bằng hai lớp:
- ReplicaSet — giữ cho luôn đủ N bản Pod sống; chết là tạo lại.
- Deployment — quản ReplicaSet, thêm sức mạnh thật: cập nhật version không downtime, rollback khi lỗi, co giãn theo tải.
Đây là lúc K8s bắt đầu đáng đồng tiền. Mọi siêu năng lực hứa ở Bài 01 (self-healing, auto-scale, rolling update) hiện hình ở đây.
Vấn đề: một bản Pod là không đủ
Bare Pod (Bài 02):
┌─────────┐
│ Pod │ ──💥 crash / bị xóa / node chết
└─────────┘
│
▼
Biến mất. App offline. Hết.
• Không tự hồi sinh
• Không scale (1 bản là 1 bản)
• Cập nhật = xóa thủ công + tạo lại = downtimeTa cần ai đó canh chừng: "phải luôn có đúng 3 bản Pod sống — thiếu thì tạo, thừa thì xóa." Đó là việc của ReplicaSet, và nó vận hành bằng đúng vòng lặp đối chiếu (reconciliation) đã gặp ở Bài 01.
ReplicaSet — người giữ số lượng
ReplicaSet (RS) đảm bảo luôn có một số lượng Pod cố định đang chạy. Ba phần cấu thành:
- replicas — số Pod mong muốn (vd: 3).
- selector — dùng label để nhận biết "Pod nào thuộc quyền tôi quản".
- template — khuôn Pod để tạo ra khi cần thêm.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-rs
spec:
replicas: 3 # muốn 3 bản
selector:
matchLabels:
app: nginx # quản các Pod có label app=nginx
template: # khuôn Pod sinh ra
metadata:
labels:
app: nginx # PHẢI khớp selector ở trên
spec:
containers:
- name: nginx
image: nginx:1.25
ports: [{ containerPort: 80 }]Vòng đời — reconciliation loop
Bước 1: RS đếm Pod khớp selector (app=nginx) hiện có.
Bước 2: So với replicas mong muốn:
thiếu → tạo thêm Pod từ template
thừa → xóa bớt
Bước 3: Canh mãi mãi. 1 Pod chết / node sập → tạo Pod mới thay thế. replicas: 3, đang có 2 → RS tạo thêm 1 → 3 ✅
replicas: 3, bạn xóa 1 tay → RS tạo lại ngay → 3 ✅ ← self-healing!
replicas: 3, có 5 (dư) → RS xóa 2 → 3 ✅Đây là self-healing thành hình: xóa một Pod đi, RS lập tức đẻ bản mới. Thử ở lab cuối bài sẽ thấy "ma thuật" này tận mắt.
Label & Selector — chất keo của K8s
Label = "nhãn dán" key=value gắn lên đối tượng (Pod, Node, Service...).
vd: app=nginx, tier=frontend, env=prod
Selector = bộ lọc tìm đối tượng theo label.
vd: matchLabels: { app: nginx }Toàn bộ K8s "buộc" các mảnh lại bằng label, không phải bằng tên hay IP. RS tìm Pod của mình bằng selector. Service (Bài 04) tìm Pod để rót traffic cũng bằng selector. Hiểu label = hiểu cách mọi thứ liên kết.
⚠️ selector phải khớp label trong template. Lệch một ký tự → RS không "nhận" Pod mình tạo ra → đẻ vô hạn hoặc báo lỗi. Đây là lỗi cấu hình kinh điển.
Vấn đề của ReplicaSet trần: không update êm
RS giữ đủ số bản — tốt. Nhưng đổi nginx:1.25 → nginx:1.26 thì sao? RS không có chiến lược cập nhật. Bạn phải tự xóa Pod cũ, sửa template, tạo lại — vừa thủ công vừa downtime. RS cũng không lưu lịch sử để rollback.
→ Vì thế gần như không ai tạo ReplicaSet trực tiếp. Ta dùng lớp trên nó: Deployment.
Deployment — người quản lý thật sự
Deployment quản ReplicaSet, và ReplicaSet quản Pod. Một phân cấp 3 tầng:
Deployment ← bạn khai báo & cập nhật ở tầng này
│ tạo & quản
▼
ReplicaSet ← Deployment tự tạo, bạn hiếm khi đụng tay
│ tạo & quản
▼
Pod(s) ← container thật chạy ở đây
│
▼
ContainerDeployment cho bạn:
- Tự triển khai đúng số Pod (qua RS).
- Cập nhật version không downtime (Rolling Update).
- Rollback về bản trước khi lỗi.
- Scale thủ công hoặc tự động.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports: [{ containerPort: 80 }]kubectl apply -f deployment.yaml
kubectl get deployments # tổng quan
kubectl get rs # thấy RS do Deployment tự tạo
kubectl get pods -o wide # 3 Pod, rải trên các node💡 Không cần tạo ReplicaSet bằng tay! Tạo Deployment là K8s tự sinh RS cho bạn. Bạn chỉ làm việc ở tầng Deployment.
Rolling Update — cập nhật không downtime
Đây là tính năng "ăn tiền" nhất. Đổi image:
kubectl set image deployment/nginx-deploy nginx=nginx:1.26
# hoặc sửa image trong YAML rồi kubectl apply -f lại
kubectl rollout status deployment/nginx-deploy # theo dõi tiến trìnhK8s không giết hết Pod cũ rồi mới tạo mới (như vậy là downtime). Nó tạo RS mới và thay từng Pod một, đan xen cũ–mới, đảm bảo luôn đủ Pod phục vụ:
RS cũ (v1.25) RS mới (v1.26)
───────────── ──────────────
● ● ● ← bắt đầu: 3 cũ
● ● ○ ← tạo 1 mới, mới READY rồi mới...
● ● ● ← ...xóa 1 cũ
● ● ●
● ● ● ← kết thúc: 3 mới, 0 downtimeHai núm điều khiển nhịp thay:
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # tối đa BAO NHIÊU Pod được phép thiếu trong lúc thay
maxSurge: 1 # tối đa BAO NHIÊU Pod dư được tạo thêm tạm thờimaxUnavailable: 1→ luôn giữ ít nhấtreplicas - 1Pod phục vụ.maxSurge: 1→ cho phép tạm córeplicas + 1Pod để thay nhanh hơn.- Cả hai nhận số tuyệt đối (
1) hoặc phần trăm (25%).
💡 Readiness probe (Bài 02) là linh hồn của zero-downtime. Rolling update chỉ xóa Pod cũ sau khi Pod mới readiness PASS. Không có readiness → K8s tưởng Pod mới sẵn sàng ngay khi container start, rót traffic vào app chưa boot xong → khách thấy 502 giữa lúc deploy. Rolling update + readiness probe đi với nhau như hình với bóng.
Recreate — khi không thể chạy song song 2 version
spec:
strategy:
type: Recreate # xóa HẾT Pod cũ → rồi mới tạo Pod mớiCó downtime, nhưng cần khi: app không cho chạy 2 version cùng lúc (vd thay đổi schema DB lớn), hoặc dùng volume ReadWriteOnce mà 2 Pod không thể cùng mount.
Rollback — quay đầu khi lỡ deploy bản lỗi
Deployment lưu lịch sử các revision. Deploy nhầm bản hỏng? Quay lại trong 1 lệnh:
kubectl rollout history deployment/nginx-deploy # xem các revision
kubectl rollout undo deployment/nginx-deploy # về bản ngay trước
kubectl rollout undo deployment/nginx-deploy --to-revision=2 # về revision cụ thểspec:
revisionHistoryLimit: 10 # giữ tối đa 10 revision cũ để rollback (mặc định 10)⚠️ Rollback chỉ trả về cấu hình Pod (image, env...). Nó không hoàn tác thay đổi đã ghi vào DB. Đổi schema xong rollback code = lệch schema. Cẩn thận với migration.
Scaling — co giãn
Thủ công
kubectl scale deployment/nginx-deploy --replicas=5 # tăng lên 5
kubectl scale deployment/nginx-deploy --replicas=2 # giảm còn 2Tự động — HPA (phần tài liệu gốc nhắc tên nhưng chưa làm)
HPA (Horizontal Pod Autoscaler) tự thêm/bớt số Pod theo tải thực tế (CPU/RAM/metric tùy chỉnh):
# Tự scale 2→10 bản, giữ CPU trung bình quanh 70%
kubectl autoscale deployment/nginx-deploy --min=2 --max=10 --cpu-percent=70
kubectl get hpa Tải tăng (CPU > 70%) → HPA thêm Pod → 5 → 7 → 10
Tải giảm → HPA bớt Pod → 10 → 4 → 2 (không xuống dưới min)⚠️ HPA cần biết Pod đang dùng bao nhiêu tài nguyên → phải cài Metrics Server trong cluster, và Pod phải đặt resources.requests (Bài 02) làm mốc tính %. Thiếu một trong hai, HPA "mù" và không scale.
Phân biệt nhanh: HPA = thêm Pod (horizontal). Cluster Autoscaler = thêm node khi cluster hết chỗ đặt Pod. Hai cái phối hợp: HPA đẻ Pod, hết node thì Cluster Autoscaler mua thêm máy.
Họ hàng của Deployment — các workload khác
Deployment hợp cho app không trạng thái (stateless) — web, API. Nhưng K8s còn vài "controller" anh em, biết để khỏi dùng nhầm:
Deployment → app stateless, nhiều bản giống hệt (web, API) ← mặc định
StatefulSet → app CÓ trạng thái, mỗi Pod có danh tính & ổ riêng
(database, Kafka, Redis cluster)
DaemonSet → đúng 1 Pod trên MỖI node (log agent, monitoring,
CNI) — thêm node là tự có thêm 1 bản
Job → chạy MỘT lần tới khi xong (batch, migration)
CronJob → Job chạy theo lịch (như crontab: backup mỗi đêm)→ Bài này tập trung Deployment vì 80% workload là stateless. Khi gặp database trên K8s, nhớ là phải dùng StatefulSet (mỗi replica cần ổ đĩa và định danh ổn định), không phải Deployment.
Điều khiển Pod đáp xuống node nào
Mặc định Scheduler (Bài 01) tự chọn node. Nhưng đôi khi bạn cần can thiệp: "Pod này phải chạy trên node có SSD", "đừng đặt Pod app lên node master", "rải 3 bản ra 3 node khác nhau cho an toàn". Đây chính là phần taints, affinities mà Bài 01 nhắc tới — giờ ta mở ra.
nodeSelector — luật đơn giản nhất
Gán label cho node, rồi bắt Pod chỉ chạy trên node có label đó:
kubectl label node worker-01 disktype=ssd # dán nhãn lên nodespec:
template:
spec:
nodeSelector:
disktype: ssd # Pod CHỈ đáp xuống node có label disktype=ssdNode Affinity — luật mềm dẻo hơn
nodeSelector chỉ biết "bắt buộc khớp". Affinity cho phép "bắt buộc" hoặc "ưu tiên (mềm)":
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # BẮT BUỘC
nodeSelectorTerms:
- matchExpressions:
- { key: disktype, operator: In, values: [ssd] }
preferredDuringSchedulingIgnoredDuringExecution: # ƯU TIÊN (mềm)
- weight: 50
preference:
matchExpressions:
- { key: zone, operator: In, values: [zone-a] }Họ hàng còn có podAffinity (đặt Pod gần Pod khác — vd web gần cache) và podAntiAffinity (đặt Pod xa nhau — vd 3 replica rải 3 node, một node chết không mất cả 3). Anti-affinity là mẹo HA rất hay dùng.
Taints & Tolerations — node "đuổi khách"
Cơ chế ngược với affinity. Affinity là Pod chọn node; taint là node đuổi Pod, trừ khi Pod có toleration (giấy phép) tương ứng.
Taint trên node = "node này có rào, Pod thường ĐỪNG vào"
Toleration trên Pod = "tôi có giấy phép, được vào node có rào đó"# Đánh taint: node này chỉ dành cho Pod GPU
kubectl taint nodes worker-02 gpu=true:NoSchedulespec:
template:
spec:
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule" # Pod này có giấy phép → được đáp xuống worker-02→ Đây chính là cơ chế khiến node Control Plane không nhận Pod thường (pitfall Bài 01): kubeadm tự taint master node-role.kubernetes.io/control-plane:NoSchedule. Muốn ép Pod lên master (cluster 1 node học tập) thì gỡ taint hoặc thêm toleration.
effect có 3 mức: NoSchedule (không xếp mới), PreferNoSchedule (tránh nếu được), NoExecute (đuổi cả Pod đang chạy).
PodDisruptionBudget — đừng tắt quá nhiều cùng lúc
Khi bạn bảo trì node (drain) hoặc cluster autoscale, K8s có thể tắt nhiều Pod cùng lúc. PDB đặt sàn an toàn:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: web-pdb }
spec:
minAvailable: 2 # luôn giữ ít nhất 2 Pod web sống khi bảo trì
selector:
matchLabels: { app: web }💡 Phối hợp đẹp: podAntiAffinity (rải Pod ra nhiều node) + PDB (không tắt quá nhiều cùng lúc) = app sống sót khi bảo trì và khi node chết. Đây là tư duy HA thật, không chỉ "tăng replicas".
🚀 Lab — self-healing, scale, update, rollback
Dùng cluster kind từ bài trước.
1. Deploy 3 bản
kubectl create deployment web --image=nginx:1.25 --replicas=3
kubectl get pods -o wide # 3 Pod nginx
kubectl get rs # 1 ReplicaSet do Deployment tạo2. Chứng kiến self-healing
# Mở 1 terminal theo dõi:
kubectl get pods --watch
# Terminal khác — xóa tay 1 Pod:
kubectl delete pod <tên-1-pod>
# Quay lại terminal watch: 1 Pod Terminating, NGAY LẬP TỨC 1 Pod mới
# được tạo. Vẫn đủ 3. RS không cho bạn "giết" được app. ✨3. Scale
kubectl scale deployment/web --replicas=6
kubectl get pods # 6 bản
kubectl scale deployment/web --replicas=2
kubectl get pods # còn 24. Rolling update + rollback
# Update image, xem thay từng Pod một
kubectl set image deployment/web nginx=nginx:1.26
kubectl rollout status deployment/web # "successfully rolled out"
kubectl get rs # thấy RS cũ (0 Pod) + RS mới (2 Pod)
# Deploy nhầm bản hỏng:
kubectl set image deployment/web nginx=nginx:phien-ban-ma
kubectl get pods # Pod mới kẹt ImagePullBackOff — nhưng Pod cũ VẪN chạy!
# (rolling update không xóa Pod cũ khi Pod mới chưa OK)
# Quay đầu:
kubectl rollout undo deployment/web
kubectl rollout status deployment/web # trở lại bản 1.26 lành lặn→ Để ý điểm vàng ở bước deploy hỏng: app không hề sập dù bạn đẩy image ma. Rolling update từ chối xóa bản cũ khi bản mới chưa khỏe. Đây là tấm lưới an toàn Compose không có.
5. Dọn
kubectl delete deployment webCâu chuyện thực tế
Lần đầu tôi demo K8s cho cả team startup, tôi không giảng slide. Tôi mở kubectl get pods --watch lên màn hình lớn, rồi nói: "App đang chạy 3 bản. Tôi sẽ giết một bản ngay bây giờ." — kubectl delete pod .... Trước mặt 6 người, một Pod chết, trong 2 giây một Pod mới mọc lên thế chỗ. Cả phòng "ồ".
Rồi tôi poweroff hẳn một worker node. Các Pod trên đó chuyển Unknown, và K8s lẳng lặng tái tạo chúng trên node còn sống. Không ai phải thức dậy. Đó là lúc cả team "mua" K8s — không phải vì tôi thuyết phục, mà vì họ thấy tận mắt thứ mà cái đêm 502 lúc 21:47 (chuyện Bài 01) đã thiếu.
Nhưng có một lần rolling update suýt thành thảm họa, và nó dạy tôi bài đắt nhất. Chúng tôi deploy version mới, app boot mất ~25 giây để nạp cache. Không ai gắn readiness probe. K8s tưởng Pod mới sẵn sàng ngay khi container start → rót traffic vào → app chưa nạp xong cache → trả lỗi cho khách trong suốt quá trình rolling update. "Zero-downtime deploy" hóa ra full-downtime deploy vì thiếu đúng một probe.
Thêm readinessProbe trỏ vào endpoint /ready (chỉ trả 200 sau khi cache nạp xong) → lần deploy kế tiếp mượt như nhung. Khách không hề biết có deploy.
Bài học:
Self-healing là thật, và nó đáng tiền. Nhưng chỉ hoạt động khi bạn để Deployment quản — bare Pod thì vô dụng.
Rolling update KHÔNG zero-downtime nếu thiếu readiness probe. Đây là cái bẫy số một khi chuyển từ Compose lên. Probe ở Bài 02 không phải trang trí — nó là điều kiện sống còn của deploy êm.
maxUnavailablequyết định bạn còn bao nhiêu Pod phục vụ giữa lúc deploy. App nhạy cảm → để 0, chấp nhận deploy chậm hơn nhưng không bao giờ tụt công suất.Rollback cứu code, không cứu data. Hoàn tác image trong 1 lệnh; nhưng schema DB đã đổi thì rollback gây lệch. Tách migration ra, làm cẩn thận.
Demo bằng cách phá, không bằng slide. Muốn ai đó tin self-healing, hãy giết Pod trước mặt họ. Niềm tin đến từ mắt thấy.
Pitfalls
Tạo ReplicaSet trực tiếp. 99% trường hợp dùng Deployment — nó tự quản RS và cho bạn update/rollback. Tạo RS trần là tự bỏ các tính năng hay.
selector ≠ label trong template. Lệch là RS/Deployment không nhận Pod của mình → tạo loạn hoặc lỗi. Khớp chính xác.
Rolling update mà quên readiness probe → "zero-downtime" thành "full-downtime". Cái bẫy phổ biến nhất.
maxUnavailablequá cao cho app nhạy cảm → giữa lúc deploy tụt quá nhiều công suất, khách nghẽn. Cân nhắc theo độ chịu tải.HPA không scale vì thiếu Metrics Server hoặc Pod không có
resources.requests. HPA cần mốc để tính %.Dùng Deployment cho database. DB cần danh tính + ổ riêng ổn định → phải StatefulSet. Deployment làm DB dễ mất/lẫn dữ liệu.
Rollback xong tưởng đã an toàn tuyệt đối. Data/schema đã đổi không tự lùi theo. Để ý migration.
Scale stateful app như stateless. Tăng replicas một Deployment chạy DB = nhiều Pod ghi đè nhau lên cùng volume = hỏng dữ liệu.
Quên
resourcesnên 1 Pod ăn hết node. Khi scale lên nhiều bản, thiếu limits là một Pod lỗi kéo sập cả node, RS đẻ lại Pod lên node khác rồi lại sập — domino.Đổi
selectorcủa Deployment đang chạy. Selector gần như bất biến; đổi nó khiến Deployment "bỏ rơi" các Pod cũ (thành mồ côi) và tạo bộ mới. Tránh.Tăng replicas nhưng không podAntiAffinity → K8s có thể dồn cả 3 bản lên 1 node → node đó chết là mất sạch, "nhiều bản" thành vô nghĩa. Muốn HA thật phải rải Pod ra nhiều node.
Đặt taint lên node rồi quên, Pod kẹt
Pendingmãi.describe podbáo "node(s) had untolerated taint". Hoặc thêm toleration, hoặc gỡ taint.
Tóm tắt
- Phân cấp: Deployment → ReplicaSet → Pod → Container. Bạn làm việc ở tầng Deployment.
- ReplicaSet giữ đủ N bản Pod (selector + replicas + template), tự hồi sinh Pod chết = self-healing.
- Label/Selector là chất keo buộc mọi thứ — K8s liên kết bằng label, không bằng IP/tên.
- Deployment thêm: Rolling Update (thay từng Pod, zero-downtime), Rollback (
rollout undo), Scale. - Rolling Update cần readiness probe để thật sự không downtime — chỉ rót traffic khi Pod mới sẵn sàng.
- maxSurge/maxUnavailable điều khiển nhịp thay Pod. Recreate khi không thể chạy 2 version song song.
- HPA tự co giãn số Pod theo CPU/RAM (cần Metrics Server +
requests). Cluster Autoscaler thêm node. - Workload khác: StatefulSet (DB, có trạng thái), DaemonSet (1 Pod/node), Job/CronJob (chạy 1 lần/theo lịch).
- Điều khiển nơi Pod đáp:
nodeSelector(luật cứng đơn giản), Affinity (cứng/mềm, pod-affinity/anti-affinity), Taints & Tolerations (node đuổi Pod trừ khi có giấy phép — cơ chế bảo vệ master). PDB giữ sàn Pod sống khi bảo trì. - Rollback cứu code, không cứu data.
Bài sau (Bài 04 — Service & Namespace) trả lời câu hỏi treo từ Bài 02–03: Pod sinh ra chết đi liên tục, IP đổi xoành xoạch, 3 bản replica có 3 IP khác nhau — vậy thứ khác tìm và gọi chúng kiểu gì? Câu trả lời là Service: một địa chỉ cố định + DNS + cân bằng tải, đứng trước đám Pod hay thay đổi. Và Namespace để dựng vách ngăn giữa các nhóm tài nguyên.
Nếu hôm nay bạn giết một Pod và thấy nó hồi sinh trong 2 giây — bạn vừa chạm vào lý do người ta chịu khổ học K8s.
— Minh Hưng