Bài 03

09 tháng 7, 2026 · 30 phút đọc

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ản1 bản)
   • Cập nhật = xóa thủ công + tạo lại = downtime

Ta 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 khp selector (app=nginx) hin có.
   Bước 2: So vi replicas mong mun:
              thiếuto thêm Pod ttemplate
              thaxóa bt
   Bước 3: Canh mãi mãi. 1 Pod chết / node spto Pod mi thay thế.
   replicas: 3,  đang có 2  →  RS tạo thêm 13replicas: 3,  bạn xóa 1 tay → RS tạo lại ngay → 3 ✅   ← self-healing!
   replicas: 3,  có 5 (dư)   →  RS xóa 23

Đâ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.25nginx: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ậttầng này
       │ tạo & quản
       ▼
   ReplicaSet   ← Deployment ttạo, bạn hiếm khi đụng tay
       │ tạo & quản
       ▼
     Pod(s)     ← container thật chạy ở đây
       │
       ▼
   Container

Deployment 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ình

K8s 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 mi (v1.26)
   ─────────────        ──────────────
   ● ● ●                                ← bt đầu: 3 cũ
   ● ●                  ○               ← to 1 mi, mi READY ri mi...
   ● ●                  ●               ← ...xóa 1 cũ
   ●                    ● ●
                        ● ● ●           ← kết thúc: 3 mi, 0 downtime

Hai 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ời
  • maxUnavailable: 1 → luôn giữ ít nhất replicas - 1 Pod phục vụ.
  • maxSurge: 1 → cho phép tạm có replicas + 1 Pod để 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ới

Có 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 2

Tự độ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  →  5710
   Tải giảm              →  HPA bớt Pod    →  1042 (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 node
spec:
  template:
    spec:
      nodeSelector:
        disktype: ssd        # Pod CHỈ đáp xuống node có label disktype=ssd

Node 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:NoSchedule
spec:
  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ì 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ạo

2. 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 2

4. 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 web

Câ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:

  1. 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.

  2. 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.

  3. maxUnavailable quyế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.

  4. 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.

  5. 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

  1. 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.

  2. 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.

  3. Rolling update mà quên readiness probe → "zero-downtime" thành "full-downtime". Cái bẫy phổ biến nhất.

  4. maxUnavailable quá 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.

  5. HPA không scale vì thiếu Metrics Server hoặc Pod không có resources.requests. HPA cần mốc để tính %.

  6. 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.

  7. Rollback xong tưởng đã an toàn tuyệt đối. Data/schema đã đổi không tự lùi theo. Để ý migration.

  8. 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.

  9. Quên resources nê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.

  10. Đổi selector củ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.

  11. 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.

  12. Đặt taint lên node rồi quên, Pod kẹt Pending mãi. describe pod bá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

← Bài trước
Bài 02
Bài tiếp theo →
Bài 04
Zalo tư vấn