Phần 33 đo được mạng overlay có MTU 1450 thay vì 1500 và nói rằng con số đó gây ra "kiểu lỗi khó chẩn đoán nhất". Bài này dựng lại đúng lỗi đó để xem nó trông như thế nào.

Kết quả cuối cùng, trên một đường mạng mà mọi giao diện đều khai MTU 1500:

Kích thước response Kết quả
1000 byte 200, đủ 1000 byte, 0,1 s
1300 byte 200, đủ 1300 byte, 0,1 s
1340 byte 200, đủ 1340 byte, 0,1 s
1350 byte 200, nhận 0 byte, treo tới hết giờ
2000 byte 200, nhận 0 byte, treo 10,1 s
100.000 byte 200, nhận 0 byte, treo 10,1 s

Chú ý cột kết quả: mã 200 đã về. Bắt tay TCP xong, HTTP header xong, rồi im lặng. Với ứng dụng, đây là "server trả lời rồi mà response rỗng" — nghe như lỗi ứng dụng, không giống lỗi mạng chút nào.

Dựng lại

Ba container: client, router, server. Router bật ip_forward, mỗi bên một mạng.

Bước 1 — hạ MTU chân client của router xuống 1400, giữ client và server ở 1500:

Kích thước Kết quả
500 byte
100.000 byte

Vẫn chạy tốt. Path MTU Discovery làm đúng việc của nó: router gửi ICMP "fragmentation needed" về, server học được và thu nhỏ gói. Bằng chứng nằm trong bảng định tuyến của server:

10.10.1.10 via 10.10.2.2 dev eth0 ... cache expires 599sec mtu 1400

Vậy MTU lệch một mình không gây hoạ. Đó là điều tôi phải đo mới tin, vì rất nhiều bài viết nói ngược lại.

Bước 2 — chặn đúng gói ICMP đó trên router:

iptables -I OUTPUT -p icmp --icmp-type fragmentation-needed -j DROP

Và dựng lại server để nó chưa học PMTU. Đó là lúc bảng ở đầu bài xuất hiện.

Chỉ khi hai điều kiện cùng xảy ra — MTU trên đường nhỏ hơn MTU của giao diện, ICMP bị chặn — mới thành hố đen. Ngoài đời cả hai đều rất phổ biến: VPN và mạng overlay tạo ra điều thứ nhất, còn "chặn hết ICMP cho an toàn" là một cấu hình tường lửa kinh điển.

Ngưỡng nằm ở đâu

1340 chạy, 1350 treo. Cộng ngược lại: MTU đường đi 1400, trừ 20 byte header IP và 20 byte header TCP còn 1360 byte dữ liệu mỗi gói; response HTTP còn có mấy chục byte header nữa, nên phần thân vượt qua khoảng 1340 là gói đầu tiên đã chạm trần.

Điều này giải thích một triệu chứng rất đặc trưng: API trả JSON nhỏ thì chạy, JSON lớn thì treo. Cùng một endpoint, cùng một mã, chỉ khác dữ liệu.

Nó còn làm chết cả server

Server trong lần đo đầu của tôi là TCPServer đơn luồng. Sau vài request treo:

tcp  84  0  10.10.2.10:8000  10.10.1.10:51698  CLOSE_WAIT
tcp  84  0  10.10.2.10:8000  10.10.1.10:55728  CLOSE_WAIT
tcp  83  0  10.10.2.10:8000  10.10.1.10:47146  CLOSE_WAIT
tcp  84  0  10.10.2.10:8000  10.10.1.10:49890  CLOSE_WAIT

Sau đó mọi request đều hỏng, kể cả request 100 byte vốn chạy tốt trước đó. Tiến trình vẫn running, cổng vẫn mở, nhưng nó kẹt cứng ở một lời gọi ghi không bao giờ trả về.

