Nghĩ về khác biệt giữa đạp ga và bật cruise control. Khi bạn đạp ga, bạn đặt chân ga ở một vị trí; lên dốc thì xe chậm lại, và chẳng có ai tự sửa cho bạn. Cruise control thì khác: bạn khai báo một tốc độ mong muốn, rồi một cơ chế ngồi đó liên tục chỉnh chân ga để giữ đúng tốc độ ấy bất kể dốc lên dốc xuống. Nó tốn thêm máy móc, nhưng nó chịu được những con dốc bạn không thấy trước. Đây chính xác là khác biệt giữa Docker và Kubernetes — và tôi sẽ đo nó, kể cả cái giá của phần máy móc thêm vào.
Kubernetes thường được giới thiệu bằng sơ đồ kiến trúc. Bài này bắt đầu khác: dựng cùng một ứng dụng bằng Docker thuần và bằng Kubernetes, rồi đo cả hai.
Cùng một việc, hai cái giá
Chạy 3 bản nginx:
| Thời gian | Bộ nhớ | |
|---|---|---|
docker run × 3 |
0,5 giây | ~14 MB mỗi container |
kubectl create deployment |
10,6 giây | cộng 743 MB cho chính cụm |
Và một cụm Kubernetes rỗng — chưa chạy ứng dụng nào — đã có 9 pod hệ thống:
etcd kube-apiserver
kube-controller-manager kube-scheduler
kube-proxy kindnet
coredns × 2 local-path-provisioner
Docker thắng rõ ràng ở mọi chỉ số trong bảng trên: nhanh hơn hai mươi lần, không tốn bộ nhớ nền nào.
Vậy 743 MB đó mua được gì? Câu trả lời chỉ hiện ra khi có thứ gì đó hỏng.
Giết một thứ đang chạy
docker kill w2
w2: Exited (137) 39 seconds ago
Nằm im. Mãi mãi. Cho tới khi có người để ý.
kubectl delete pod web-6d689fbfdf-gtgxc
3 pod chạy lại sau 1,1 giây
Và nếu tiến trình bên trong pod chết — mô phỏng ứng dụng sập chứ không phải container bị xoá:
kubectl exec <pod> -- sh -c 'kill 1'
pod vẫn Running, cột RESTARTS = 1
Đây là khác biệt cơ bản nhất, và nó không phải chuyện kỹ thuật mà là chuyện mô hình:
Docker chạy thứ bạn bảo nó chạy. Kubernetes giữ thứ bạn khai báo là phải có.
Bạn nói "tôi muốn ba bản nginx". Kubernetes so trạng thái thật với lời khai đó, liên tục, và sửa chênh lệch. Không quan trọng vì sao chênh — container chết, máy sập, ai đó xoá nhầm.
Docker Compose có restart: unless-stopped và nó xử lý được trường hợp container chết. Nó không xử lý được máy chết.
Cập nhật phiên bản trong lúc đang phục vụ
kubectl set image deployment/web nginx=nginx:1.27-alpine
Trong lúc đó, gọi HTTP liên tục vào dịch vụ:
200 lời gọi: 200 thành công, 0 thất bại
Không mất một yêu cầu nào trong khi cả ba pod được thay bằng phiên bản mới.
Kubernetes dựng pod mới, đợi nó sẵn sàng, rồi mới xoá pod cũ — từng cái một, và luôn giữ đủ số bản đang phục vụ.
Làm việc này bằng tay với Docker cần một kịch bản: dựng container mới, kiểm tra sức khoẻ, thêm vào bộ cân bằng tải, bỏ container cũ ra, chờ kết nối đang mở đóng lại, xoá. Rồi xử lý trường hợp bản mới hỏng. Rồi xử lý trường hợp kịch bản bị ngắt giữa chừng.
Kịch bản đó chính là thứ Kubernetes thay thế. Nếu bạn đã từng viết nó, bạn biết nó dài bao nhiêu và hỏng theo bao nhiêu cách.
Bốn thứ khác Kubernetes làm sẵn
Khám phá dịch vụ. web-svc là một tên DNS trong cụm, tự trỏ tới các pod đang khoẻ. Pod chết thì tên đó tự bỏ nó ra. Với Docker, đây là chỗ người ta dựng Consul hoặc sửa tay tệp cấu hình nginx.
Xếp chỗ theo tài nguyên. Khai requests: cpu: 500m thì bộ lập lịch chỉ đặt pod lên node còn đủ chỗ. Với Docker, việc quyết định "máy nào chạy cái gì" là của con người.
Cấu hình và bí mật tách khỏi ảnh. ConfigMap và Secret gắn vào pod lúc chạy, nên cùng một ảnh chạy được ở mọi môi trường.
Tự mở rộng. Tải tăng thì tăng số bản, tải giảm thì giảm. Cần chỉ số, cần cấu hình, nhưng cơ chế có sẵn.
Ngưỡng đáng dùng
Con số 743 MB và 10,6 giây ở đầu bài là thật, và chúng là lý do Kubernetes không phải lựa chọn mặc định.
Dùng Docker Compose khi:
- Một máy, và bạn chấp nhận máy đó là điểm hỏng duy nhất.
- Vài container, ít khi thay đổi.
- Đội nhỏ, không có ai chuyên vận hành.
Với hoàn cảnh đó, 743 MB và toàn bộ độ phức tạp đi kèm không mua lại được gì tương xứng.
Dùng Kubernetes khi:
- Nhiều máy, và bạn cần ứng dụng sống sót khi một máy chết.
- Cập nhật thường xuyên và không được gián đoạn.
- Nhiều đội cùng chạy dịch vụ trên chung hạ tầng.
- Tải thay đổi mạnh và bạn muốn tự co giãn.
Nói cách khác: Kubernetes trả lời câu hỏi "chuyện gì xảy ra khi có thứ hỏng". Nếu bạn chưa cần trả lời câu đó, bạn chưa cần Kubernetes.
Một hiểu nhầm phổ biến
Kubernetes không thay thế Docker. Nó điều phối container, và container vẫn được chạy bởi một runtime — containerd trong cụm tôi vừa dựng, như kubectl get nodes -o wide cho thấy.
Bạn vẫn viết Dockerfile, vẫn xây ảnh, vẫn đẩy lên registry. Cái đổi là ai quyết định chạy chúng ở đâu và giữ chúng sống.
Cùng một lằn ranh "mệnh lệnh vs khai báo", đổi ngăn xếp
Khác biệt lõi ở đây — "làm thứ tôi bảo" với "giữ thứ tôi khai" — không phải phát minh của Kubernetes. Nó là một trong những lằn ranh thiết kế sâu nhất trong lập trình, và ở đâu cũng đi kèm đúng cái giá ấy: một cơ chế phải chạy suốt để điều hoà.
Trong thế giới Java/Spring, bạn có thể tự new từng đối tượng và tự nối chúng lại (mệnh lệnh), hoặc khai báo các bean rồi để container IoC dựng và duy trì cả đồ thị phụ thuộc (khai báo) — đổi lại phải nuôi một container chạy nền. Ngay cả bộ thu gom rác cũng là một vòng điều hoà: bạn "khai báo" rằng mình không cần một object nữa bằng cách buông tham chiếu, và một cơ chế chạy nền liên tục kéo bộ nhớ về trạng thái mong muốn — bạn trả CPU nền để khỏi phải tự free() từng mảnh.
Còn ở cơ sở dữ liệu, đây là khác biệt giữa viết thủ tục cập nhật từng bước với khai báo một ràng buộc (CHECK, FOREIGN KEY) hay một materialized view: bạn nêu bất biến cần giữ, engine liên tục giữ cho nó đúng sau mọi lần ghi — trả bằng chi phí kiểm tra mỗi lần ghi. Và cả Terraform ở tầng hạ tầng cũng đúng mô hình này: khai trạng thái mong muốn, một bộ điều hoà kéo thực tại về khớp.
Mẫu số chung: có hai cách ra lệnh cho máy — mệnh lệnh ("làm bước này, rồi bước kia, ngay bây giờ") và khai báo ("giữ cho điều này luôn đúng") — và mô hình khai báo luôn phải trả một khoản phí thường trực: một cơ chế điều hoà phải luôn chạy và luôn quan sát để kéo thực tại về phía trạng thái mong muốn; đổi lại, nó chịu được những nhiễu loạn bạn không lường trước mà không cần bạn viết tay cách xử lý từng cái. Nên câu hỏi đúng khi chọn không phải "cái nào mạnh hơn" mà là "tôi có đủ nhiều nhiễu loạn bất ngờ — máy chết, tiến trình sập, cấu hình trôi — để đáng nuôi một cơ chế chạy suốt ngày chỉ để tự sửa không". 743 MB chính là giá của cái cơ chế đó, và nó chỉ đáng khi bạn thật sự cần nó tự sửa.
Muốn tự thấy nó tự chữa, dựng thử một cụm:
kind create cluster --name thu
kubectl create deployment web --image=nginx:alpine --replicas=3
kubectl delete pod $(kubectl get pods -l app=web -o jsonpath='{.items[0].metadata.name}')
kubectl get pods -l app=web -w
Bạn sẽ thấy pod thứ tư xuất hiện trước khi pod bị xoá kịp biến mất. Đó là toàn bộ ý tưởng của Kubernetes, gói trong một dòng đầu ra.
Phần sau dựng cụm nhiều node bằng kind và đo: node chết thì mất bao lâu pod mới được chuyển đi.