Hai chi nhánh của một công ty, mỗi bên tự đánh số phòng làm việc từ 101 tới 999. Trong nội bộ từng chi nhánh thì ổn. Nhưng ngày công ty muốn nối hệ thống thư nội bộ giữa hai toà nhà, họ vấp phải một chuyện không sửa nhanh được: "phòng 305" bây giờ mơ hồ — có hai cái, mỗi toà một cái, và không lá thư nào biết phải tới cái nào. Lỗi không nằm ở lá thư mà ở chỗ cả hai bên đã chọn cùng một dải số một cách độc lập. Hai cụm Kubernetes dựng theo mặc định rơi vào đúng cái bẫy này, và bài này đo nó cụ thể trên hai cụm thật chạy cạnh nhau.

"Chia thành nhiều cụm" nghe như một quyết định kiến trúc. Ta đo xem nó tốn gì cụ thể.

Chi phí cố định của mỗi cụm

Một cụm ba node, không chạy ứng dụng nào đáng kể. Đọc RSS thật của từng tiến trình:

kube-apiserver            298 MB
kube-controller-manager   113 MB
kube-scheduler             69 MB
etcd                       56 MB
                          ------
                          536 MB

Nửa gigabyte RAM trước khi có ứng dụng nào. Kèm 12 pod hệ thống phải giám sát, và một bộ chứng chỉ phải xoay vòng. Con số này không lớn với một máy chủ hiện đại. Nhưng nó là chi phí cố định nhân với số cụm, và nó tăng khi cụm bận: cụm bốn node có nhiều việc hơn của tôi dùng 1,4 GB cho chính node control plane.

Chi phí đáng kể hơn không nằm ở RAM mà ở phần con người: mỗi cụm là một phiên bản để nâng cấp, một bộ RBAC để đồng bộ, một chỗ nữa để cấu hình lệch đi.

Hai cụm mặc định dùng chung một dải IP

Đây là chỗ tôi không lường trước.

cụm sc   cluster-cidr = 10.244.0.0/16
cụm up   cluster-cidr = 10.244.0.0/16

Y hệt nhau. Đó là mặc định của kubeadm, nên mọi cụm dựng theo mặc định đều dùng chung dải này — đúng hai chi nhánh cùng đánh số phòng từ 101. Hậu quả đo được ngay:

pod bên up   10.244.1.7
pod bên sc   10.244.1.77

Hai địa chỉ trong cùng một dải, thuộc hai cụm khác nhau. Không có bảng định tuyến nào phân biệt được chúng.

gọi thẳng IP pod qua cụm  ->  TimeoutError (3001 ms)
phân giải tên Service kia ->  Name does not resolve

Cả hai tầng đều tách biệt: mạng không định tuyến, DNS không biết gì về nhau. CoreDNS của mỗi cụm chỉ phục vụ vùng cluster.local của chính nó.

Điều rút ra thực dụng: nếu bạn có thể sẽ nối hai cụm với nhau, hãy cấp dải IP khác nhau ngay từ khi tạo. Đổi --pod-network-cidr sau đó nghĩa là dựng lại cụm. Đây là quyết định năm phút lúc đầu, hoặc một dự án di trú về sau.

Đường đi được: NodePort

NodePort 32190 trên node IP 172.21.0.9  ->  KẾT NỐI ĐƯỢC

Cách duy nhất chạy được ngay là đi qua địa chỉ của node, thứ nằm ngoài dải pod và có thật trên mạng chung. Trong thực tế đó là NodePort, LoadBalancer, hoặc Ingress — và đi kèm mọi thứ bạn phải tự lo: địa chỉ cố định, TLS, xác thực, phát hiện dịch vụ. Cái mà trong một cụm là http://web thì qua cụm trở thành một điểm cuối phải quản lý.

Đo độ trễ, và một lần tôi tự đo sai

trong cụm, tên DNS      p50 0,64 ms
trong cụm, ClusterIP    p50 0,17 ms
trong cụm, IP pod       p50 0,15 ms
qua cụm,  NodePort      p50 0,17 ms

Lần đầu tôi chỉ đo hai dòng: "trong cụm bằng tên DNS" (0,64 ms) và "qua cụm bằng NodePort" (0,17 ms). Kết quả trông như gọi qua cụm nhanh hơn gọi trong cụm, và tôi suýt viết ra điều đó.

Sai. Tôi so một tên DNS với một IP trần. Thư viện HTTP phân giải tên ở mỗi kết nối, và 0,47 ms chênh lệch là chi phí của DNS chứ không phải của mạng. Đo lại bằng ClusterIP thì hai bên bằng nhau — 0,17 ms cả hai. Hai cụm này nằm trên cùng một cầu mạng Docker nên qua cụm gần như không tốn gì; giữa hai vùng địa lý thì con số sẽ khác hẳn, và đó là điều phép đo này không nói được.

