Nghĩ về một cụm Kubernetes như một sân bay: đài kiểm soát không lưu và những chiếc máy bay đang trên trời. Đài kiểm soát cấp phép cất cánh, điều phối đường bay, dẫn máy bay tránh giông. Nhưng nếu đài tối đèn, những chiếc đang bay vẫn bay — động cơ vẫn chạy, hành khách vẫn ngồi yên. Cái mất đi là khả năng điều phối: không ai được cấp đường băng mới, không ai dẫn đường vòng qua sự cố. Và bạn chỉ nhận ra đài đã tắt vào đúng lúc cần nó nhất.
Sơ đồ kiến trúc Kubernetes có ở mọi nơi. Bài này làm khác: tắt từng thành phần một trên cụm đang chạy và đo xem cái gì hỏng, cái gì không.
Bộ nhớ thật của từng thành phần
Đo RSS trực tiếp trong node:
| Thành phần | kB |
|---|---|
kube-apiserver |
270.832 |
kube-controller-manager |
108.112 |
kubelet |
83.768 |
containerd |
70.656 |
coredns × 2 |
62.092 + 60.332 |
kube-scheduler |
56.488 |
etcd |
55.708 |
kube-proxy |
49.016 |
kindnetd |
47.072 |
local-path-provisioner |
41.196 |
kube-apiserver chiếm 265 MB — gấp gần năm lần etcd.
Điều này ngược với hình dung phổ biến rằng etcd là thành phần nặng. etcd chỉ lưu trữ; apiserver là cửa duy nhất vào cụm, phải xác thực, phân quyền, kiểm tra hợp lệ, chuyển đổi phiên bản API, và phục vụ hàng loạt watch cho mọi thành phần khác.
Tắt kube-scheduler
pod cũ: Running
pod mới: Pending Pending
Pod đang chạy không bị ảnh hưởng gì. Pod mới nằm ở Pending vô thời hạn.
Scheduler chỉ làm đúng một việc: quyết định pod chạy ở đâu. Nó ghi tên node vào spec.nodeName và hết trách nhiệm. Sau đó kubelet trên node đó nhận việc.
Bật lại thì hai pod chuyển sang Running gần như ngay lập tức — chúng vẫn nằm chờ trong etcd suốt thời gian đó.
Tắt kube-controller-manager
Xoá một pod của deployment 2 bản, đợi 20 giây:
còn 1/2 -> KHÔNG được thay thế
Bật lại controller-manager thì đủ 2 pod ngay lập tức.
Đây là thành phần thực hiện lời hứa ở phần 41: giữ số bản thật bằng số bản khai báo. Nó chạy một tập vòng lặp điều hoà — ReplicaSet, Deployment, Node, Endpoint, và hàng chục cái khác — mỗi vòng so trạng thái thật với mong muốn rồi sửa chênh lệch.
Không có nó, Kubernetes trở thành một nơi lưu cấu hình. Mọi thứ đang chạy vẫn chạy, nhưng không có gì được sửa nữa.
Tắt kube-apiserver
kubectl get pods: client rate limiter Wait returned an error: context deadline exceeded
số container nginx vẫn chạy: 4
kubectl chết hoàn toàn. Ứng dụng thì không.
Bốn container nginx vẫn chạy và vẫn phục vụ. kubelet đã có bản kê pod cần chạy và nó tiếp tục giữ chúng sống mà không cần hỏi ai.
Phục hồi khi bật lại: ~6 giây.
Tắt etcd
kubectl get nodes: Unable to connect to the server
container apiserver vẫn chạy: 1
container nginx vẫn chạy: 4
Chú ý dòng giữa: container apiserver vẫn chạy, nhưng vô dụng.
apiserver không có trạng thái riêng. Nó là một tầng dịch giữa API HTTP và etcd. Mất etcd là mất tất cả — dù tiến trình apiserver trông vẫn khoẻ trong mọi phép kiểm tra tiến trình.
Đây là lý do kiểm tra sức khoẻ theo "tiến trình có chạy không" là kiểu kiểm tra tệ nhất, và nó lặp lại bài học ở phần 31 của sê-ri Kafka.
Phục hồi etcd + apiserver: ~36 giây, chậm hơn sáu lần so với chỉ apiserver — vì apiserver phải đợi etcd sẵn sàng rồi mới khởi động được.
Kết luận thực dụng
Control plane chết không làm ứng dụng chết. Nó làm cụm mất khả năng tự chữa.
Và bạn chỉ phát hiện ra điều đó vào lần tiếp theo có thứ gì hỏng. Cụm mất control plane trông hoàn toàn bình thường: mọi trang web vẫn tải, mọi API vẫn trả lời. Cho tới khi một pod chết và không ai thay nó — đúng như chiếc máy bay đang bay cho tới lúc cần đổi đường.
Đây là lý do control plane cần được theo dõi riêng, và cần chạy nhiều bản trên nhiều máy. Cụm sản xuất thường có 3 bản etcd và 3 bản apiserver.
Thành phần trên node
Ba thứ chạy trên mọi node, kể cả control plane:
kubelet — 83.768 kB. Nó là thứ thật sự chạy container. Nó hỏi apiserver "pod nào thuộc về node của tôi", rồi bảo containerd tạo container, rồi báo cáo trạng thái ngược lại. Đây là thành phần duy nhất không chạy dưới dạng pod — nó là dịch vụ hệ thống, vì phải có nó thì pod mới chạy được.
containerd — 70.656 kB. Runtime thật sự. Kubernetes không tự chạy container; nó nói chuyện với runtime qua giao diện CRI.
kube-proxy — 49.016 kB. Dựng luật iptables (hoặc IPVS) để Service hoạt động. Phần sau về Service sẽ đo nó.
Vòng đời một lời gọi kubectl
Gộp lại thành một chuỗi, và mỗi mũi tên là một thành phần đã đo ở trên:
kubectl apply
-> kube-apiserver xác thực, phân quyền, kiểm tra hợp lệ
-> etcd ghi trạng thái mong muốn
-> kube-controller-manager thấy Deployment mới, tạo ReplicaSet, tạo Pod
-> kube-scheduler gán pod vào node
-> kubelet trên node đó, thấy pod của mình
-> containerd tạo container
Sáu bước, và mỗi bước đi qua apiserver. Không thành phần nào nói chuyện trực tiếp với thành phần khác — tất cả đều đọc và ghi qua API. Đó là lý do apiserver là thành phần nặng nhất, và cũng là lý do nó là điểm hỏng đáng lo nhất.
Cùng một lằn ranh "điều khiển vs thực thi", đổi ngăn xếp
Chuyện "tắt phần điều khiển đi mà phần làm việc vẫn chạy" không phải đặc sản Kubernetes. Nó là một kiểu tách tầng lặp lại ở khắp nơi, và kiểu hỏng đi kèm cũng giống hệt: âm thầm, chỉ lộ ra ở lần thay đổi tiếp theo.
Trong thế giới Java, nghĩ tới một hệ giám sát kiểu actor (Akka) hay một thread pool có bộ quản lý: các actor/luồng đang xử lý message vẫn xử lý bình thường nếu bộ giám sát chết — nhưng cái chết đó nghĩa là actor nào sập sẽ không còn được khởi động lại, và luồng nào chết sẽ không được thay. Công việc đang chạy không hề gợn, hỏng hóc chỉ hiện ra khi có cái gì đó cần được chữa mà không ai chữa.
Trong Go, đó là một goroutine giám sát đẻ lại các worker goroutine: worker vẫn chạy nếu supervisor thoát, nhưng worker chết thì nằm chết luôn.
Còn ở cơ sở dữ liệu, đúng lằn ranh này là primary đang phục vụ đọc-ghi với một bộ điều phối failover bên cạnh (như Patroni): CSDL vẫn chạy ngon nếu bộ điều phối chết — cho tới khi chính primary gục, và lúc đó không có ai thăng cấp replica lên thay. Hoặc nhẹ hơn: autovacuum và các background worker là "control plane" của sự khoẻ mạnh lâu dài — tắt chúng đi thì truy vấn hôm nay vẫn chạy, chỉ là bảng phình dần rồi một ngày chậm hẳn.
Mẫu số chung: một tầng điều khiển và một tầng thực thi hỏng theo hai kiểu rất khác nhau — mất tầng thực thi thì công việc dừng ngay và bạn thấy liền, còn mất tầng điều khiển thì công việc đang chạy vẫn chạy, cái mất đi là khả năng thích nghi và tự chữa, nên sự cố trở nên vô hình cho tới đúng lần tiếp theo có thứ gì đó cần được điều chỉnh. Và có một hệ quả về giám sát: kiểm tra "tiến trình còn sống không" là tín hiệu yếu nhất, vì một tiến trình có thể còn chạy mà hoàn toàn vô dụng khi cái nó phụ thuộc đã biến mất (như apiserver sống nhăn trong khi etcd đã chết). Nên câu hỏi đúng khi theo dõi một hệ tách tầng không phải "control plane còn sống không" mà là "nếu nó đã chết âm thầm từ sáng, thì đến khi nào tôi mới biết — và liệu có phải là đúng lúc một sự cố khác vừa nổ ra".
Muốn tự xem cả chuỗi đó chạy, bật thử:
kubectl get events -A --sort-by=.lastTimestamp -w &
kubectl create deployment thu --image=nginx:alpine
Bạn sẽ thấy Scheduled → Pulled → Created → Started lần lượt hiện ra, mỗi dòng do một thành phần khác nhau ghi. Đó là toàn bộ kiến trúc, quan sát từ bên ngoài.
Phần sau đo Pod: vì sao nó là đơn vị nhỏ nhất chứ không phải container, và một cái bẫy về thời gian xoá.