Bóp băng thông (traffic shaping) là chuyện thường ngày: nhà mạng giới hạn gói cước của bạn, một container bị đặt trần tốc độ, một proxy giữ cho một khách không nuốt hết đường truyền. Công cụ kinh điển trên Linux là tc. Câu hỏi tôi mang vào bài này tưởng đã có sẵn đáp án: bóp băng thông xuống thấp rồi cho đường truyền bão hòa thì độ trễ sẽ nổ tung vì gói xếp hàng dài — hiện tượng "bufferbloat" nổi tiếng. Tôi dựng đúng thí nghiệm đó trong container để đo con số cụ thể — và phép đo bác bỏ điều tôi chắc mẩm.

Bóp băng thông

Bóp băng thông và cái hàng đợi phía sau

Khi bạn đặt một trần tốc độ bằng tc (qua tbf — token bucket filter, hay htb — hierarchical token bucket), cơ chế rất đơn giản: gói nào tới nhanh hơn trần cho phép thì không bị vứt ngay, mà bị xếp vào một hàng đợi ở nút thắt, chờ tới lượt được gửi đúng tốc độ đã đặt. Phần "bóp tốc độ" này dễ và chính xác. Nhưng cái hàng đợi đó mới là nơi hành vi thật ẩn náu: một gói phải chờ sau cả hàng dài thì độ trễ của nó phình lên, kể cả khi nó là một gói bé tí nhạy cảm với thời gian (một cú ping, một tin chat). Đó là bufferbloat — đệm phình — và nó là lý do một đường truyền "đầy" thường kèm độ trễ tệ hại.

Tôi dựng nút thắt 10 mbit trong container với một đệm FIFO to (3 MB), nền RTT ~29 ms, rồi đo hai thứ: bóp có chính xác không, và độ trễ (ping) biến dạng ra sao khi đường truyền bão hòa.

Đo: bóp thì chính xác, đệm thì không phình như tôi tưởng

Trước hết, bóp hoạt động đúng: tải một file lớn qua trần 10 mbit cho thông lượng đạt 9,0 mbit — sát mức đặt (phần thiếu là overhead của gói và HTTP). Không bất ngờ.

Bất ngờ nằm ở độ trễ. Tôi cho bốn luồng TCP tải file cùng lúc để bão hòa nút thắt, rồi ping trong lúc đó:

Tình huống Hàng đợi đọng Ping
Rảnh (không tải) ~29 ms
4 luồng TCP bão hòa 8 gói (~24 KB) 38 ms

Đệm to 3 MB, mà trong lúc bốn luồng TCP đang kéo căng đường truyền, hàng đợi chỉ đọng 8 gói — khoảng 24 KB, gần như trống. Ping chỉ nhích từ 29 lên 38 ms. Không hề có màn "độ trễ nổ lên hàng giây" mà tôi chờ đợi. Tôi thử tắt luôn cơ chế TCP Small Queues (đặt tcp_limit_output_bytes cực lớn để nó không ghìm) — vẫn thấp y như vậy. Cái đệm phình mà tôi định phô ra... không chịu phình.

Một lần tôi đo hớ: bufferbloat cần một nguồn không lùi

Tôi vào bài với một "sự thật" đóng đinh trong đầu: bóp băng thông + đệm to + bão hòa = bufferbloat, độ trễ vọt lên hàng giây. Đó là kết quả kinh điển từ các bài viết khoảng năm 2011. Tôi đã chuẩn bị sẵn câu chữ mô tả cảnh ping nhảy lên 1-2 giây. Phép đo cho 8 gói và 38 ms tát thẳng vào giả định đó.

Lý do không nằm ở tc mà nằm ở cái nguồn phát. TCP hiện đại tự ghìm: nó không mù quáng bơm dữ liệu vào đường truyền hết cỡ. Nó nhịp gói ra đều (pacing), và quan trọng hơn, khi hàng đợi bắt đầu đầy và gói bắt đầu bị trễ hoặc rớt, TCP hiểu đó là dấu hiệu nghẽn và giảm tốc (kiểm soát tắc nghẽn đã đo ở phần trước). Kết quả: TCP giữ cho hàng đợi ngắn trong khi vẫn lấp đầy băng thông — đúng thứ nó được thiết kế để làm. Cái đệm 3 MB gần như trống vì không có ai muốn nhồi đầy nó.

Để thật sự thấy đệm phình, tôi cần một nguồn không hề lùi. Tôi bắn một luồng flood UDP — gửi gói ào ạt nhanh hơn trần, và UDP không có cơ chế nào để chậm lại. Lần này:

Tình huống Hàng đợi đọng Ping
Flood UDP qua đệm 3 MB 86 gói (~124 KB) 100 ms (gấp 3,4 lần)

