Phần 32 dựng rootless Docker và đo chênh lệch tài nguyên với rootful. Phần này rẽ sang một trục khác hẳn: mạng. Docker cho container chọn một trong bốn driver mạng — bridge, host, none, overlay — và chọn sai không chỉ là vấn đề lý thuyết cô lập, nó đổi cả throughput lẫn latency thật. Bài này đo cả bốn bằng iperf3 và ping, trên đúng một máy, cùng image alpine:3.20.
Môi trường đo, và vì sao số Gbit/s ở đây không dùng để so với máy bạn
Máy đo là Docker Desktop 4.87.0 trên macOS, tức là daemon chạy bên trong một VM Linux (linuxkit, 10 vCPU cấp cho VM):
$ docker run --rm alpine:3.20 uname -a
Linux 733ce7d57294 7.0.12-linuxkit #1 SMP PREEMPT ... aarch64 Linux
Mọi phép đo bridge/host/none/overlay dưới đây đều diễn ra bên trong VM đó, không đụng tới NIC vật lý nào — đó là lý do throughput ra tới 100+ Gbit/s, nhanh hơn bất kỳ card mạng thật nào. Con số tuyệt đối vì vậy không dùng để so với máy chủ Linux thật của bạn. Thứ đáng tin là tỷ lệ tương đối giữa bốn loại, vì cả bốn cùng chạy trên một phần cứng, một lúc, cùng cách đo.
bridge — driver mặc định, qua veth và NAT
Không truyền --network là container rơi vào mạng bridge mặc định. Dựng server rồi lấy IP:
$ docker run -d --name iperf-server --network bridge alpine:3.20 \
sh -c "apk add -q iperf3 && iperf3 -s"
$ docker inspect -f '{{.NetworkSettings.Networks.bridge.IPAddress}}' iperf-server
172.17.0.3
(Docker mới không còn điền NetworkSettings.IPAddress ở gốc nữa — phải đọc qua NetworkSettings.Networks.<tên mạng>.IPAddress. Đường cũ báo lỗi "map has no entry".)
Throughput:
$ docker run --rm --network bridge alpine:3.20 sh -c \
"apk add -q iperf3 && iperf3 -c 172.17.0.3 -t 5"
...
[ 5] 0.00-5.01 sec 80.0 GBytes 137 Gbits/sec 5 sender
[ 5] 0.00-5.01 sec 80.0 GBytes 137 Gbits/sec receiver
Latency:
$ docker run --rm --network bridge alpine:3.20 sh -c \
"apk add -q iputils && ping -c 20 172.17.0.3" | tail -2
rtt min/avg/max/mdev = 0.040/0.080/0.096/0.010 ms
137 Gbit/s, 0,080 ms trung bình, 5 lần phải truyền lại (Retr) trên 80 GB. Đường đi: gói tin ra khỏi container qua veth, tới docker0 (một bridge Linux thật), qua iptables NAT nếu cần ra ngoài, rồi vào veth của container đích. Nhiều chặng hơn host, nhưng vẫn là chuyển mạch trong kernel, không đụng driver vật lý — đắt hơn host một chút chứ không đắt kiểu overlay.
host — không có network namespace riêng
$ docker run -d --name iperf-server-host --network host alpine:3.20 \
sh -c "apk add -q iperf3 && iperf3 -s"
$ docker run --rm --network host alpine:3.20 sh -c \
"apk add -q iperf3 && iperf3 -c 127.0.0.1 -t 5"
...
[ 5] 0.00-5.01 sec 101 GBytes 174 Gbits/sec 1 sender
$ docker run --rm --network host alpine:3.20 sh -c \
"apk add -q iputils && ping -c 20 127.0.0.1" | tail -2
rtt min/avg/max/mdev = 0.020/0.061/0.100/0.025 ms
174 Gbit/s, 0,061 ms, chỉ 1 lần truyền lại. Cao hơn bridge đúng 27% về throughput, thấp hơn về latency — hợp lý, vì --network host bỏ hẳn veth và docker0, container dùng thẳng interface, bảng định tuyến và cổng của namespace mạng bên ngoài nó.
Chỗ cần nói rõ trên macOS: namespace "bên ngoài" đó là namespace của VM linuxkit, không phải của macOS. Trên Linux thật, --network host cho container thấy đúng NIC vật lý của máy chủ. Trên Docker Desktop, nó chỉ đưa container ra ngang hàng với daemon — vẫn cách macOS một lớp VM. Nếu bạn đang dùng bản Docker Desktop cũ hơn đáng kể so với 4.87.0 đang test ở đây, kiểm tra lại xem --network host có thật sự hoạt động không trước khi tin bài này — hỗ trợ cho macOS/Windows còn khá mới so với Linux, nơi tính năng này có từ đầu.
none — không chỉ "chậm", mà không route đi đâu cả
Thử điều hiển nhiên trước: cài iperf3 vào container --network none.
$ docker run --rm --network none alpine:3.20 apk add -q iperf3
ERROR: unable to select packages:
iperf3 (no such package):
required by: world[iperf3]
apk add tự nó cần mạng để tải chỉ mục gói — mà --network none không có route nào ra ngoài, nên ngay bước cài công cụ đo đã thất bại. Đây là bẫy thực dụng đầu tiên: không build sẵn image có công cụ, bạn không đo nổi gì bên trong container none. Build trước:
$ docker build -t iperf3-baked - <<'EOF'
FROM alpine:3.20
RUN apk add -q --no-cache iperf3 iputils
EOF
Xem container có gì:
$ docker run --rm --network none alpine:3.20 ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 ...
inet 127.0.0.1/8 scope host lo
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN ...
...
Chỉ có lo ở trạng thái UP; mấy interface tunnel còn lại (tunl0, gre0...) là module kernel mặc định của Linux, đều DOWN, không phải Docker tạo ra. Không có eth0, không có route mặc định:
$ docker run --rm --network none alpine:3.20 wget -T 2 -O /dev/null http://172.17.0.1
wget: can't connect to remote host (172.17.0.1): Network unreachable
Vậy "throughput của none" là câu hỏi sai — không có cách nào để hai container --network none nói chuyện với nhau, kể cả khi chúng chạy trên cùng một máy. Thứ duy nhất đo được là tốc độ tự vòng lặp qua lo, server và client trong cùng một container:
$ docker run --rm --network none iperf3-baked sh -c \
"iperf3 -s -D && sleep 1 && iperf3 -c 127.0.0.1 -t 5"
...
[ 5] 0.00-5.01 sec 110 GBytes 189 Gbits/sec 0 sender
189 Gbit/s, 0 lần truyền lại — nhanh nhất trong bốn loại, vì nó không đi qua bất kỳ interface ảo nào, chỉ là kernel copy dữ liệu qua lo trong chính namespace đó. Con số này không so được với ba loại kia: nó đo một thứ khác hẳn (một tiến trình tự nói chuyện với chính nó), không phải throughput giữa hai bên. Dùng none khi container không cần mạng chút nào — job xử lý file cục bộ, worker đọc từ volume — không phải khi bạn cần "mạng nhanh nhất".
overlay — driver duy nhất đòi Swarm, và cái giá của VXLAN
overlay là driver duy nhất trong bốn loại không tạo được bằng docker network create trần — nó cần Swarm, kể cả khi chỉ chạy trên một node:
$ docker swarm init
Swarm initialized: current node (kagvi...) is now a manager.
$ docker network create -d overlay --attachable iperf-overlay
$ docker run -d --name iperf-server-overlay --network iperf-overlay iperf3-baked iperf3 -s
$ docker inspect -f '{{(index .NetworkSettings.Networks "iperf-overlay").IPAddress}}' \
iperf-server-overlay
10.0.1.2
$ docker run --rm --network iperf-overlay iperf3-baked iperf3 -c 10.0.1.2 -t 5
...
[ 5] 0.00-5.01 sec 69.9 GBytes 120 Gbits/sec 241 sender
$ docker run --rm --network iperf-overlay iperf3-baked ping -c 20 10.0.1.2 | tail -2
rtt min/avg/max/mdev = 0.046/0.117/0.187/0.034 ms
120 Gbit/s — thấp nhất trong ba loại có thể so sánh — và 241 lần truyền lại, gấp 48 lần bridge (5 lần) và 241 lần host (1 lần), trên cùng lượng dữ liệu 5 giây. Latency trung bình 0,117 ms, gần gấp đôi bridge. Nguyên nhân nằm ở MTU:
$ docker run --rm --network bridge alpine:3.20 sh -c "ip link show eth0 | grep mtu"
mtu 65535
$ docker run --rm --network iperf-overlay alpine:3.20 sh -c "ip link show eth0 | grep mtu"
mtu 1450
overlay đóng gói mỗi gói tin bằng VXLAN (UDP cổng 4789) trước khi gửi qua mạng underlay, và phần header thêm vào (Ethernet ngoài + IP + UDP + VXLAN, cộng khoảng 50 byte) buộc Docker hạ MTU bên trong container từ 1500 xuống 1450. MTU nhỏ hơn nghĩa là cùng một lượng dữ liệu phải cắt thành nhiều gói hơn, và mỗi gói còn tốn thêm một vòng encap/decap ở cả hai đầu — hai lý do cộng lại giải thích cả throughput thấp hơn lẫn số lần truyền lại cao hơn hẳn.
Một điểm cần nói thẳng: bài đo này chạy overlay trên một node duy nhất (Swarm một-manager). Trong triển khai thật, hai container overlay thường nằm trên hai máy vật lý khác nhau, và gói tin VXLAN phải đi qua mạng underlay thật — thêm một chặng latency mà phép đo này chưa hề chạm tới. Số 0,117 ms ở đây là cận dưới của overlay đa node, không phải con số đại diện.
Tổng kết: chọn driver nào
| Driver | Cần gì | Cô lập | Throughput đo được | Latency đo được | Retr |
|---|---|---|---|---|---|
bridge |
mặc định, không cần cấu hình | container ⇄ container qua NAT | 137 Gbit/s | 0,080 ms | 5 |
host |
--network host |
không cô lập, chung namespace | 174 Gbit/s | 0,061 ms | 1 |
none |
--network none |
tuyệt đối, kể cả loopback ra ngoài | 189 Gbit/s (tự vòng lặp, không so được) | — | — |
overlay |
Swarm, docker network create -d overlay |
container ⇄ container qua VXLAN, xuyên node | 120 Gbit/s | 0,117 ms | 241 |
Thực dụng: bridge là lựa chọn mặc định đúng cho phần lớn ứng dụng một máy. Đổi sang host khi bạn cần latency thấp nhất có thể và chấp nhận mất cô lập cổng — ví dụ một proxy hiệu năng cao đứng một mình trên máy. Dùng overlay khi container thật sự cần nói chuyện xuyên nhiều node (Swarm services); đừng dùng nó trên một node chỉ vì "nghe có vẻ mạnh hơn" — bạn trả giá MTU thấp hơn và CPU encap/decap mà không được lợi gì. none dành cho container không cần mạng chút nào, không phải một tuỳ chọn hiệu năng.
Thử ba mươi giây
Kiểm tra MTU thật của container đang chạy trên máy bạn — chỉ số này thay đổi tuỳ driver và tự nó đã tiết lộ container đang dùng loại mạng nào:
docker inspect <container> -f '{{.HostConfig.NetworkMode}}' && \
docker exec <container> sh -c "ip link show eth0 2>/dev/null | grep mtu || echo 'không có eth0'"
MTU 1450 là overlay, khác 1500 (thường gặp trên interface vật lý thật) là dấu hiệu có tầng đóng gói nào đó đang ăn bớt băng thông hữu dụng của bạn.
Bài viết liên quan
- Rootless Docker: cấp --memory=64m rồi cấp phát 200MB vẫn chạy trót lọt — dựng và đo chênh lệch với rootful
- cap-drop=ALL vẫn ping được, cap-add lại vô dụng: đo capabilities và seccomp bằng docker run thật
- Bật
--read-onlylên bốn image quen thuộc: một cái chạy ngon, ba cái chết — và đường vá chính xác