Chạy hai container web, mỗi cái đều lắng nghe cổng 80 — và chúng không xung đột. Container có địa chỉ IP riêng, khác host. docker exec vào một container và bạn thấy eth0, bảng định tuyến, iptables của riêng nó. Làm sao có được điều đó khi tất cả chạy trên một máy, một card mạng vật lý? Câu trả lời là network namespace — kernel cho mỗi container một stack mạng hoàn toàn độc lập — cộng với veth, cặp "cáp ảo" nối container về host. Bài này (phần 4 loạt Docker) chạy thật để dựng lại cơ chế đó, kể cả tự tay tạo veth và ping qua namespace.

Mỗi container một stack mạng độc lập

Network namespace cô lập toàn bộ mạng: interface, địa chỉ IP, bảng định tuyến, bảng iptables, và cả không gian cổng. Nghĩa là cổng 80 trong container A và cổng 80 trong container B là hai thứ khác nhau, không đụng nhau. Đo thật một container:

docker run alpine ip addr    # eth0 inet 172.17.0.5/16 (IP của riêng nó)
                             # default via 172.17.0.1 (gateway = bridge docker0)

Container có eth0 với IP 172.17.0.5, và gateway 172.17.0.1 — chính là bridge docker0 của host.

Ảnh chụp đoạn mã nền tối minh hoạ network namespace vì sao mỗi container có địa chỉ IP của riêng nó, mỗi container một stack mạng độc lập không chia sẻ IP interface với host container có interface riêng eth0 IP riêng bảng định tuyến riêng bảng iptables riêng cổng riêng cổng 80 trong 2 container không đụng nhau docker run alpine ip addr ra eth0 172.17.0.x của riêng nó, network namespace mới chỉ có loopback tách biệt unshare --net sh tạo net namespace rỗng chỉ có lo còn DOWN không có eth0 tiến trình bị cắt hoàn toàn khỏi mạng cho tới khi được nối, veth sợi cáp ảo nối 2 namespace ip link add veth0 type veth peer name veth1 tạo cặp veth hai đầu như hai đầu một sợi cáp ip link set veth1 netns ns1 đẩy 1 đầu vào namespace container đầu kia ở host gói đi vào đầu này ra đầu kia đây chính là cách docker nối container với bridge docker0 của host, bức tranh đầy đủ container net ns veth bridge docker0 host NAT ngoài bridge như một switch ảo mọi container cắm veth vào đó chúng nói chuyện với nhau và ra Internet qua NAT

Hình 1: Mỗi container một stack mạng riêng (eth0/IP/route/cổng); unshare --net tạo namespace rỗng chỉ có loopback; veth là cặp cáp ảo nối container về bridge docker0 của host, ra Internet qua NAT.

Đo thật: từ namespace rỗng tới veth thông mạng

Ảnh chụp bảng kết quả chạy thật network namespace cộng veth output thật, một container có eth0 và IP riêng docker run alpine ra eth0 inet 172.17.0.5 trên 16 default via 172.17.0.1 dev eth0 gateway bằng bridge docker0, hai net namespace mới unshare --net chỉ có lo không eth0 host của container lo tunl0 eth0 at if1384 net namespace mới chỉ lo LOOPBACK không có eth0 bị cắt mạng, ba cặp veth nối 2 namespace như docker nối container host veth0 host side bằng 10.10.0.1 trên 24 veth1 trong ns1 bằng 10.10.0.2 trên 24 ping từ ns1 sang 10.10.0.1 64 bytes from 10.10.0.1 seq 0 time 0.041 ms 2 packets transmitted 2 received 0 phần trăm packet loss, kết container net ns veth bridge docker0 NAT Internet mỗi container một stack mạng riêng nối vào host qua veth

