Phần 1 đo sáu trục namespace và để lại một trục cho riêng phần này: mạng. Network namespace là trục mà lập trình viên backend chạm vào thường nhất, vì mọi câu hỏi "vì sao container này không nối được container kia", "vì sao cổng không vào được từ ngoài", "IP của service là gì" đều nằm ở đây. Và nó cũng là trục dễ bị coi là phép thuật nhất: docker run xong tự nhiên container có IP, có mạng, ping được nhau. Bài này tháo lớp phép thuật đó ra bằng cách đo từng thành phần, rồi dựng lại toàn bộ cơ chế bằng tay với năm lệnh ip để thấy Docker thật ra không làm gì khác.

Ngăn mạng riêng, cặp veth và cầu docker0

Ảnh chụp đoạn mã nền tối network namespace card mạng ảo veth và bridge docker0 với Docker 29 container tạm debian 12 cộng python 3.12 đo bằng ip sys class net ping docker port ip netns, container có một ngăn mạng hoàn toàn riêng network namespace cho container card mạng bảng định tuyến bảng iptables cổng riêng như máy riêng về mạng, Docker nối chúng bằng cặp veth hai đầu một sợi dây ảo một đầu là eth0 trong container đầu kia nằm trên host cắm vào cầu docker0 bridge mặc định subnet 172.17.0.0/16 gateway 172.17.0.1, cổng từ ngoài vào container cần port mapping -p host container iptables DNAT trên host, eth0@ifN trong container N là ifindex của đầu veth bên host đọc sys class net eth0 iflink để ghép cặp, cách đo 1 dựng net-a net-b debian ip addr xem IP riêng mỗi container, 2 docker network inspect bridge subnet và gateway của docker0, 3 veth sys class net eth0 ifindex trong container vs iflink đầu host, 4 ping net-a tới net-b bằng IP đo rtt cùng docker0 nên thấy nhau, 5 ip route trong container default qua gateway 172.17.0.1, 6 port mapping -p 49231 8000 curl từ host vào so với IP nội bộ 172.17.x, 7 dựng tay ip netns add cộng veth pair cộng addr chứng minh Docker làm đúng thứ này, vì sao dựng netns bằng tay Docker tạo mạng cho container nghe như phép thuật thật ra là 5 lệnh ip ai cũng chạy được ip netns add ip link add veth set veth vào netns gán IP hai đầu up y hệt cái container có dựng tay rồi ping xuyên veth đọc inode net namespace thấy không có gì bí ẩn chỉ là veth cộng namespace cộng bridge

Hình 1: Network namespace cho container card mạng riêng, cặp veth nối sang host cắm vào docker0, và bảy phép đo.

Network namespace cho mỗi container một ngăn mạng độc lập: card mạng riêng, bảng định tuyến riêng, bảng iptables riêng, và không gian cổng riêng. Về mặt mạng, container hành xử như một máy riêng, nó có thể nghe cổng 8000 mà không đụng cổng 8000 của host hay của container khác. Vấn đề là một ngăn mạng rỗng thì không nối được đi đâu, nên Docker nối nó ra ngoài bằng một cặp veth, viết tắt của virtual ethernet, là hai đầu của một sợi dây ảo: gói đi vào đầu này thì ra ở đầu kia. Một đầu đặt trong container và đặt tên là eth0; đầu kia nằm trên host và được cắm vào một cầu nối ảo tên docker0. Cầu docker0 là bridge mặc định với subnet 172.17.0.0/16 và gateway 172.17.0.1, đóng vai một switch ảo nối mọi container lại với nhau và ra ngoài.

Có một chi tiết cho phép ghép cặp veth một cách chính xác. Khi xem eth0 trong container, tên nó hiện là eth0@ifN, trong đó N là chỉ số interface của đầu veth bên host; đọc /sys/class/net/eth0/iflink cho ra đúng số N đó, và tìm interface có ifindex bằng N trên host là ra đầu dây bên kia. Cách đo của bài gồm bảy bước: dựng hai container net-a và net-b rồi xem IP riêng của mỗi cái; đọc subnet và gateway của docker0; ghép cặp veth qua ifindex và iflink; ping giữa hai container để đo độ trễ; đọc bảng định tuyến trong container; thử port mapping bằng -p 49231:8000 rồi curl từ host; và cuối cùng, dựng lại một network namespace bằng tay để chứng minh Docker chỉ chạy đúng năm lệnh ip mà ai cũng gõ được.

Đo thật: 172.17.0.6 và .7, ping 0,09 ms, và netns tự dựng

