kind chạy Kubernetes bằng cách đặt mỗi node vào một container Docker. Bài này dựng cụm một node và ba node, đo tài nguyên, rồi giết một node để xem "tự chữa" thật sự mất bao lâu.

Thời gian dựng, tài nguyên chiếm, và đồng hồ đếm khi node chết

Dựng

kind create cluster --name do

Cụm 3 node cần một tệp cấu hình:

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
kind create cluster --name ba --config kind3.yaml
Tạo xong Tất cả Ready
cụm 1 node 10,4 giây +6,2 giây
cụm 3 node 17,2 giây ở giây thứ 29,8

Ba mươi giây cho một cụm Kubernetes ba node. Đây là lý do kind là công cụ mặc định để thử nghiệm và chạy CI.

Tài nguyên

ba-control-plane   555,5 MB
ba-worker          116,9 MB
ba-worker2         118,7 MB
------------------------------
tổng                 791 MB     cho 13 pod hệ thống, chưa có ứng dụng nào

Node worker rẻ: 117 MB mỗi cái. Cái đắt là control plane — etcd, kube-apiserver, kube-controller-manager, kube-scheduler cộng lại gần 556 MB.

Điều này khớp với trực giác về kiến trúc: control plane giữ toàn bộ trạng thái cụm và phục vụ mọi lời gọi API; worker chỉ chạy kubeletkube-proxy.

Với máy phát triển 16 GB, chạy một cụm 3 node tốn khoảng 5% bộ nhớ. Thêm worker gần như miễn phí.

Pod được xếp lên node nào

kubectl create deployment app --image=nginx:alpine --replicas=6
3 ba-worker
3 ba-worker2

Chia đều, và không pod nào lên control plane — kind đánh dấu node đó bằng một vết (taint) mà pod thường không chịu được. Đó là mặc định đúng: control plane không nên tranh tài nguyên với ứng dụng.

Node chết: gần mười phút và không có gì xảy ra

Đây là phép đo quan trọng nhất của bài.

docker stop ba-worker2     # 3 trong 6 pod đang chạy trên đó
Thời điểm Node NotReady Pod trên node chết kubectl báo Running
0 s 0 3 6
61 s 1 3 6
122 s 1 3 6
304 s 1 3 6
426 s 1 3 6
547 s 1 3 6
593 s 1 3 6

Gần mười phút. Không pod nào được chuyển đi, và kubectl get pods báo đủ sáu Running suốt thời gian đó — trong khi ba trong sáu bản đã không phục vụ được từ giây thứ 61.

Ba pod đó là pod ma. kubelet trên node chết không cập nhật được trạng thái, nên chúng đứng nguyên ở Running.

Vì sao Kubernetes không dựng bản thay thế

Đây là phần phản trực giác, và nó là hệ quả trực tiếp của hiện tượng trên.

Pod ma vẫn được tính là Ready. ReplicaSet đếm được 6 pod Ready, thấy đúng bằng số mong muốn, và không làm gì cả. Nó không biết ba trong số đó đã chết — theo mọi thông tin nó có, cụm đang ở trạng thái đúng.

Chỉ khi cơ chế đuổi pod theo vết (taint-based eviction) gỡ pod ma đi thì ReplicaSet mới thấy thiếu và dựng bản thay thế. Hai đồng hồ quyết định việc đó:

node-monitor-grace-period      40 giây     (đo được 61 giây)
tolerationSeconds mặc định    300 giây     trước khi đuổi pod

Cộng lại là khoảng sáu phút theo lý thuyết — nhưng phép đo này chạy tới 593 giây mà chưa thấy nó xảy ra, và tôi ghi lại đúng như vậy.

Ở một lần chạy trước đó, tôi có thấy ba pod thay thế được tạo. Nhưng lần chạy đó bị lẫn vì tôi đã bật lại node giữa chừng, nên tôi không dùng số của nó. Bảng trên là lần chạy sạch, và nó chỉ nói được một điều — nhưng nói chắc chắn: trong mười phút đầu, không có gì được sửa.