Hình 2: Chạy thật — container có eth0 172.17.0.5/16, gateway 172.17.0.1; net namespace mới (unshare --net) chỉ có lo (không eth0, bị cắt mạng); cặp veth 10.10.0.1↔10.10.0.2 nối hai namespace, ping 0% loss (0,041 ms).

  • Container có eth0/IP riêng: 172.17.0.5/16, gateway 172.17.0.1. Đây là mạng riêng của container, không dùng chung với host.
  • Namespace rỗng bị cắt mạng: unshare --net tạo một network namespace mới — nó chỉ có lo (loopback, còn DOWN), không có eth0. Một tiến trình trong namespace này hoàn toàn bị cắt khỏi mạng cho tới khi ai đó nối cho nó một interface.
  • veth nối hai namespace: ip link add veth0 type veth peer name veth1 tạo một cặp interface như hai đầu của một sợi cáp — gói vào đầu này ra đầu kia. Đẩy veth1 vào namespace ns1, đặt IP hai đầu (10.10.0.1 ở host, 10.10.0.2 trong ns1), rồi ping — 0% packet loss, 0,041 ms. Đây chính xác là cách Docker nối một container: một đầu veth ở trong network namespace của container (thành eth0), đầu kia cắm vào bridge docker0 của host.

Bức tranh đầy đủ của mạng Docker

Ghép lại: mỗi container có network namespace riêng, một đầu veth trở thành eth0 của container, đầu kia cắm vào bridge docker0 — một switch ảo trên host. Mọi container trên cùng bridge nói chuyện với nhau qua đó; và để ra Internet, gói đi qua bridge rồi được NAT (masquerade bằng iptables) sang IP của host. Đây là toàn bộ "phép màu" mạng bridge mặc định của Docker — chỉ là namespace + veth + bridge + NAT, tất cả là tính năng kernel Linux.

Đánh đổi cần cân nhắc

--network host bỏ cô lập mạng để lấy hiệu năng. Với --network host, container dùng chung network namespace của host — không veth, không NAT, không bridge. Nhanh hơn (bỏ một lớp), nhưng mất cô lập: container thấy mọi interface host và cổng của nó đụng trực tiếp cổng host. Chỉ dùng khi thực sự cần hiệu năng mạng tối đa hoặc truy cập mạng host, và hiểu rõ đánh đổi bảo mật.

NAT bridge mặc định che IP container khỏi bên ngoài. Vì container nằm sau NAT, bên ngoài không tự kết nối vào được — bạn phải -p (publish) để mở đường (chủ đề bài networking). Đây vừa là bảo vệ (container không lộ ra ngoài trừ khi bạn muốn) vừa là nguồn bối rối phổ biến ("sao gọi container từ máy khác không được?"). Hiểu NAT là hiểu vì sao cần publish cổng.

veth có chi phí; mạng dày đặc cần cân nhắc. Mỗi cặp veth và mỗi lần qua bridge/NAT tốn một chút CPU. Với hầu hết ứng dụng, chi phí này không đáng kể. Nhưng ở quy mô rất lớn (hàng nghìn container, throughput cực cao), người ta cân nhắc các chế độ mạng khác (macvlan, ipvlan, hoặc eBPF-based như Cilium) để giảm lớp. Bridge + veth là mặc định tốt, không phải lựa chọn duy nhất.

Ba ý mang về

  1. Mỗi container một stack mạng độc lập: đo thật, container có eth0 172.17.0.5/16 và gateway riêng — IP, route, cổng đều riêng, nên hai container cùng mở cổng 80 không đụng nhau.
  2. Network namespace mới bị cắt mạng cho tới khi được nối: đo thật unshare --net chỉ có lo, không eth0 — cô lập mạng hoàn toàn là điểm khởi đầu.
  3. veth là cặp cáp ảo nối container về host: đo thật tự dựng veth 10.10.0.1↔10.10.0.2 ping 0% loss — Docker dùng đúng cơ chế này để cắm container vào bridge docker0, ra Internet qua NAT.

Nguồn

Phần sau ta xem nửa còn lại của container: cgroups — cách giới hạn CPU và bộ nhớ thật, đo khi container vượt --memory bị OOM kill và khi vượt --cpus bị điều tiết (throttle).