Ảnh chụp output thật nền tối hai container 172.17.0.6 chấm 7 ping 0,09 ms qua docker0 với container debian python trên bridge mặc định IP veth ping port mapping netns tự dựng, 1 tới 5 IP riêng docker0 veth ping route net-a eth0 172.17.0.6/16 net-b eth0 172.17.0.7/16 mỗi container một IP riêng, docker0 bridge subnet 172.17.0.0/16 gateway 172.17.0.1 cầu nối mọi container mặc định, veth net-a eth0@if798 ifindex trong container bằng 11 iflink đầu kia trên host bằng 798 eth0 là một đầu dây đầu kia là veth ifindex 798 cắm vào docker0, ping net-a tới net-b 172.17.0.7 3/3 gói 0% mất rtt 0,094 ms cùng bridge thấy nhau trực tiếp, route trong net-a default via 172.17.0.1 dev eth0 172.17.0.0/16 dev eth0 scope link, 6 port mapping từ ngoài vào phải khai -p docker run -p 49231 8000 python http.server trong container ở cổng 8000 docker port 8000/tcp map 0.0.0.0:49231 IP nội bộ container 172.17.0.8 curl host 127.0.0.1:49231 xin chao tu container 172.17 Docker dựng iptables DNAT host tới container không có -p thì cổng 8000 chỉ sống trong ngăn mạng container ngoài không với tới, 7 dựng network namespace bằng tay Docker làm đúng 5 lệnh này ip netns add ns1 tạo ngăn mạng rỗng ip link add veth-h type veth peer name veth-c cặp veth 2 đầu ip link set veth-c netns ns1 cắm 1 đầu vào ns1 gán 10.99.0.1 host và 10.99.0.2 ns1 up ns1 có veth-c@if13 10.99.0.2/24 host ping 10.99.0.2 rtt 0,040 ms ns1 net namespace inode 4026533781 khác net của container 4026533650 ngăn mạng riêng thật, nói thẳng mạng container không có gì huyền bí là network namespace ngăn riêng cộng cặp veth dây nối cộng docker0 cầu

Hình 2: Mỗi container một IP trên docker0, veth ghép cặp qua ifindex, ping giữa hai container, port mapping, và network namespace dựng bằng tay.

phép đo kết quả
IP eth0 của net-a / net-b 172.17.0.6 / 172.17.0.7 (riêng mỗi container)
docker0 subnet / gateway 172.17.0.0/16 / 172.17.0.1
veth net-a: ifindex trong / iflink host 11 / 798 (eth0@if798)
ping net-a → net-b 3/3 gói, 0% mất, 0,094 ms
route mặc định trong container via 172.17.0.1 dev eth0
port mapping -p 49231:8000, curl host trả về nội dung container
netns tự dựng: ns1 inode / container inode 4026533781 ≠ 4026533650

Mỗi container có IP riêng, và đó là IP nội bộ chứ không phải địa chỉ thật. net-a nhận 172.17.0.6, net-b nhận 172.17.0.7, cả hai thuộc subnet 172.17.0.0/16 của docker0. Đây là dải địa chỉ riêng tư, chỉ có ý nghĩa trong mạng nội bộ của Docker trên host này; từ bên ngoài host, không ai nối tới 172.17.0.6 được. Đó là lý do một service nói "IP của tôi là 172.17.0.6" chỉ đúng với các container cùng host, và là lý do hai container trên hai máy khác nhau không tự ping nhau bằng IP nội bộ. Bảng định tuyến trong container xác nhận cấu trúc: mọi gói không thuộc 172.17.0.0/16 đi qua gateway 172.17.0.1, tức qua docker0, để ra ngoài.

eth0 trong container là một đầu của cặp veth, kiểm chứng được bằng ifindex. Tên đầy đủ của interface là eth0@if798, và /sys/class/net/eth0/iflink trả về 798: đầu dây bên kia là interface có ifindex 798 trên host, một veth được cắm vào docker0. Hai con số này, ifindex 11 bên trong và iflink 798 chỉ ra bên ngoài, là hai đầu của cùng một sợi dây ảo; mọi gói eth0 gửi đi xuất hiện ở veth 798 trên host, và ngược lại. Đây là cơ chế vật lý thật sự nối ngăn mạng cô lập của container với phần còn lại của hệ thống, không phải một lớp trừu tượng mơ hồ mà là một cặp interface có số hiệu đọc được.

Hai container cùng docker0 ping nhau trực tiếp, 0,094 ms. net-a ping net-b được ngay, 3 trên 3 gói, 0% mất, độ trễ 0,094 ms. Chúng thấy nhau vì cùng cắm vào một cầu docker0, cầu này chuyển khung Ethernet giữa các veth như một switch; gói từ net-a đi ra veth của nó, qua docker0, vào veth của net-b, lên eth0 của net-b. Độ trễ 0,094 ms phản ánh đây hoàn toàn là chuyển mạch trong bộ nhớ kernel, không có phần cứng mạng hay dây thật nào; đó cũng là lý do giao tiếp giữa các container cùng host rất nhanh, và vì sao đặt các service hay nói chuyện với nhau trên cùng host, hoặc cùng một Docker network tùy biến, có lợi về độ trễ.