Hệ quả cho việc theo dõi

Bài học không nằm ở con số chính xác của đồng hồ, mà ở chỗ:

Đếm pod Running không cho biết ứng dụng có đủ bản đang phục vụ hay không.

Bảng theo dõi dựa vào kubectl get pods sẽ báo xanh trong suốt mười phút đó. Thứ báo đúng là:

# node nào không khoẻ
kubectl get nodes | grep -v ' Ready '

# endpoint thật sự đang nhận lưu lượng
kubectl get endpoints web-svc -o jsonpath='{.subsets[*].addresses[*].ip}' | wc -w

Lệnh thứ hai là lệnh đáng đặt cảnh báo: Endpoints chỉ chứa pod qua được kiểm tra sẵn sàng, nên pod ma trên node chết sẽ rơi khỏi danh sách sớm hơn nhiều so với việc nó rơi khỏi kubectl get pods.

Nếu muốn phản ứng nhanh hơn, hạ ngưỡng chịu đựng ngay trên pod:

tolerations:
  - key: node.kubernetes.io/not-ready
    operator: Exists
    effect: NoExecute
    tolerationSeconds: 30
  - key: node.kubernetes.io/unreachable
    operator: Exists
    effect: NoExecute
    tolerationSeconds: 30

Cái giá: mạng chập chờn 30 giây sẽ gây dựng lại pod. Với ứng dụng khởi động nhanh và không giữ trạng thái, đó là đánh đổi tốt. Với ứng dụng khởi động mất vài phút, nó làm mọi thứ tệ hơn.

Và so với phần trước để thấy khoảng cách: pod bị xoá được thay trong 1,1 giây; node chết thì mười phút chưa thấy gì.

Ba điều kind làm khác cụm thật

Không có bộ cân bằng tải. Service kiểu LoadBalancer sẽ mãi ở trạng thái <pending>. Dùng NodePort hoặc cài metallb.

Lưu trữ là local-path. PersistentVolume nằm trên đĩa của node, nên pod chuyển sang node khác thì mất dữ liệu. Cụm thật dùng lưu trữ mạng, và hành vi khác hẳn.

Ảnh phải được nạp thủ công. kind không thấy registry cục bộ của Docker:

kind load docker-image ten-anh:tag --name ba

Quên bước này là pod treo ở ErrImagePull với ảnh mà docker images rõ ràng đang có — một trong những chỗ mất thời gian nhất khi mới dùng kind.

Ba lệnh nên thuộc

# xem node và IP
kubectl get nodes -o wide

# xem pod ở mọi namespace, kèm node
kubectl get pods -A -o wide

# xem chuyện gì vừa xảy ra
kubectl get events --sort-by=.lastTimestamp | tail -20

Lệnh thứ ba là lệnh hữu ích nhất khi có gì đó không như mong đợi, và cũng là lệnh ít được dùng nhất. Nó cho biết bộ lập lịch đã quyết định gì và vì sao.

Thử ba mươi giây

Tự đo trên cụm của bạn:

kubectl create deployment app --image=nginx:alpine --replicas=6
docker stop <ten-node-worker>

while true; do
  echo "$(date +%T) NotReady=$(kubectl get nodes --no-headers | grep -c NotReady) \
Running=$(kubectl get pods -l app=app --no-headers | grep -c Running) \
Endpoints=$(kubectl get endpoints app -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null | wc -w)"
  sleep 15
done

Cột Running sẽ đứng yên rất lâu. Cột Endpoints là cột nói thật. Khoảng cách giữa hai cột đó là khoảng thời gian bảng theo dõi của bạn đang báo sai.

Phần sau đo Pod: đơn vị nhỏ nhất của Kubernetes, và vì sao nó không phải container.