Giờ đệm mới bắt đầu đầy (86 gói) và ping mới gấp hơn ba lần. Đây mới là bufferbloat — nhưng nó chỉ xuất hiện với một nguồn không tự điều tiết.

Bài học đo lường có hai lớp. Lớp thứ nhất về nội dung: "bóp băng thông làm độ trễ nổ tung" là câu đúng cho một nguồn không lùi, sai cho TCP biết điều — thủ phạm của bufferbloat không phải cái trần tốc độ, mà là bộ đôi "đệm ngu (FIFO to) + nguồn không lùi". Lớp thứ hai, và đáng nhớ hơn, về phương pháp: cái nhân Linux tôi đang đo trên đó là một biến số, không phải một hằng số bất biến. Các bài demo bufferbloat kinh điển ra đời trước khi nhân có nhiều lớp chống phình đệm (pacing, TSQ, các mặc định khôn hơn như fq_codel). Tôi định dựng lại một kết quả cũ mà quên rằng hệ điều hành đã được vá suốt mười mấy năm — nên cùng một thí nghiệm giờ cho kết quả khác. Nếu tôi tin định kiến và viết đại "ping vọt lên hàng giây", tôi đã bịa ra một con số mà chính máy của mình bác bỏ. Phải đo trên đúng nhân của mình, ở đúng thời của mình.

Vì sao điều này quan trọng khi lập trình

Hệ quả đầu tiên là hiểu đúng bufferbloat để chẩn đoán nó. Khi bạn thấy độ trễ tệ đi lúc đường truyền bận, thủ phạm không phải "băng thông hết" mà là hàng đợi ở nút thắt đang dài. Và nó phụ thuộc vào ai đang phát: nhiều luồng TCP biết điều thường tự giữ hàng đợi ngắn; một luồng tải lên/xuống ồ ạt qua một router gia đình đời cũ với đệm to và không có quản lý hàng đợi thông minh mới là công thức kinh điển làm nghẹt cả nhà. Biết cơ chế giúp bạn nhìn đúng chỗ: đo độ trễ khi có tải, không chỉ khi rảnh, vì cái đuôi trễ này (như bài trễ đuôi p99) trốn khỏi phép đo lúc nhàn.

Hệ quả thứ hai là chọn kỷ luật hàng đợi (qdisc) thông minh khi bạn kiểm soát nút thắt. Nếu bạn dựng một bộ shaper (trên router, trên gateway, trong container), đừng để nó dùng một FIFO to ngu ngốc. Các qdisc hiện đại như fq_codel hay cake chủ động giữ hàng đợi ngắn (thả gói sớm để báo nghẽn) và tách luồng để một luồng tham không bóp nghẹt các luồng nhạy trễ. Đây chính là những thứ đã "âm thầm cứu" phép đo của tôi — và là lý do một đường truyền hiện đại bị bão hòa vẫn giữ được độ trễ tử tế, trong khi một thiết bị đời cũ thì không.

Hệ quả thứ ba là một cách nghĩ chung về đo lường, đúng cho cả sê-ri: một kết quả đúng trong quá khứ không mặc nhiên còn đúng trên hệ thống hôm nay. Con số mang theo: bóp băng thông bằng tc thì chính xác (10 mbit ra 9 mbit), nhưng bufferbloat — độ trễ phình vì hàng đợi — chỉ xảy ra khi nguồn phát không tự lùi; TCP đời nay tự ghìm nên đệm 3 MB chỉ đọng 8 gói, còn flood UDP mới bơm nó lên 86 gói và làm ping gấp ba. Khi định dựng lại một thí nghiệm kinh điển, hãy chạy nó trên chính môi trường của mình trước khi tin vào cái kết luận cũ; hệ thống thay đổi, và phép đo là cách duy nhất bắt kịp.

Thử ba mươi giây

Mở hai cửa sổ dòng lệnh. Ở cửa sổ một, chạy ping mot-may-chu và để nó chạy — bạn thấy độ trễ nền, giả sử 20-30 ms. Ở cửa sổ hai, khởi động một tải lớn: tải lên một tệp nặng, hoặc chạy một bản sao lưu qua mạng. Nhìn lại cửa sổ ping: nếu độ trễ vọt lên hàng trăm mili giây trong lúc tải, mạng của bạn đang bufferbloat — cái đệm ở đâu đó trên đường (thường là router nhà) đang phình. Nếu nó gần như đứng yên, thiết bị của bạn đã có quản lý hàng đợi thông minh. Nhiều router hiện đại cho bật fq_codel/cake trong phần cài đặt QoS — bật nó lên rồi đo lại chính là thấy tận mắt điều bài này đo: cùng một đường truyền bị bóp, hàng đợi ngu và hàng đợi khôn cho hai trải nghiệm trễ khác hẳn nhau.