Từ ngoài vào container phải khai port mapping, và Docker dịch bằng iptables. Chạy một container với -p 49231:8000 và một server HTTP nghe cổng 8000 bên trong: docker port cho biết 8000/tcp -> 0.0.0.0:49231, nghĩa là cổng 49231 trên host được ánh xạ vào cổng 8000 của container, và curl tới 127.0.0.1:49231 trên host trả về đúng nội dung container phục vụ. Container đó có IP nội bộ 172.17.0.8, không với tới được từ ngoài host, nhưng qua port mapping thì cổng 49231 của host trở thành cửa vào. Cơ chế đằng sau là một luật iptables DNAT trên host, dịch địa chỉ đích của gói từ host:49231 thành 172.17.0.8:8000. Điểm mấu chốt về vận hành: không có -p, cổng mà container nghe chỉ sống trong ngăn mạng của nó, và mọi lỗi "service chạy trong container mà không nối được từ ngoài" gần như luôn là quên khai port mapping hoặc map nhầm cổng.

Dựng lại toàn bộ bằng năm lệnh, không có gì bí ẩn. Trong một container đặc quyền, năm lệnh ip tái tạo đúng cái Docker làm: ip netns add ns1 tạo một network namespace rỗng; ip link add veth-h type veth peer name veth-c tạo một cặp veth; ip link set veth-c netns ns1 cắm một đầu vào namespace mới; rồi gán địa chỉ 10.99.0.1 cho đầu ở host và 10.99.0.2 cho đầu trong ns1, và bật cả hai lên. Kết quả: ns1 có một interface veth-c@if13 với IP 10.99.0.2/24, và host ping được 10.99.0.2 với độ trễ 0,040 ms. Đọc inode network namespace của ns1 cho 4026533781, khác với 4026533650 của container đang chạy nó: một ngăn mạng riêng thật, dựng bằng tay, giống hệt về bản chất với cái mỗi container Docker có. Khi đã thấy năm lệnh này, "Docker tạo mạng cho container" không còn là phép thuật mà là một quy trình đơn giản: một namespace, một cặp dây, một cây cầu.

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

Bridge mặc định tiện cho một host, nhưng dùng network tùy biến cho ứng dụng thật. docker0 nối mọi container nhưng không cho phân giải tên: trên bridge mặc định, container chỉ gọi nhau bằng IP, mà IP lại thay đổi mỗi lần tạo lại. Tạo một Docker network riêng bằng docker network create bật DNS nội bộ, cho container gọi nhau bằng tên, và cô lập chúng khỏi các container khác; đây là mặc định đúng cho mọi ứng dụng nhiều container, và là cái mà Docker Compose tự làm. Bridge mặc định chỉ nên dùng cho thử nghiệm nhanh.

Mạng container nhanh trong một host, nhưng qua nhiều host là chuyện khác. Độ trễ 0,094 ms chỉ đúng khi hai container cùng host và cùng bridge; khi chúng ở hai máy, gói phải đi qua mạng thật, qua lớp overlay network của orchestrator như Kubernetes, với độ trễ và chi phí cao hơn nhiều. Thiết kế đặt service hay nói chuyện gần nhau, và đo độ trễ liên-service thật trên hạ tầng thật, không giả định con số nội-host.

Phạm vi đo. Các số ở đây là của bridge mặc định trên một host Docker Desktop; chế độ --network host bỏ hẳn network namespace và container dùng chung mạng host, nhanh hơn nhưng mất cô lập; chế độ none cho ngăn mạng rỗng. Chưa đo overlay network nhiều host, chưa đo IPv6, và chưa đo chi phí của chính lớp iptables DNAT dưới tải cao, nơi số luật lớn có thể thành nút thắt, chủ đề thuộc về mạng của orchestrator hơn là của một container đơn lẻ.

Ba ý mang về

  1. Mạng container là ba thứ ghép lại: network namespace, cặp veth, cầu docker0. Mỗi container một IP nội bộ trên 172.17.0.0/16, eth0 là một đầu veth nối sang host, docker0 là switch ảo nối tất cả; ping nội-host 0,094 ms vì hoàn toàn trong kernel.
  2. Từ ngoài vào phải khai -p, Docker dịch bằng iptables DNAT. IP nội bộ không với tới từ ngoài host; mọi lỗi "service không nối được từ ngoài" thường là quên port mapping.
  3. Dựng lại bằng năm lệnh ip chứng minh không có phép thuật. ip netns add cộng cặp veth cộng gán IP là đủ tái tạo mạng của một container; hiểu điều này giúp gỡ lỗi mạng container bằng công cụ Linux thường, không cần đoán.

Nguồn

Phần sau chuyển từ namespace, thứ cô lập cái container thấy, sang cgroups, thứ giới hạn cái container dùng. Bắt đầu với CPU: đặt --cpus=0.5 rồi cho một tiến trình ăn CPU hết sức, đo nó bị ghìm lại bao nhiêu qua cpu.stat và nr_throttled, và vì sao một app Java hay Go có thể chạy chậm bất ngờ trong container dù CPU host còn rảnh.