Phần 33 đo bốn driver mạng bằng iperf3 và trả lời câu hỏi "mạng nào truyền được nhiều byte/giây hơn". Bài này đổi ống kính: không hỏi thông lượng, mà hỏi một gói tin mất bao lâu để đi và quay về — độ trễ (latency), thứ quyết định cảm giác "nhanh" của một request đơn lẻ nhiều hơn là băng thông. Ba đường đi được so: bridge (hai container riêng, mạng tự định nghĩa), host (hai container --network host), và trong cùng một container (hai tiến trình gọi nhau qua 127.0.0.1).
Môi trường đo
Docker Engine 29.7.2 trên Docker Desktop (macOS), daemon chạy trong VM linuxkit — container nào cũng chạy alpine:3.20. Điểm này quan trọng cho mục sau về --network host, nên nhắc trước: trên Docker Desktop, "host" nghĩa là netns của VM, không phải netns của macOS thật. Trên Linux thật, --network host là netns của chính máy chủ — số đo dưới đây có thể lệch đi nếu chạy trên Linux, nhưng đường đi (không veth, không bridge ảo) thì giống hệt.
$ docker version --format '{{.Server.Version}}'
29.7.2
Ba đường đi, dựng bằng ba cách nối container
- bridge: hai container
lat-clientvàlat-servertrên một mạng tự định nghĩa (docker network create), mỗi bên một network namespace riêng, nối nhau qua cặpveth+ bridge ảo. - host: hai container
--network host. Cả hai không có netns riêng — chúng dùng chung đúng một netns với nhau (và với chính VM), nên "container A gọi container B" thực chất là gọi127.0.0.1trong một netns dùng chung, chẳng khác gì hai tiến trình bất kỳ trên cùng máy. - cùng container: một container
alpinechạy cảhttpd(bind cổng 8082) lẫncurl, gọi nhau qua127.0.0.1trong netns của riêng nó — không container thứ hai, không mạng ảo nào.
Bridge: hai lượt qua veth và bridge ảo
$ docker network create latnet
$ docker run -d --name lat-server --network latnet alpine:3.20 sleep 3600
$ docker run -d --name lat-client --network latnet alpine:3.20 sleep 3600
$ docker exec lat-client ping -c 100 -i 0.2 -q 172.19.0.2
100 packets transmitted, 100 received, 0% packet loss, time 20745ms
rtt min/avg/max/mdev = 0.024/0.110/0.224/0.040 ms
Gói tin rời eth0 của client, qua veth của nó, tới bridge ảo latnet, qua veth của server, vào eth0 của server — rồi ngược lại y hệt để về. Bốn chặng veth/bridge cho một round-trip.
Host: chung một netns, không phải một đường ống riêng
$ docker run -d --name lat-host2 --network host alpine:3.20 sleep 3600
$ docker exec lat-host2 ping -c 100 -i 0.2 -q 127.0.0.1
100 packets transmitted, 100 packets received, 0% packet loss
round-trip min/avg/max = 0.024/0.083/0.198 ms
Không có veth, không có bridge ảo — gói tin (nếu còn gọi là "gói tin" hợp lý ở đây) đi thẳng qua loopback của netns dùng chung. Về bản chất mạng, đây là cùng một lớp thao tác với kịch bản "cùng container", chỉ khác ở chỗ có hai tiến trình httpd/PID namespace tách biệt thay vì một.
Cùng container: loopback không đi đâu cả
$ docker exec lat-solo ping -c 100 -i 0.2 -q 127.0.0.1
100 packets transmitted, 100 received, 0% packet loss, time 20747ms
rtt min/avg/max/mdev = 0.013/0.058/0.146/0.021 ms
0,058 ms trung bình — thấp nhất trong ba kịch bản, và thấp nhất một cách nhất quán ở mọi lần đo lại bên dưới. Không có veth, không có bridge ảo, không có netns thứ hai nào để băng qua.
Đo lại ba lần: bridge và host đổi chỗ nhau, không giải thích được
Theo đúng quy tắc của sê-ri này — không đoán khi không có bằng chứng — đây là ba lần chạy lại ping -c 100 liên tiếp, cùng lệnh, cách nhau vài phút:
| Lần đo | bridge (avg) | host (avg) | cùng container (avg) |
|---|---|---|---|
| 1 (n=50, interval mặc định) | 0,096 ms | 0,081 ms | 0,053 ms |
| 2 (n=100, interval 0,2s) | 0,097 ms | 0,119 ms | 0,056 ms |
| 3 (n=100, interval 0,2s) | 0,110 ms | 0,083 ms | 0,058 ms |
bridge thắng host ở lần 1 và 3, thua ở lần 2. Chênh lệch giữa hai cột đó (0,02–0,04 ms) nhỏ hơn cả độ lệch chuẩn (mdev) đo được trong từng lần — tức là nằm trong vùng nhiễu của môi trường đo (VM, scheduler, tải nền của macOS), không phải một quy luật của bản thân Docker. Kết luận trung thực: ở tầng ICMP, không đủ căn cứ để nói bridge hay host nhanh hơn nhau. Chỉ có một điều lặp lại ổn định ở cả ba lần: cùng container luôn thấp hơn hẳn cả hai, chênh khoảng 40–50%.
Ở tầng TCP + HTTP, thứ tự ổn định trở lại — nhưng chênh lệch co gần hết
ping đo ICMP thuần, còn ứng dụng thật gọi nhau qua TCP rồi HTTP. Dựng httpd (busybox) ở mỗi kịch bản, gọi curl -w '%{time_connect} %{time_total}' 60 lần mỗi bên:
$ docker exec lat-client sh -c \
'for i in $(seq 60); do curl -s -o /dev/null -w "%{time_connect} %{time_total}\n" http://172.19.0.2/; done'
| Kịch bản | time_connect (bắt tay TCP) |
time_total (cả request) |
|---|---|---|
| bridge | 0,034 ms | 0,149 ms |
| host | 0,029 ms | 0,142 ms |
| cùng container | 0,027 ms | 0,138 ms |
Lần này thứ tự giữ nguyên và lặp lại qua nhiều lần chạy: cùng container < host < bridge, cả ở bắt tay TCP lẫn tổng thời gian request. Nhưng biên độ chỉ còn 7–20 µs (micro giây) — nhỏ hơn 1% của chính con số time_total, và nhỏ hơn hàng trăm tới hàng nghìn lần so với độ trễ một truy vấn database hay một lần gọi API bên ngoài thường gặp trong ứng dụng thật.
Vậy chọn đường mạng nào?
- Đừng chọn network mode vì vài chục micro giây ping. Ở quy mô một request ứng dụng thật (thường tính bằng mili giây, không phải micro giây), chênh lệch này biến mất trong nhiễu.
--network hostđáng cân nhắc vì lý do khác: bỏ NAT port-mapping và một lớp bridge, có ích rõ nhất khi cần thông lượng cao hoặc container cần thấy IP nguồn thật của client — đúng góc đo của Phần 33, không phải góc độ trễ của bài này.- Đừng nhét nhiều tiến trình vào một container chỉ để ăn phần loopback nhanh nhất. Số đo ở đây xác nhận nó nhanh hơn thật, nhưng đổi lại là mất khả năng scale từng tiến trình độc lập, mất cô lập lỗi — một sự đánh đổi không đáng cho vài chục micro giây.
bridgevẫn là lựa chọn mặc định hợp lý, đúng kết luận của Phần 33: cô lập tốt, độ trễ thêm vào là có thật nhưng nhỏ tới mức không quyết định trải nghiệm người dùng cuối.
Bài viết liên quan
- host nhanh hơn bridge 27%, overlay chậm nhất và retransmit gấp 48 lần: đo bốn loại mạng Docker bằng iperf3
- DNS nội bộ Docker: container tìm nhau bằng tên thế nào, và cái bẫy IP cũ sau khi tái tạo
- 0.0.0.0 bị Docker Desktop khoá về loopback, nhưng trên Linux thật thì mở toang: đo publish, expose và bind địa chỉ
Thử ba mươi giây
Dán thẳng vào terminal — dựng một cặp container bridge và đo ping 20 gói, không cần dọn dẹp gì thêm ngoài docker rm -f:
docker network create latnet-test
docker run -d --name srv --network latnet-test alpine:3.20 sleep 300
docker run --rm --network latnet-test alpine:3.20 \
ping -c 20 -q $(docker inspect -f '{{.NetworkSettings.Networks.latnet-test.IPAddress}}' srv)
docker rm -f srv >/dev/null; docker network rm latnet-test >/dev/null