Hình dung một tuyến giao hàng có một cây cầu chui thấp ở giữa đường. Xe con chui lọt, xe tải cao quá thì không. Bình thường trước cầu có tấm biển "quá cao, quay lại" — tài xế thấy biển thì đổi xe nhỏ hơn hoặc đi đường khác, mọi thứ vẫn trôi. Nhưng nếu ai đó gỡ mất tấm biển, chiếc xe tải cứ thế lao vào, kẹt cứng dưới gầm cầu, và ở đầu bên kia người nhận chỉ thấy hàng không bao giờ tới — không một lời báo. MTU lệch chính là cây cầu thấp đó, còn ICMP bị chặn là tấm biển bị gỡ; và điều đáng nhớ nhất của cả bài là: chỉ một trong hai thứ thì vô hại, phải cả hai cùng lúc mới thành hố đen.
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, và 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.
Tự bắt lỗi nà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
-stớ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 linktrên mọi chặng đều ghi 1500, màping -M do -s 1472không qua.
Mẫu số chung
Bài học đầu tiên, chính là tấm biển bị gỡ: rất nhiều hệ thống tự sửa được nhờ một kênh phản hồi riêng — và khi bạn bịt cái kênh đó "cho an toàn", một tình huống lẽ ra hồi phục được biến thành hỏng im lặng. Path MTU Discovery dựa vào gói ICMP "fragmentation needed" để hai đầu tự thu nhỏ gói; chặn ICMP vì "cho chắc" là rút mất đúng cái tín hiệu giữ cho mọi thứ tự điều chỉnh — và cầu thấp một mình thì vô hại, mất biển báo mới thành hố đen. Cùng cái bẫy "giết kênh phản hồi" ở khắp nơi: tắt heartbeat/health-check vì thấy ồn, catch rồi nuốt ngoại lệ, bỏ qua giá trị lỗi trả về, lọc hết cảnh báo trình biên dịch. Nguyên tắc: trước khi tắt một luồng tín hiệu vì nó "gây phiền", hỏi xem có cơ chế nào đang âm thầm dựa vào nó để tự sửa không — cái bạn coi là nhiễu có thể đúng là thứ đang biến sự cố nặng thành sự cố nhẹ.
Điều thứ hai, đọc từ "mã 200 mà thân rỗng, nghe như lỗi ứng dụng": triệu chứng thường hiện ra ở tầng khác với nguyên nhân — tầng nhìn thấy được (ứng dụng: response rỗng) thì vô tội, tầng có lỗi (đường mạng) thì vô hình — nên cách khoanh vùng là chạy một ca biết chắc kết quả. Chính ca đối chứng đã hai lần cứu tôi ở bài này: tải nhỏ cũng hỏng → lỗi ở server chứ không ở MTU; gói 100 byte cũng "không qua" → lỗi ở công cụ đo (busybox) chứ không ở mạng. Cùng chuyện "triệu chứng lệch tầng" ở khắp nơi: hết bộ nhớ hiện ra thành truy vấn chậm, cạn thread-pool trông như CSDL timeout, lỗi DNS đội lốt lỗi ứng dụng. Nguyên tắc: khi triệu chứng và nguyên nhân ở hai tầng khác nhau, hãy chèn một ca đối chứng mà bạn đã biết đáp án để chốt xem tầng nào thật sự hỏng — trước khi sửa nhầm tầng vô tội.
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.