Thợ điện tìm một ổ cắm chết không đoán bừa: họ đi lần theo đường dây từ tủ điện ra ngoài — cầu dao, rồi dây, rồi ổ cắm, rồi cái thiết bị — và mỗi điểm dừng cho biết lỗi nằm phía trước hay phía sau nó. Gỡ lỗi mạng container y hệt: cùng một triệu chứng "không kết nối được" có thể do sai mạng, do ứng dụng nghe nhầm, do tiến trình chưa chạy — và cách duy nhất để tách chúng là đi lần theo đúng đường gói tin, từng bậc một. Tôi dựng bốn container: một cái nằm sai mạng, một cái nghe nhầm 127.0.0.1, một cái không chạy ứng dụng, một cái bình thường. Rồi gọi cả bốn từ cùng một chỗ:
d-api1 ma=000
d-api2 ma=000
d-api3 ma=000
d-api4 ma=200
Ba nguyên nhân hoàn toàn khác nhau, một triệu chứng duy nhất. Bài này là cái thang để tách chúng ra.
Bước 0: đọc mã lỗi của curl
Mẹo rẻ nhất, và rất hay bị bỏ qua vì -s nuốt mất thông báo. Dùng -sS:
| Thông báo | |
|---|---|
| d-api1 | curl: (6) Could not resolve host: d-api1 |
| d-api2 | curl: (7) Failed to connect to d-api2:8000 |
| d-api3 | curl: (7) Failed to connect to d-api3:8000 |
Mã 6 và mã 7 là hai thế giới khác nhau. Mã 6 là vấn đề tên; mã 7 là vấn đề kết nối. Chỉ một chữ số đó đã cắt đôi không gian tìm kiếm.
Bước 1: tên có phân giải được không
docker exec <nguon> getent hosts <dich>
| Kết quả | |
|---|---|
| d-api1 | rỗng |
| d-api2 | 192.168.0.2 |
| d-api3 | 192.168.0.3 |
| d-api4 | 192.168.0.4 |
d-api1 xong ngay tại đây: nó nằm ở mạng khác nên DNS nhúng không biết nó (phần 38). Kiểm tra bằng:
docker inspect <dich> --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'
Hai container phải có ít nhất một mạng chung. Nếu bạn đang dùng Compose, hai service ở hai project khác nhau thì mặc định không chung mạng.
Bước 2: gói tin có tới nơi không
docker exec <nguon> ping -c1 -W2 <dich>
Cả d-api2, d-api3, d-api4 đều tới được. Nghĩa là định tuyến ổn, không có tường lửa nào chặn, container đích đang sống. Vấn đề nằm bên trong nó.
(Nếu ping không tới trong khi tên phân giải được, hãy nghi enable_icc=false hoặc một luật iptables — cả hai đều cho đúng triệu chứng "tên ra IP nhưng gói tin biến mất".)
Bước 3: bên trong, cái gì đang nghe
Đây là bước tách được hai sự cố còn lại:
docker exec <dich> ss -ltn
| Đang nghe | |
|---|---|
| d-api2 | 127.0.0.1:8000 |
| d-api3 | (không có gì trên 8000) |
| d-api4 | 0.0.0.0:8000 |
d-api2nghe127.0.0.1— lỗi phổ biến nhất trong tất cả. Ứng dụng chạy hoàn hảo, log đẹp, test trong container bằngcurl localhost:8000cũng qua, mà bên ngoài không ai vào được. Sửa bằng cách cho nó nghe0.0.0.0.d-api3không nghe gì cả — tiến trình chết hoặc chưa khởi động xong. Xemdocker logs.
Cả hai đều không phải vấn đề mạng, dù triệu chứng ban đầu trông hệt như vậy.
Bước 4: container không có shell thì làm sao
Image distroless hoặc FROM scratch không có sh, không có ss, không có gì:
$ docker exec d-tron sh -c 'echo ok'
OCI runtime exec failed: exec failed: unable to start container process...
Cách vào: chạy một container khác dùng chung namespace mạng với nó.
docker run --rm --network container:d-tron nicolaka/netshoot ss -ltnp
State Recv-Q Send-Q Local Address:Port
LISTEN 0 5 127.0.0.1:8000
LISTEN 0 4096 127.0.0.11:45659
Bắt được thủ phạm ngay: dịch vụ nghe 127.0.0.1. Container đích không cần có bất cứ công cụ nào — --network container: cho container phụ chính namespace mạng đó, nên nó thấy đúng những gì container kia thấy. Cùng cách này dùng được với ip, tcpdump, dig, curl.
Dòng 127.0.0.11 là DNS nhúng của Docker, nó luôn có mặt trên mạng tự tạo (phần 34).
Bước 5: nhìn tận gói tin
Khi bốn bước trên chưa đủ, bắt gói ngay trong namespace của bên gọi:
docker run --rm --network container:<nguon> nicolaka/netshoot \
tcpdump -n -i any tcp port 8000
Hai lời gọi, một hỏng một chạy, cạnh nhau:
06:58:45.654306 Out IP 192.168.0.5.57470 > 192.168.0.2.8000: Flags [S]
06:58:45.654380 In IP 192.168.0.2.8000 > 192.168.0.5.57470: Flags [R.] <- RST sau 74 us
06:58:46.668834 Out IP 192.168.0.5.36206 > 192.168.0.4.8000: Flags [S]
06:58:46.668957 In IP 192.168.0.4.8000 > 192.168.0.5.36206: Flags [S.] <- bat tay binh thuong
Đây là chỗ phân biệt quan trọng nhất ở tầng gói tin:
| Thấy gì | Nghĩa là |
|---|---|
| RST quay về ngay | Gói tin đã tới. Máy đích có đó, cổng không mở. |
| Im lặng hoàn toàn | Gói tin không tới nơi. Định tuyến, tường lửa, sai mạng. |
| SYN lặp lại nhiều lần | Không ai trả lời — thường là tường lửa DROP. |
| Bắt tay xong rồi im | Nghi MTU (phần 39). |
RST về sau 74 micro giây như trên là bằng chứng chắc chắn rằng đây không phải vấn đề mạng — không có đường truyền nào trả lời nhanh thế nếu gói tin phải đi đâu xa.
Bảng tra nhanh
| Triệu chứng | Nguyên nhân hay gặp | Kiểm bằng |
|---|---|---|
curl: (6) không phân giải tên |
Khác mạng, hoặc đang ở bridge mặc định | getent hosts, docker inspect |
curl: (7), ping tới được, RST ngay |
Ứng dụng nghe 127.0.0.1, hoặc chưa chạy |
ss -ltn trong container đích |
curl: (7), ping không tới |
enable_icc=false, luật iptables, sai mạng |
ping, iptables -L |
| Kết nối được, treo với dữ liệu lớn | MTU | ping -M do -s 1472 |
| Chạy được vài phút rồi hỏng | DNS cache giữ IP cũ sau deploy | so IP ứng dụng dùng với docker inspect |
| Từ máy chủ vào được, từ máy khác thì không | Bind 127.0.0.1: khi publish |
docker ps --format '{{.Ports}}' |
Muốn chạy nhanh ba bước đầu mỗi lần nghi ngờ, dán một hàm vào ~/.zshrc:
kiemtra() { # kiemtra <container-nguon> <ten-dich> <cong>
docker exec "$1" sh -c "
echo '--- ten:' ; getent hosts $2 || echo 'KHONG phan giai duoc'
echo '--- ping:' ; ping -c1 -W2 $2 >/dev/null 2>&1 && echo 'toi duoc' || echo 'khong toi'
echo '--- goi:' ; curl -sS --max-time 3 -o /dev/null -w '%{http_code}\n' http://$2:$3/
"
echo '--- ben trong dich dang nghe gi:'
docker run --rm --network container:$2 nicolaka/netshoot ss -ltn 2>/dev/null
}
Ba bước đó giải quyết gần hết. Chỉ khi cả ba đều bình thường mà vẫn hỏng thì mới cần tới tcpdump.
Mẫu số chung
Bài học đầu tiên, chính là cái thang: một triệu chứng che nhiều nguyên nhân thì đừng đoán — chạy một chuỗi phép thử mà mỗi bước cắt đôi không gian nguyên nhân, và định vị trước khi sửa. Mã 000 giống hệt nhau ở ba container, nhưng curl mã 6 hay 7 đã cắt đôi (tên hay kết nối), ping cắt tiếp (định tuyến hay ứng dụng), ss cắt nốt (nghe nhầm hay chưa chạy) — ba bậc, ba nhát, ra thủ phạm. Cùng phương pháp "nhị phân hoá để định vị" ở khắp nơi: chẩn đoán phân biệt của bác sĩ, git bisect tìm commit hỏng, đi lần theo các tầng OSI, thu hẹp một test đang đỏ. Nguyên tắc: đọc tín hiệu rẻ nhất mà phân biệt nhiều nhất trước (mã thoát, không chỉ "nó hỏng"), rồi mỗi phép thử tiếp theo loại bỏ cả một nhóm khả năng — chứ đừng thử sửa từng thứ một cách ngẫu nhiên.
Điều thứ hai, đọc từ "ping tới được mà vẫn 000": cái nhãn mà triệu chứng gợi ra thường sai — "không kết nối được" gần như không bao giờ là lỗi mạng, mà là ứng dụng nghe 127.0.0.1 hoặc chưa chạy. Đừng để chữ "network error" kéo bạn đi soi tường lửa với định tuyến; hãy kiểm đúng cái tầng mà lỗi thật sự hay tụ về. Cùng cái bẫy "tin cái nhãn của triệu chứng" ở khắp nơi: "chậm" bị đổ cho CSDL trong khi thủ phạm là N+1 ở ứng dụng, lỗi "DNS" hoá ra là cấu hình, đổ cho mạng cái vốn là một bind sai địa chỉ. Và một mẹo kèm theo: khi không chọc thẳng vào được cái cần soi (container không shell), hãy mượn một điểm quan sát nhìn thấy đúng thứ nó thấy (--network container: chia chung namespace) — nguyên tắc chung là khi không đo trực tiếp được, tìm một vị trí có cùng góc nhìn với đối tượng, đừng bỏ cuộc vì thiếu công cụ tại chỗ.
Phần sau chuyển hẳn sang lưu trữ: volume, bind mount và tmpfs khác nhau ở đâu, và tốc độ ghi của từng loại.