Mô Hình Mạng Của Kubernetes
Ba bài vừa rồi dựng đủ bộ thành phần trên worker: containerd chạy container, kubelet điều phối node, kube-proxy hiện thực Service. Nhưng node vẫn NotReady, và lý do luôn là một câu: cni plugin not initialized. Pod chưa có mạng. Bài sau sẽ nối mạng đó bằng tay, nhưng nếu lao vào gõ lệnh ngay thì dễ làm mà không hiểu vì sao. Bài này lùi một bước, dựng nền lý thuyết: Kubernetes đòi hỏi một mô hình mạng ra sao, và những giải pháp khác nhau khớp vào đòi hỏi đó thế nào.
Bốn yêu cầu nền
Kubernetes không tự cài đặt mạng pod. Nó chỉ đặt ra một bản hợp đồng và để phần hiện thực cho bên thứ ba (CNI plugin). Hợp đồng đó, theo tài liệu chính thức, gồm những yêu cầu sau:
- Mỗi pod có một IP duy nhất trong toàn cluster — "Each pod in a cluster gets its own unique cluster-wide IP address." Không phải IP theo container, mà theo pod.
- Mọi pod nói chuyện được với mọi pod, không NAT — "All pods can communicate with all other pods, whether they are on the same node or on different nodes ... without the use of proxies or address translation (NAT)." Pod ở node A gọi pod ở node B bằng đúng IP thật của pod B, và pod B thấy nguồn là đúng IP thật của pod A.
- Agent trên node nói chuyện được với mọi pod trên node đó — "Agents on a node (such as system daemons, or kubelet) can communicate with all pods on that node." Đây là cách kubelet chạy được health check tới pod.
- Các container trong cùng một pod chia sẻ network namespace — chúng thấy chung một IP và nói chuyện với nhau qua
localhost.
Điều đáng chú ý là từ không NAT. Nó phân biệt mô hình Kubernetes với mạng container mặc định của Docker, nơi mỗi container nằm sau một bridge NAT và phải map cổng ra host để lộ diện. Trong Kubernetes, mạng phẳng: IP của pod là địa chỉ thật mà mọi nơi trong cluster định tuyến tới được, không che giấu sau cổng host. Mô hình phẳng này làm mọi thứ phía trên (Service, DNS, NetworkPolicy) suy luận đơn giản hơn nhiều, vì địa chỉ một pod nhìn thấy chính là địa chỉ pod khác dùng để gọi nó.
Pod chia sẻ một network namespace
Yêu cầu thứ tư đáng dừng lại, vì nó giải thích "pod" thực ra là gì về mặt mạng. Một pod không phải một container — nó là một nhóm container dùng chung một số namespace của Linux, trong đó có network namespace. Cụ thể: container pause (Bài 10) được tạo trước tiên và giữ network namespace của pod; các container ứng dụng sau đó tham gia vào đúng namespace ấy thay vì tạo riêng.
┌─────────────── Pod (một network namespace) ───────────────┐
│ pod IP: 10.200.0.7 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ pause │ │ app:8080 │ │ sidecar:9090 │ │
│ └──────────┘ └────┬─────┘ └──────┬───────┘ │
│ (giữ netns) │ localhost │ │
│ └────────────────┘ │
│ cùng IP, nói qua 127.0.0.1 │
└────────────────────────────────────────────────────────────┘
Hệ quả thực tế: hai container trong cùng pod không thể nghe trùng cổng (chúng chia một IP), và chúng gọi nhau qua localhost chứ không qua tên dịch vụ. Đó là lý do pause quan trọng: nó là cái neo giữ namespace sống kể cả khi container ứng dụng restart, để pod không mất IP giữa chừng.
Bốn kiểu giao tiếp
Gộp lại, lưu lượng trong một cluster rơi vào bốn kiểu, mỗi kiểu do một cơ chế khác nhau lo:
- Container ↔ container trong cùng pod — qua
localhost, nhờ chia network namespace. Không cần gì thêm. - Pod ↔ pod cùng node — qua một bridge trên node: hai pod cắm vào cùng một cầu ảo, chuyển gói cho nhau ở tầng 2. CNI plugin dựng cái bridge này.
- Pod ↔ pod khác node — phần khó, bàn ở mục sau. Cần một cách để gói rời node A tới được đúng node B rồi vào đúng pod.
- Pod ↔ Service — qua kube-proxy (Bài 12): ClusterIP ảo được DNAT thành một pod endpoint thật. Lưu ý kube-proxy chỉ đổi đích gói; còn việc gói đó đi tới được pod đích vẫn dựa trên ba kiểu trên.
Ba kiểu đầu là chuyện của CNI; kiểu thứ tư là chuyện của kube-proxy, đã xong. Nên việc còn lại của series về mạng quy về: dựng bridge trên mỗi node, và nối các bridge giữa các node.
Pod-to-pod xuyên node: phần khó
Trong một node, mọi chuyện đơn giản: các pod cùng treo trên một bridge, kernel chuyển gói giữa chúng. Vấn đề nảy sinh khi pod ở worker-0 muốn gọi pod ở worker-1. Gói có đích là một IP pod (ví dụ 10.200.1.5), nhưng IP đó không tồn tại trên mạng vật lý của VPC; nó chỉ có nghĩa bên trong mạng pod. Mạng dưới (ở đây là VPC của AWS) không biết phải gửi 10.200.1.5 đi đâu.
Cách giải bài này tách thành hai họ:
Overlay (đường hầm). Bọc gói pod-to-pod vào trong một gói khác có đích là IP node. Node A gói gói 10.200.0.7 → 10.200.1.5 vào một gói UDP/VXLAN gửi tới IP thật của node B (10.0.1.21); node B mở gói ra và giao cho pod. Mạng dưới chỉ thấy lưu lượng node-to-node bình thường, hoàn toàn không cần biết về dải pod. Đổi lại, mỗi gói cõng thêm phần header bọc ngoài (chi phí encapsulation) và giảm MTU hữu dụng. Flannel (chế độ VXLAN), Calico (chế độ overlay) đi đường này. Ưu điểm lớn: chạy được ở gần như mọi hạ tầng, kể cả khi ta không kiểm soát được bảng định tuyến của mạng dưới.
Native routing (định tuyến L3). Không bọc gì cả; thay vào đó dạy mạng dưới cách định tuyến dải pod. Mỗi node được cấp một dải con riêng (ví dụ worker-0 giữ 10.200.0.0/24, worker-1 giữ 10.200.1.0/24), rồi ta thêm route: "muốn tới 10.200.1.0/24 thì gửi cho worker-1". Gói pod-to-pod đi nguyên vẹn, không header thừa, nhanh hơn, nhưng đòi hỏi ta kiểm soát được định tuyến của mạng dưới, và mạng dưới phải chịu chuyển những gói có IP nguồn/đích lạ (không phải IP của chính node).
OVERLAY (VXLAN) NATIVE ROUTING (L3)
pod A ─► [ gói pod ] pod A ─► [ gói pod ]
bọc trong đi thẳng,
[ gói node→node ] ─► mạng mạng dưới có route
node B mở ra ─► pod B 10.200.1.0/24 → worker-1 ─► pod B
Cụm của ta sẽ đi đường nào
Cụm này nằm gọn trong một subnet của một VPC AWS (10.0.1.0/24), nên ta chọn native routing, không overlay. Lý do và các mảnh đã chuẩn bị sẵn:
- Cấp dải pod theo node. Dải pod tổng là
10.200.0.0/16(đã khai trong--cluster-cidrcủa controller-manager ở Bài 8 vàclusterCIDRcủa kube-proxy ở Bài 12). Ta chia tay:worker-0lấy10.200.0.0/24,worker-1lấy10.200.1.0/24. Hiệnkubectl get nodes -o jsonpath='{..podCIDR}'còn rỗng vì chưa node nào được gán dải; Bài 14 sẽ gán. - Route giữa hai dải. Vì cả hai node ở cùng subnet, ta thêm route trong bảng định tuyến của VPC:
10.200.0.0/24 → worker-0,10.200.1.0/24 → worker-1. Khi đó gói pod-to-pod xuyên node được chính router của VPC chuyển đi, không cần đường hầm. - Tắt source/destination check. Mặc định AWS chặn một instance gửi/nhận gói có IP không phải của chính nó, đúng thứ ta cần vi phạm, vì gói pod mang IP pod chứ không phải IP node. Ở Bài 3 ta đã tắt kiểm tra này; xác nhận lại nó đang tắt:
aws ec2 describe-instances --filters Name=tag:Name,Values=worker-0,worker-1 \
--query 'Reservations[].Instances[].[Tags[?Key==`Name`]|[0].Value,SourceDestCheck]' --output text
worker-1 False
worker-0 False
False nghĩa là kiểm tra đã tắt, node được phép chuyển gói có IP lạ. Ba mảnh này (dải pod theo node, route trong VPC, tắt source/dest check) là toàn bộ cái khung cho native routing; Bài 14 lắp chúng lại.
CNI khớp vào đâu
Còn câu hỏi: ai thực sự tạo bridge, gắn pod vào, cấp IP cho pod? Không phải kubelet trực tiếp, mà một CNI plugin kubelet gọi qua trung gian. Nhớ lại chuỗi ủy thác từ Bài 10–11, giờ nối dài thêm một nấc:
kubelet ──CRI──► containerd ──► tạo network namespace cho pod
│
└──► gọi CNI plugin (binary trong /opt/cni/bin)
đọc config trong /etc/cni/net.d
ADD: cấp IP, tạo veth, gắn pod vào bridge
DEL: thu hồi khi pod chết
CNI là một giao ước đơn giản đến bất ngờ: một plugin chỉ là một file thực thi nhận lệnh ADD/DEL qua biến môi trường và stdin (JSON), rồi dựng hoặc gỡ mạng cho một network namespace. Khi containerd tạo pod, nó gọi plugin với ADD; plugin cấp một IP từ dải của node, tạo cặp veth nối namespace pod vào bridge trên node, và trả về IP đã cấp. Khi pod chết, DEL dọn sạch.
Ta đã có sẵn hai nửa từ Bài 10: binary plugin nằm trong /opt/cni/bin (gồm bridge và host-local mà ta sẽ dùng), nhưng config trong /etc/cni/net.d thì còn rỗng. Chính cái rỗng đó là lý do crictl info báo cni plugin not initialized, và là lý do kubelet giữ node ở NotReady: kubelet từ chối nhận pod khi chưa có cách cấp mạng cho chúng. Viết file config ấy, cùng việc thêm route VPC, là phần của bài sau.
Tổng kết
Mô hình mạng Kubernetes gói gọn trong một ý: mạng phẳng, IP-per-pod, không NAT giữa các pod. Mọi cơ chế phía trên dựa vào đó. Trong bốn kiểu giao tiếp, ba kiểu là việc của CNI và một kiểu (Service) đã xong với kube-proxy. Phần hóc búa, pod-to-pod xuyên node, có hai lối giải; cụm một-subnet của ta chọn native routing vì đơn giản và nhanh, với ba mảnh chuẩn bị đã nằm sẵn.
Giờ thì gõ lệnh được rồi, vì ta biết mỗi lệnh phục vụ điều gì. Bài 14 viết CNI config cho bridge + host-local trên mỗi worker, gán dải pod cho từng node, thêm route trong VPC, rồi tạo vài pod thật để xem chúng nhận IP, ping được nhau xuyên node, và hai node cuối cùng chuyển sang Ready.