Khi hai hiện tượng có vẻ chẳng liên quan cùng làm bạn bối rối — một cái nhanh bất ngờ, một cái chậm khó hiểu — thường có một tầng ẩn ở giữa mà bạn chưa nhìn tới, và nó giải thích cả hai. Với Deployment, tầng ẩn ấy tên là ReplicaSet: hiểu nó rồi thì việc "quay lui tức thì" và việc "chờ mãi khi node chết" hoá ra là hai mặt của cùng một cơ chế. Người ta viết Deployment và nghĩ nó quản pod. Nó không. Bài này đo tầng ở giữa.

Ba tầng

deployment.apps/app                  3/4
   replicaset.apps/app-856595fd8d    4  4  3
      pod/app-856595fd8d-6c5f4       Running
      pod/app-856595fd8d-k2lmw       Running
      ...

Deployment không quản pod. Nó quản ReplicaSet, và ReplicaSet quản pod. Chú ý tên pod: app-856595fd8d-6c5f4. Phần giữa là hàm băm của template pod — mọi pod sinh ra từ cùng một cấu hình đều mang cùng phần băm đó.

Mỗi phiên bản một ReplicaSet

Đổi ảnh rồi xem lại:

rs app-7fbbdcbddc   mong muốn=4  hiện có=4  sẵn sàng=4
rs app-856595fd8d   mong muốn=0  hiện có=0  sẵn sàng=0

ReplicaSet cũ không bị xoá. Nó chỉ bị đặt mong muốn=0. Đây là toàn bộ cơ chế của Deployment: nó tạo một ReplicaSet mới cho template mới, rồi dịch chuyển dần số bản mong muốn từ cái cũ sang cái mới. Phần sau đo tốc độ dịch chuyển đó.

Quay lui mất 2,3 giây

kubectl rollout undo deployment/app
2,3 giây

rs app-7fbbdcbddc   mong muốn=0
rs app-856595fd8d   mong muốn=4      <- ReplicaSet cũ sống lại
ảnh hiện tại: nginx:1.26-alpine

Nhanh vì nó không làm gì mới: chỉ đổi số "mong muốn" giữa hai ReplicaSet đã có sẵn. Ảnh cũ vẫn nằm trên node (phần 48 đo kéo ảnh mất 6–16 giây), template cũ vẫn nằm trong etcd.

revisionHistoryLimit mặc định 10 — giữ mười ReplicaSet cũ, tức mười phiên bản lùi được. Đặt về 0 thì tiết kiệm một chút etcd nhưng mất hẳn khả năng quay lui, và đó là đánh đổi rất tệ.

kubectl rollout history deployment/app
kubectl rollout undo deployment/app --to-revision=3

CHANGE-CAUSE hiện <none> vì tôi không khai. Thêm annotation kubernetes.io/change-cause khi triển khai thì lịch sử đọc được:

kubectl annotate deployment/app kubernetes.io/change-cause="nang nginx len 1.27"

Node chết: bốn phút mới phản ứng

Bốn pod, hai trong số đó nằm trên node bị docker stop:

Thời điểm Running Trên node chết Tổng pod
0 s 4 2 4
101 s 4 2 4
202 s 4 2 4
222 s 4 2 4
243 s 4 2 6
505 s 4 2 6

Bốn phút không làm gì, rồi tạo hai bản thay thế. Và pod ma trên node chết vẫn nằm đó tới hết phép đo — hơn tám phút. Lý do đã đo ở phần 42: pod ma vẫn báo Ready. kubelet trên node chết không cập nhật được trạng thái, nên chúng đứng nguyên. ReplicaSet đếm đủ 4 và kết luận mọi thứ đúng. Chỉ khi cơ chế đuổi theo vết gỡ pod ma ra thì ReplicaSet mới thấy thiếu và tạo bản thay thế.

Đây là phép đo độc lập thứ hai cho cùng hiện tượng, lần này với Deployment thay vì pod trần, và nó cho thêm một mốc: bản thay thế xuất hiện ở khoảng 240 giây.

Hệ quả: kubectl get deployment nói dối

Trong suốt bốn phút đó:

deployment.apps/app   4/4   READY

Bốn trên bốn. Xanh hoàn toàn. Trong khi một nửa số bản đã không phục vụ được. Thứ nói thật là Endpoints — nó chỉ chứa pod qua được kiểm tra sẵn sàng:

kubectl get endpoints app-svc -o jsonpath='{.subsets[*].addresses[*].ip}' | wc -w

Nếu bảng theo dõi của bạn đọc deployment.status.readyReplicas, nó sẽ bỏ lỡ đúng loại sự cố này.