Bài học đo đạc: khi so hai đường đi, hãy giữ mọi thứ khác giống nhau. Một tên miền lẫn vào là đủ để đảo ngược kết luận. Nhân tiện, dòng đầu và dòng hai đo lại đúng điều đã thấy ở phần về DNS: phân giải tên tốn gần gấp bốn lần phần mạng. Với dịch vụ gọi nhau hàng nghìn lần mỗi giây, dùng lại kết nối là cách tiết kiệm rẻ nhất.

Khi nào thật sự cần nhiều cụm

Bốn lý do đứng vững:

  • Ranh giới sự cố. Một cụm hỏng không kéo theo cụm kia. Đây là lý do mạnh nhất, và cũng là lý do duy nhất mà namespace không thay thế được.
  • Ranh giới địa lý. Độ trễ giữa các vùng khiến một cụm trải rộng là ý tồi; etcd đặc biệt nhạy với chuyện này.
  • Yêu cầu tuân thủ. Dữ liệu phải nằm trong một quốc gia, hoặc phải cách ly vật lý.
  • Giới hạn quy mô. Kubernetes hỗ trợ tới 5.000 node, nhưng etcd và số đối tượng thường chạm trần sớm hơn nhiều.

Và lý do không đứng vững: tách theo môi trường, tách theo đội, tách theo ứng dụng. Ba việc đó namespace + RBAC + ResourceQuota làm được, với chi phí bằng không.

Ranh giới thực dụng: thêm một cụm khi bạn cần một ranh giới sự cố; đừng thêm cụm khi bạn chỉ cần một ranh giới quyền hạn.

Nếu buộc phải nối

Nhu cầu Cách
Một kubectl cho nhiều cụm Nhiều context trong kubeconfig
Triển khai cùng thứ ra mọi cụm Argo CD ApplicationSet, Flux
Gọi dịch vụ qua cụm Ingress/LoadBalancer công khai, hoặc service mesh đa cụm
Tên DNS dùng chung Multi-cluster Services, hoặc DNS ngoài
Chuyển đổi khi hỏng Cân bằng tải toàn cục ở tầng DNS

Dòng thứ ba và thứ tư là chỗ tốn công nhất, và cũng là chỗ dễ đánh giá thấp nhất — vì trong một cụm chúng miễn phí.

Mẫu số chung: hai hệ chọn cùng không gian tên mặc định thì không hợp nhất được

Cái bẫy dải IP trùng là một quy luật đúng ở mọi hệ phân tán: hai hệ dựng độc lập với cùng một không gian địa chỉ/tên mặc định thì không thể nối hay hợp nhất — vì mọi định danh trở nên mơ hồ, đúng "phòng 305" có hai cái. Và quyết định cấp phát không-gian-tên phải làm sớm, vì đổi sau khi đã có dữ liệu/địa chỉ nghĩa là dựng lại hoặc di trú, không phải sửa một dòng cấu hình.

Trong thế giới Java và cơ sở dữ liệu, đây chính xác là bẫy khoá tự tăng: hai dịch vụ mỗi bên có bảng đánh id từ 1, và ngày cần gộp hai bảng thì id=42 đụng nhau — không hợp nhất được nếu không đánh lại toàn bộ. Đó là lý do hệ phân tán dùng UUID/ULID hay khoá có tiền tố dịch vụ ngay từ đầu, thay vì auto-increment: chọn không gian định danh không đụng nhau trước, để về sau nối được. Còn ở tầng hạ tầng, đây đúng là bẫy CIDR khi peer hai VPC: hai mạng cùng dùng 10.0.0.0/16 (dải riêng RFC 1918 mặc định) thì không peer được — cùng một sự thật với cùng một mặc định.

Mẫu số chung: khi nhiều hệ được dựng độc lập nhưng có thể phải nối về sau, không gian định danh/địa chỉ của chúng phải phân biệt ngay từ đầu — vì mặc định thường trùng nhau, và sửa sau khi đã có trạng thái là dựng lại chứ không phải cấu hình. Câu hỏi đúng lúc tạo cụm/dịch vụ/bảng thứ hai không phải "nó chạy chưa" mà là "nếu ngày nào đó tôi phải nối nó với cái thứ nhất, định danh có đụng nhau không" — trả lời bằng cách cấp dải/tiền tố riêng ngay, tốn năm phút thay vì một dự án di trú.

Muốn tự kiểm các cụm của bạn có dải IP trùng nhau không, trước khi ai đó cần nối chúng lại:

for c in $(kubectl config get-contexts -o name); do
  echo -n "$c: "
  kubectl --context "$c" get node -o jsonpath='{.items[0].spec.podCIDR}' 2>/dev/null
  kubectl --context "$c" get svc kubernetes -n default \
    -o jsonpath='  svc={.spec.clusterIP}{"\n"}' 2>/dev/null
done

Trùng nhau là bình thường — đó là mặc định. Nhưng biết trước thì rẻ hơn nhiều so với phát hiện ra vào ngày ai đó nói "nối hai cụm này lại đi".

Phần sau: chi phí và tối ưu tài nguyên — đo chỗ tiền thật sự rò ra.