controller-manager và scheduler: Control Loop và Leader Election
api-server ở Bài 7 chỉ làm một việc: nhận và lưu trạng thái mong muốn. Nó không tự tạo pod, không chọn node, không dựng lại gì khi có thứ chết. Những việc đó thuộc về hai thành phần bài này: kube-controller-manager và kube-scheduler. Cả hai là client của api-server (dùng kubeconfig đã tạo ở Bài 5), và vì ta chạy ba bản mỗi loại, cả hai phải bầu leader — ta sẽ thấy điều đó xảy ra thật.
controller-manager: nơi các control loop sống
Nhớ lại Bài 1: một controller là một vòng lặp liên tục so trạng thái mong muốn với thực tế rồi hành động cho khớp. kube-controller-manager là một tiến trình gom hàng chục vòng lặp như vậy — Deployment, ReplicaSet, Node, Job, ServiceAccount, EndpointSlice... Thay vì chạy hàng chục tiến trình riêng, Kubernetes gói chúng vào một binary cho gọn.
Trong cấu hình của ta, controller-manager còn giữ vài vai đặc biệt cần cấu hình đúng:
- Ký CSR (
--cluster-signing-cert-file=ca.pem,--cluster-signing-key-file=ca-key.pem): nó là thành phần ký các yêu cầu certificate trong cluster. Đây là lý do ta phải đưa cảca-key.pemlên controller ở Bài 7. - Ký token ServiceAccount (
--service-account-private-key-file=service-account-key.pem): nhớ Bài 2, controller-manager dùng private key để ký token, api-server dùng public key để kiểm. - Biết dải mạng pod (
--cluster-cidr=10.200.0.0/16): dải ta sẽ chia cho từng node ở phần mạng (Bài 13–14).
scheduler: lọc rồi chấm điểm
kube-scheduler theo dõi các pod chưa được gán node (spec.nodeName trống), và với mỗi pod, quyết định nó nên chạy ở node nào. Quyết định đó đi qua hai pha:
Tất cả node
│
▼ PHA 1 — FILTERING (lọc)
loại các node KHÔNG đủ điều kiện:
thiếu CPU/RAM, không khớp nodeSelector/affinity,
dính taint mà pod không tolerate, port đã bận...
│
▼ (còn lại các node khả thi)
▼ PHA 2 — SCORING (chấm điểm)
cho mỗi node khả thi một điểm theo nhiều tiêu chí
(cân tải, ưu tiên trải đều, dữ liệu cục bộ...)
│
▼
node điểm cao nhất ──► ghi spec.nodeName cho pod (qua api-server)
scheduler chỉ chọn và ghi tên node vào pod; nó không khởi container. Việc khởi container thuộc kubelet ở node được chọn (Bài 11). Đây là kiểu phân vai sạch sẽ rất Kubernetes: mỗi thành phần làm đúng một việc rồi để lại kết quả cho thành phần sau qua trạng thái trong cluster.
Leader election: ba bản, một người làm
Cả controller-manager lẫn scheduler đều chạy ba bản (mỗi controller một bản). Nếu cả ba cùng hoạt động, ta sẽ có ba scheduler cùng gán node cho một pod, hoặc ba ReplicaSet controller cùng tạo pod — hỗn loạn. Giải pháp là leader election: ba bản tranh nhau giữ một khóa, chỉ kẻ giữ khóa mới làm việc.
Khóa đó là một đối tượng Lease trong namespace kube-system. Bản nào đang giữ Lease sẽ định kỳ "gia hạn"; nếu nó chết và không gia hạn nữa, sau một thời gian một bản khác giành lấy và tiếp quản. Ta bật cơ chế này bằng --leader-elect=true (controller-manager) và leaderElection.leaderElect: true (scheduler).
Bước 1 — Đưa kubeconfig lên controller
Binary kube-controller-manager và kube-scheduler đã tải sẵn ở Bài 7. Giờ chỉ cần đưa hai kubeconfig tương ứng (đã tạo ở Bài 5, trỏ 127.0.0.1:6443) lên mỗi controller:
# từ workstation, trong ~/k8s-scratch/pki
for h in controller-0 controller-1 controller-2; do
scp kube-controller-manager.kubeconfig kube-scheduler.kubeconfig ${h}:/tmp/
ssh $h 'sudo mv /tmp/kube-controller-manager.kubeconfig /tmp/kube-scheduler.kubeconfig /var/lib/kubernetes/'
done
ca.pem, ca-key.pem, và service-account-key.pem đã có sẵn trong /var/lib/kubernetes từ Bài 7, nên không phải copy lại.
Bước 2 — systemd unit cho controller-manager
Unit giống hệt nhau trên cả ba controller (leader election lo phần phối hợp, nên không cần phân biệt theo máy):
[Unit]
Description=Kubernetes Controller Manager
After=network.target
[Service]
ExecStart=/usr/local/bin/kube-controller-manager \
--bind-address=0.0.0.0 \
--cluster-cidr=10.200.0.0/16 \
--cluster-name=kubernetes \
--cluster-signing-cert-file=/var/lib/kubernetes/ca.pem \
--cluster-signing-key-file=/var/lib/kubernetes/ca-key.pem \
--kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \
--authentication-kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \
--authorization-kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \
--leader-elect=true \
--root-ca-file=/var/lib/kubernetes/ca.pem \
--service-account-private-key-file=/var/lib/kubernetes/service-account-key.pem \
--service-cluster-ip-range=10.32.0.0/24 \
--use-service-account-credentials=true \
--v=2
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
--use-service-account-credentials=true đáng nói: nó khiến mỗi control loop bên trong dùng một ServiceAccount riêng khi gọi api-server, thay vì dùng chung một danh tính. Như vậy RBAC phân quyền cho từng loop ở mức tối thiểu cần thiết.
Bước 3 — Config và unit cho scheduler
scheduler hiện đại nhận cấu hình qua một file KubeSchedulerConfiguration thay vì nhồi hết vào cờ. Tạo file đó, trỏ tới kubeconfig và bật leader election:
sudo tee /var/lib/kubernetes/kube-scheduler.yaml >/dev/null <<'EOF'
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /var/lib/kubernetes/kube-scheduler.kubeconfig
leaderElection:
leaderElect: true
EOF
Unit của scheduler nhờ vậy rất ngắn:
[Unit]
Description=Kubernetes Scheduler
After=network.target
[Service]
ExecStart=/usr/local/bin/kube-scheduler \
--config=/var/lib/kubernetes/kube-scheduler.yaml \
--v=2
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Ghi cả hai unit và file config vào mỗi controller, rồi nạp lại và bật:
sudo systemctl daemon-reload
sudo systemctl enable kube-controller-manager kube-scheduler
Bước 4 — Khởi động và kiểm chứng
Start cả hai service trên ba controller:
for h in controller-0 controller-1 controller-2; do
ssh $h 'sudo systemctl start kube-controller-manager kube-scheduler'
done
Kiểm tra trạng thái:
for h in controller-0 controller-1 controller-2; do
printf "%-14s cm=%s sched=%s\n" "$h" \
"$(ssh $h 'systemctl is-active kube-controller-manager')" \
"$(ssh $h 'systemctl is-active kube-scheduler')"
done
controller-0 cm=active sched=active
controller-1 cm=active sched=active
controller-2 cm=active sched=active
Khi một service không lên — đọc log, đừng đoán. Lần đầu tôi chạy,
kube-controller-managertrên controller-0 cứactivatingrồi restart liên tục.journalctl -u kube-controller-managercho thấystatus=203/EXEC— mã lỗi systemd báo "không chạy được file binary". So sánh kích thước file lộ ra nguyên nhân: binary trên controller-0 chỉ 12MB, trong khi ở controller-1 là 74MB — nó bị tải cụt từ Bài 7 (đúng cái bẫycurlđứt giữa chừng đã cảnh báo). Tải lại cho đủ rồisystemctl restartlà xong:bash ls -l /usr/local/bin/kube-controller-manager # 12582912 — quá nhỏ! sudo curl -fSL -o /usr/local/bin/kube-controller-manager \ https://dl.k8s.io/release/v1.36.1/bin/linux/amd64/kube-controller-manager sudo chmod +x /usr/local/bin/kube-controller-manager sudo systemctl restart kube-controller-manager
Giờ phần thú vị: xem leader election. Mỗi loại có một Lease trong kube-system, và trường holderIdentity cho biết bản nào đang giữ. Gọi api-server bằng cert admin:
C="--cacert ca.pem --cert admin.pem --key admin-key.pem"
curl -s $C "https://127.0.0.1:6443/apis/coordination.k8s.io/v1/namespaces/kube-system/leases" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); [print(i["metadata"]["name"],"->",i["spec"].get("holderIdentity")) for i in d["items"]]'
apiserver-tyzveoctxbnh6lvbkdt2xqncle -> apiserver-tyzveoctxbnh6lvbkdt2xqncle_14cfdd62-...
apiserver-wlwnjbc6t2b2g566g3usjzlbdm -> apiserver-wlwnjbc6t2b2g566g3usjzlbdm_979b5321-...
apiserver-zjy5m4qienmmbtxvgklj3tgz6i -> apiserver-zjy5m4qienmmbtxvgklj3tgz6i_18335984-...
kube-controller-manager -> controller-1_2bc8f705-33eb-46f5-bf37-a7c9946369a6
kube-scheduler -> controller-0_422ddcd2-4dd9-46a2-b7d4-819843c626c0
Đọc kết quả: leader của controller-manager đang là controller-1, leader của scheduler là controller-0. Hai bản còn lại của mỗi loại đang chạy không tải, chờ đến lượt. (Ba lease apiserver-... là chuyện khác: chúng là danh tính HA mà mỗi api-server tự đăng ký, không phải leader election.)
Nếu giờ bạn dừng controller-manager trên controller-1, sau ít giây một trong hai bản còn lại sẽ giành Lease và holderIdentity đổi sang tên máy đó — đúng cơ chế chịu lỗi mà ta thiết kế. Bạn có thể tự thử để thấy tận mắt.
🧹 Dọn dẹp
Cả hai service là thành phần thường trú, không tắt. Nhớ xóa các file cert tạm nếu bạn copy lên controller để chạy lệnh curl kiểm tra.
Tổng kết
Control plane về cơ bản đã đủ: etcd lưu trạng thái, api-server làm cổng vào, controller-manager chạy các control loop, scheduler chọn node — và cả hai loại sau đều đã bầu leader đúng cách. Điều đáng mang theo từ bài này không chỉ là các cờ, mà là thấy leader election là một cơ chế cụ thể (một Lease, một holderIdentity) chứ không phải khái niệm trừu tượng.
Nhưng đến giờ ta vẫn gọi api-server qua 127.0.0.1 trên từng controller, và chưa có worker nào. Bài 9 dựng HAProxy trên lb-0 để gom ba api-server thành một địa chỉ, cấu hình kubectl trên laptop trỏ qua Elastic IP của nó, và thiết lập RBAC để api-server được phép gọi xuống kubelet — chuẩn bị nốt trước khi thêm worker vào cluster ở Bài 10.