Tôi suýt kết luận rằng ngưỡng đã đổi giữa hai lần chạy. Cái làm lộ ra là chạy lại một tải nhỏ đã biết chắc phải chạy — nó cũng hỏng, và một tải nhỏ không thể là vấn đề MTU. Đó là dấu hiệu vấn đề nằm ở server chứ không ở mạng.

Sau khi đổi sang server đa luồng, ngưỡng 1340/1350 mới ổn định và lặp lại được.

Cách bắt: đo MTU thật của đường đi

ip link chỉ cho bạn biết giao diện khai gì, không cho biết đường đi chịu được gì. Muốn biết thật thì gửi gói có bit "không phân mảnh":

ping -M do -s 1372 -c1 10.10.2.10
Dữ liệu Gói IP tổng Kết quả
100 byte 128 qua được
1300 byte 1328 qua được
1372 byte 1400 qua được
1373 byte 1401 KHÔNG qua
1472 byte 1500 KHÔNG qua

Ranh giới nằm chính xác ở 1400, trong khi mọi ip link trên đường đều ghi 1500. Cộng 28 byte (20 IP + 8 ICMP) vào con số -s lớn nhất còn qua được là ra MTU thật.

Và để ý: gói quá cỡ không sinh ra thông báo lỗi nào cảping chỉ im lặng rồi báo mất gói. Không có dòng "Frag needed and DF set" quen thuộc, vì đó chính là gói ICMP đang bị chặn.

Một cái bẫy trong chính công cụ đo

Lần đầu chạy, ping -M do báo thất bại ở mọi kích thước, kể cả 100 byte. Tôi suýt kết luận đường mạng hỏng hoàn toàn.

Thật ra ping trong Alpine là busybox, và busybox không có tuỳ chọn -M. Nó in ra trang trợ giúp thay vì chạy, và vì tôi chỉ xét mã thoát nên mọi thứ trông như "không qua".

apk add --no-cache iputils    # ping that, co -M do

Ca đối chứng phát hiện ra chuyện này: một gói 100 byte phải qua được. Khi ca chắc chắn đúng cũng báo sai, thì thứ hỏng là công cụ đo. Đây là lần thứ ba trong sê-ri này busybox làm hỏng một phép đo bằng cách im lặng không hỗ trợ thứ tôi tưởng nó hỗ trợ.

Hai cách vá, đo cả hai

Kẹp MSS ở router. Sửa MSS trong gói SYN để hai đầu tự thoả thuận kích thước nhỏ hơn:

iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu
Kích thước Sau khi kẹp MSS
1350 byte ✅ 0,1 s
2000 byte ✅ 0,1 s
100.000 byte ✅ 0,1 s

Hạ MTU của chính mạng Docker. Không cần đụng tường lửa:

docker network create --opt com.docker.network.driver.mtu=1400 ten-mang

Đo lại với eth0 của hai container ở 1400: cả ba kích thước đều xong trong 0,1 giây.

Cách thứ hai đúng hơn về mặt khái niệm — bạn khai đúng sự thật thay vì vá triệu chứng — nhưng nó đòi bạn biết con số đúng, và phải nhớ khai cho mọi mạng mới tạo.

Thử ba mươi giây

Khi một dịch vụ "chạy với dữ liệu nhỏ, treo với dữ liệu lớn":

docker run --rm --network <mang> nicolaka/netshoot \
  ping -M do -s 1472 -c2 <dich>
  • Qua được → không phải MTU, tìm chỗ khác.
  • Không qua → giảm dần -s tới khi qua, cộng 28, đó là MTU thật của đường đi.

Ba dấu hiệu nhận dạng nhanh:

  • Request nhỏ chạy, request lớn treo — cùng một endpoint.
  • Mã HTTP đã về nhưng thân rỗng.
  • ip link trên mọi chặng đều ghi 1500, mà ping -M do -s 1472 không qua.

Phần sau gom lại thành một quy trình gỡ lỗi mạng container hoàn chỉnh, chạy trên vài sự cố dựng sẵn.