Ba trường status và ý nghĩa khác nhau

kubectl get deployment app -o jsonpath='{.status}' | python3 -m json.tool
  • replicas — số pod ReplicaSet đang quản.
  • readyReplicas — số pod báo Ready. Đây là con số nói dối ở trên.
  • availableReplicas — số pod đã Ready liên tục trong minReadySeconds.

minReadySeconds mặc định 0, nên hai con số cuối thường bằng nhau. Đặt nó lên 10–30 giây là cách rẻ để tránh triển khai một bản hỏng nhanh: pod khởi động rồi sập trong 5 giây sẽ không bao giờ được tính là available, và việc triển khai cuốn chiếu dừng lại.

Khi nào không dùng Deployment

Deployment giả định các bản thay thế được cho nhau: không danh tính riêng, không lưu trữ riêng, thứ tự khởi động không quan trọng. Không đúng với ba trường hợp:

  • Cơ sở dữ liệu, hàng đợi, bất cứ thứ gì có trạng thái → StatefulSet. Nó cho tên ổn định (db-0, db-1), thứ tự khởi động, và volume riêng cho từng bản.
  • Một bản trên mỗi node — thu log, đo đạc, plugin mạng → DaemonSet.
  • Chạy một lần rồi thôi → Job hoặc CronJob.

Dùng Deployment cho cơ sở dữ liệu là sai lầm hay gặp, và nó chỉ lộ ra khi hai bản cùng ghi vào một volume.

Mẫu số chung: một tầng ẩn giải thích nhiều hành vi, và trạng thái tự-báo có thể nói dối khi người báo đang hỏng

Bài này gói hai bài học. Thứ nhất: một tầng gián tiếp mà bạn chưa để ý thường giải thích nhiều hành vi tưởng chừng chẳng liên quan cùng lúc. ReplicaSet là lý do quay lui tức thì và cũng là lý do node chết phản ứng chậm — cùng một tầng, hai mặt. Và quay lui rẻ vì một lẽ sâu hơn: đảo ngược rẻ khi bạn giữ lại trạng thái cũ và chỉ dịch một con trỏ. ReplicaSet cũ không bị xoá, undo chỉ đổi số "mong muốn" giữa hai cái đã có — đúng như git quay lui là dịch HEAD (các commit vẫn nằm đó), như một CSDL giữ ảnh chụp MVCC, như blue-green lật con trỏ sang bản đã dựng sẵn. Hệ quả thực dụng: giữ bản cũ mua cho bạn khả năng đảo ngược gần như miễn phí, còn vứt lịch sử đi (revisionHistoryLimit: 0) đổi một chút tiết kiệm lấy việc biến một cú dịch-con-trỏ thành một lần xây lại. Khi thiết kế khả năng hoàn tác, hãy hỏi: cái cũ có còn được giữ để chỉ cần trỏ lại không?

Thứ hai: một trạng thái tự báo cáo có thể cũ hoặc sai đúng vào lúc chính người báo đang hỏng. readyReplicas báo 4/4 vì pod ma vẫn "nói" mình Ready — mà kubelet trên node chết không còn cách nào cải chính; trong khi Endpoints, được kiểm chứng từ bên ngoài qua readiness, nói thật. Cùng nguyên tắc với một tiến trình tự báo sức khoẻ so với một prober độc lập đo từ ngoài, với trạng thái tự-khai của một node so với một nhịp tim do bên khác kiểm. Ưu tiên tín hiệu được kiểm chứng từ ngoài hơn lời tự báo — nhất là khi kẻ báo cáo có thể chính là kẻ đã hỏng.

Mẫu số chung: gặp nhiều hành vi khó hiểu thì tìm tầng ẩn chung giải thích tất cả; thiết kế đảo ngược bằng cách giữ bản cũ và dịch con trỏ; và tin tín hiệu kiểm-chứng-từ-ngoài hơn lời tự-báo.

Muốn tự soi, xem lịch sử phiên bản và khả năng lùi của bạn:

kubectl get rs -l app=<ten> \
  -o custom-columns=TEN:.metadata.name,MONG_MUON:.spec.replicas,ANH:.spec.template.spec.containers[0].image

Số dòng có MONG_MUON=0 là số phiên bản bạn lùi về được ngay lập tức, không cần kéo ảnh. Nếu chỉ có một dòng, revisionHistoryLimit đã bị đặt về 0 — và lần triển khai hỏng tiếp theo sẽ phải sửa bằng cách xây lại ảnh.

Phần sau đo rolling update: cùng một lần đổi ảnh, có readinessProbe thì 0 lỗi, không có thì 8% yêu cầu thất bại.