Nâng Cấp và Version Skew

K
Kai··5 min read

Cụm đang ở v1.36, và có ngày phải lên v1.37. Nâng cấp Kubernetes không phải thay tất cả binary cùng lúc — làm vậy gần như chắc chắn vỡ. Có một quy tắc về việc thành phần nào được lệch phiên bản bao nhiêu so với apiserver, và một thứ tự nâng bắt buộc theo từ đó. Bài này soi quy tắc trên cụm thật, rồi diễn tập phần thao tác khó nhất của một lần nâng node.

Version skew: ai được lệch bao nhiêu

kubectl version cho thấy mọi thành phần đang đồng bộ:

kubectl version | head -3
kubectl get nodes -o wide | awk '{print $1, $5}'
Client Version: v1.36.1
Server Version: v1.36.1
worker-0 v1.36.1
worker-1 v1.36.1

Đồng bộ là trạng thái lý tưởng, nhưng giữa các bước nâng cấp thì không thể tránh lệch. Quy tắc version skew giới hạn lệch đó (so với apiserver mới nhất):

   kube-apiserver (HA)              các apiserver lệch nhau tối đa 1 minor
        │
        ├── controller-manager/scheduler   tối đa CŨ hơn 1 minor, KHÔNG mới hơn
        ├── kubelet                         tối đa CŨ hơn 3 minor (N-3), KHÔNG mới hơn
        ├── kube-proxy                      tối đa CŨ hơn 3 minor, KHÔNG mới hơn
        └── kubectl                         trong ±1 minor

Mấu chốt lặp ở mọi dòng: không thành phần nào được mới hơn apiserver. kubelet được tụt sau apiserver tới ba minor (apiserver 1.36 thì kubelet 1.36/1.35/1.34/1.33 đều chạy), nhưng kubelet 1.37 nói chuyện với apiserver 1.36 thì không được hỗ trợ. controller-manager và scheduler chặt hơn — chỉ được tụt một minor.

Thứ tự nâng cấp suy ra từ quy tắc

Vì không gì được mới hơn apiserver, thứ tự nâng là hệ quả trực tiếp:

1. kube-apiserver (cả 3 control plane, lần lượt)  ← nâng TRƯỚC
2. controller-manager + scheduler                  ← rồi đến (không thứ tự giữa hai)
3. kubelet + kube-proxy trên từng worker            ← cuối, mỗi lần 1 minor

Nếu nâng kubelet trước, kubelet 1.37 sẽ mới hơn apiserver 1.36 — vi phạm skew. Nâng apiserver trước thì trong lúc kubelet còn 1.36, nó chỉ tụt sau apiserver 1.37 một minor, vẫn hợp lệ. Cùng lý do, chỉ nâng một minor mỗi lần (1.35→1.36→1.37), không nhảy cóc 1.35→1.37, vì kubelet 1.35 với apiserver 1.37 là lệch hai minor — vẫn trong N-3 cho kubelet, nhưng controller-manager 1.35 với apiserver 1.37 là lệch hai, vượt N-1.

Với cụm tự dựng, "nâng một thành phần" nghĩa là tải binary phiên bản mới, thay vào, khởi động lại service — chính là việc đã làm ở Part I, lặp lại với version mới. (v1.36.1 hiện là patch mới nhất của 1.36, nên bài này không bump số; phần đáng học không phải lệnh tải binary, mà là xử lý node lúc nâng kubelet.)

Diễn tập nâng một node: drain

Khi nâng kubelet trên một worker, không thể vừa nâng vừa để pod chạy trên đó — phải dời pod đi trước, nâng, rồi cho pod quay lại. Đây là phần dễ sai và cũng là phần ta diễn tập thật trên worker-0 (hoàn tác được). kubectl drain làm hai việc: cordon (đánh dấu node không nhận pod mới) và evict pod đang chạy:

kubectl drain worker-0 --ignore-daemonsets --delete-emptydir-data --force
evicting pod kube-system/coredns-8569db9899-wxzlr
evicting pod kube-system/ebs-csi-controller-74ddd54f5b-zdgg2
evicting pod kube-system/cilium-operator-778946fc48-nhmnp
...
node/worker-0 drained

--ignore-daemonsets là bắt buộc vì DaemonSet (cilium, ebs-csi-node — Bài 26) có mặt trên mọi node và sẽ được tạo lại ngay nếu evict, nên drain bỏ qua chúng. Pod thường (coredns, cilium-operator, ebs-csi-controller, hubble-ui, snapshot-controller) bị evict và scheduler dời sang worker-1. Node giờ ở trạng thái an toàn để nâng:

kubectl get nodes
NAME       STATUS                     ROLES    AGE   VERSION
worker-0   Ready,SchedulingDisabled   <none>   9h    v1.36.1
worker-1   Ready                      <none>   9h    v1.36.1

Ready,SchedulingDisabled — node vẫn chạy (DaemonSet còn đó) nhưng không nhận pod mới. Lúc này mới thay binary kubelet và restart service một cách an toàn: không pod nào của ứng dụng bị giật vì kubelet khởi động lại, vì chúng đã dời đi. Drain tôn trọng PodDisruptionBudget (Bài 23), nên nếu một ứng dụng khai PDB, drain sẽ evict từ từ để giữ đủ bản chạy.

Cho node quay lại: uncordon

Nâng xong, uncordon mở lại cho node nhận pod:

kubectl uncordon worker-0
kubectl get nodes
node/worker-0 uncordoned
NAME       STATUS   ROLES    AGE   VERSION
worker-0   Ready    <none>   9h    v1.36.1
worker-1   Ready    <none>   9h    v1.36.1

worker-0 về Ready và lại nhận pod. Lưu ý: uncordon không tự kéo pod đã dời quay lại — scheduler chỉ đặt pod mới lên worker-0; các pod đang chạy trên worker-1 ở yên đó cho tới khi chúng bị tạo lại. Một lần nâng cấp cụm là lặp drain → nâng → uncordon cho từng worker, mỗi lần một node, để cụm luôn còn node phục vụ.

🧹 Dọn dẹp

Bài này không cài hay đổi version gì — chỉ cordon/drain/uncordon worker-0, và đã uncordon trả node về Ready. Không có gì để dọn thêm. Lệnh dùng trong bài ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 63-upgrade.

Tổng kết

Nâng cấp Kubernetes bị ràng bởi version skew: không thành phần nào được mới hơn apiserver; kubelet/kube-proxy được tụt sau tới ba minor (N-3), controller-manager/scheduler chỉ một minor (N-1), kubectl trong ±1, các apiserver HA lệch nhau tối đa một. Thứ tự nâng suy ra từ đó — apiserver trước, rồi controller-manager/scheduler, cuối cùng kubelet/kube-proxy — và mỗi lần chỉ một minor. Với cụm tự dựng, nâng một thành phần là thay binary + restart như Part I. Phần thao tác khó nhất là nâng node, ta diễn tập thật trên worker-0: drain --ignore-daemonsets cordon node và evict pod thường sang worker-1 (giữ DaemonSet), đưa node về Ready,SchedulingDisabled để thay kubelet an toàn, rồi uncordon cho nhận pod lại. Drain tôn trọng PDB (Bài 23); uncordon không tự kéo pod cũ về.

Bài 64 gom mấy việc vận hành ở tầng node mà kubelet lo âm thầm: dọn image và container cũ (garbage collection), cách pod ánh xạ vào cgroup v2, hỗ trợ swap, và tắt máy có trật tự (graceful node shutdown) để pod không bị giật khi node tắt.

Related Posts