Bộ lập lịch chỉ ghi tên node vào pod rồi hết trách nhiệm. kubelet là thứ thật sự chạy container. Bài này đo đoạn đường đó.

Bốn mốc khởi động, chi phí kéo ảnh, container pause, và pod tĩnh

Bốn mốc, gần như cùng một giây

15:40:11   Scheduled     bộ lập lịch ghi spec.nodeName
15:40:12   Pulled        kubelet thấy pod của mình, ảnh đã có sẵn
15:40:12   Created       kubelet bảo containerd tạo container
15:40:12   Started       container chạy

Ba mốc cuối trong cùng một giây.

Khi ảnh đã có trên node, kubelet gần như không tốn thời gian. Nó nhận pod qua watch (phần 46), gọi runtime qua giao diện CRI, và container chạy.

Nghĩa là: toàn bộ độ trễ khởi động pod nằm ở việc kéo ảnh, không nằm ở Kubernetes.

Kéo ảnh là phần đắt

alpine:3.20      4,1 MB    ->   5,9 giây
python:3.12     400 MB    ->  15,7 giây

Gấp 100 lần kích thước nhưng chỉ gấp 2,7 lần thời gian — phần cố định (kết nối tới registry, xác thực, tải và giải nén siêu dữ liệu) chiếm khá nhiều trong ảnh nhỏ.

Nhưng mười giây chênh lệch nhân với số pod và số lần triển khai thì rất lớn. Với deployment 20 bản triển khai mười lần một ngày, đó là hơn nửa giờ mỗi ngày chỉ để kéo ảnh.

Bốn cách rút ngắn:

Ảnh nhỏ hơn. Xây nhiều tầng (multi-stage build) và dùng ảnh nền tối giản. Đây là cách hiệu quả nhất và cũng cải thiện bảo mật.

imagePullPolicy: IfNotPresent. Mặc định là IfNotPresent trừ khi tag là latest — và đó là một lý do nữa để không bao giờ dùng tag latest: nó ép kéo ảnh mỗi lần khởi động pod.

Tận dụng tầng chung. Ảnh chia sẻ tầng nền chỉ kéo phần khác biệt. Chuẩn hoá ảnh nền trong toàn tổ chức tiết kiệm rất nhiều.

Registry gần. Registry trong cùng vùng, hoặc một bản sao đệm trong cụm.

Mỗi pod có một container ẩn

crictl pods   ->  POD ID a9a7f065a4001   NAME tm   Ready
crictl ps     ->  CONTAINER 996a893...   NAME tm   POD a9a7f065a4001
số container pause trên node:  7

Bảy container pause cho bảy pod, và chúng không xuất hiện trong kubectl get pods.

pause là một tiến trình gần như không làm gì — nó chỉ giữ không gian mạng cho cả pod. Container ứng dụng vào và ra; pause ở lại.

Đây là cơ chế đằng sau hai điều đã đo ở phần 44:

  • IP của pod không đổi khi container khởi động lại, vì không gian mạng thuộc về pause.
  • Các container trong pod gọi nhau qua localhost, vì chúng cùng tham gia vào không gian mạng của pause.

Nếu bạn từng thấy crictl ps có nhiều container hơn kubectl get pods gợi ý, đây là lý do.

Pod tĩnh: kubelet đọc thẳng từ đĩa

/etc/kubernetes/manifests/
   etcd.yaml
   kube-apiserver.yaml
   kube-controller-manager.yaml
   kube-scheduler.yaml

Bốn tệp này kubelet đọc trực tiếp từ đĩa, không qua apiserver.

Đây là câu trả lời cho một nghịch lý: nếu mọi pod cần apiserver để chạy, thì apiserver — vốn cũng là một pod — chạy bằng cách nào?

kubelet quét thư mục manifests và chạy mọi thứ trong đó ngay khi khởi động, không hỏi ai. Sau đó nó tạo một "pod gương" trong apiserver để bạn nhìn thấy chúng bằng kubectl — nhưng xoá pod gương đó không xoá container, vì nguồn sự thật là tệp trên đĩa.

Đây cũng là cơ chế phần 43 dùng để tắt từng thành phần: di chuyển tệp đi thì kubelet dừng container tương ứng.

Ba việc khác của kubelet

Kiểm tra sức khoẻ. livenessProbereadinessProbe do kubelet chạy, không phải apiserver. Đó là lý do chúng vẫn hoạt động khi control plane chết.

Dọn ảnh cũ. Khi đĩa vượt imageGCHighThresholdPercent, kubelet xoá ảnh không dùng. Trên cụm kéo nhiều ảnh lớn, đây là nguồn của việc "ảnh vừa có giờ lại phải kéo lại".

Đuổi pod khi node cạn tài nguyên. evictionHard đặt ngưỡng cho bộ nhớ và đĩa. Vượt là kubelet giết pod theo thứ tự: BestEffort trước (không khai requests), rồi Burstable, cuối cùng Guaranteed (requests bằng limits).

Đây là lý do thực tế nhất để khai requestslimits — nó quyết định pod nào chết trước khi node hết RAM.

kubelet là thành phần duy nhất không phải pod

Mọi thứ khác trong Kubernetes chạy dưới dạng container. kubelet thì không — nó là dịch vụ hệ thống (systemd), vì phải có nó thì container mới chạy được.

Hệ quả khi vận hành: kubelet chết thì kubectl không sửa được, phải vào thẳng máy. Và log của nó nằm ở journalctl -u kubelet, không ở kubectl logs.

# trong kind
docker exec <node> journalctl -u kubelet --no-pager | tail -50

Đây là chỗ đầu tiên nên nhìn khi một node báo NotReady mà bản thân máy vẫn ping được.

Thử ba mươi giây

Xem đoạn đường từ lệnh tới container trên cụm của bạn:

kubectl run thu --image=nginx:alpine
kubectl get events --field-selector involvedObject.name=thu \
  --sort-by=.metadata.creationTimestamp \
  -o jsonpath='{range .items[*]}{.metadata.creationTimestamp}{" "}{.reason}{"\n"}{end}'

Khoảng cách giữa ScheduledPulled là thời gian kéo ảnh. Nếu nó lớn hơn vài giây với ảnh bạn dùng hàng ngày, đó là chỗ tiết kiệm được ở mọi lần triển khai.

Phần sau đo Deployment và ReplicaSet: cơ chế đứng sau việc cập nhật không gián đoạn.