Chuyển hướng (redirect) là thứ xảy ra thầm lặng: bạn gõ một địa chỉ, và trước khi thấy trang, trình duyệt lặng lẽ đi qua một hoặc vài địa chỉ trung gian. Mỗi bước trung gian đó không miễn phí — nó là một vòng khứ hồi mạng phụ trước khi nội dung thật kịp về. Bài này dựng một máy chủ có các chuỗi chuyển hướng của riêng tôi, đo chính xác mỗi 301/302 thêm bao nhiêu vòng, và chỉ ra vì sao con số đó phụ thuộc vào một điều tôi suýt bỏ qua.

Chuyển hướng

Cách chuyển hướng hoạt động

Khi client xin một địa chỉ A và server muốn nó đi chỗ khác, server trả về một mã 3xx (thường là 301 hoặc 302) kèm header Location: B. Client đọc header đó rồi tự gửi một request mới tới B. Nội dung thật chỉ về sau bước thứ hai này — nên mỗi lần chuyển hướng chèn thêm một chuyến đi-về mạng.

Khác biệt quan trọng giữa hai mã: 301 (Moved Permanently) nói "địa chỉ này đã dời hẳn", nên trình duyệt được phép nhớ và lần sau đi thẳng tới B, bỏ qua A. 302 (Found / tạm thời) nói "tạm thời dùng B thôi", nên trình duyệt không nhớ — lần nào cũng phải qua A trước. Sự khác biệt này không thấy ở lần đầu, nhưng lộ rõ ở những lần sau.

Đo: mỗi chuyển hướng thêm một vòng

Tôi đo trên đường có RTT giả lập ~60 ms, dùng lại một kết nối keep-alive (như một client tử tế nói chuyện với cùng một host):

Tình huống Số 3xx Thời gian
Trực tiếp /final (không 3xx) 0 ~120 ms
1 chuyển hướng (dùng lại kết nối) 1 ~185 ms
Chuỗi 3 chuyển hướng 3 ~300 ms

Con số rất đều: mỗi chuyển hướng thêm khoảng một vòng khứ hồi (~60 ms). Từ 0 lên 1 chuyển hướng, thời gian tăng ~65 ms; chuỗi 3 chuyển hướng cộng thêm ~180 ms, tức ~60 ms mỗi bước. Một chuỗi http://old.comhttps://old.comhttps://new.com → trang thật, ba bước, là ba vòng khứ hồi phụ chồng lên trước khi người dùng thấy gì.

Và đây là cái lợi của 301, đo được ở lần thứ hai:

/r301 lần 2 (đã cache) -> 0 chuyển hướng, ~120 ms
/r302 lần 2 (không cache) -> vẫn 1 chuyển hướng, ~185 ms

Vì trình duyệt nhớ 301, lần thứ hai nó đi thẳng tới đích như thể chưa từng có chuyển hướng. Còn 302, không được nhớ, nên lần nào cũng trả cái vòng đó. Với một địa chỉ được truy cập triệu lần, chọn 301 hay 302 là chọn có nhân cái vòng khứ hồi đó lên triệu lần hay không.

Một lần tôi đo hớ: không phải chuyển hướng nào cũng rẻ như nhau

Đo tới đây, tôi định chốt gọn: "một chuyển hướng bằng một vòng khứ hồi". Nhưng phép đo trên có một giả định ẩn mà tôi suýt quên nói: nó dùng lại kết nối. Cả A và B đều trên cùng một host, nên client giữ nguyên kết nối TCP đã mở và chỉ gửi thêm một request — đúng một vòng.

Tôi đo lại với client mở một kết nối mới cho mỗi bước (mô phỏng chuyển hướng sang host khác, nơi kết nối cũ không dùng lại được):

1 chuyển hướng, kết nối MỚI: ~250 ms  (không phải 185)
chuỗi 3, kết nối MỚI mỗi hop: ~500 ms  (không phải 300)

Mỗi chuyển hướng giờ tốn khoảng hai vòng thay vì một, vì mỗi bước phải bắt tay TCP lại từ đầu. Và điều này rất thực tế: phần lớn chuyển hướng đáng kể trong đời là đổi hostold.com sang new.com, www sang không-www, một link rút gọn sang đích thật, một luồng đăng nhập OAuth nhảy qua vài tên miền. Mỗi lần đổi host không chỉ là một request mới, mà là một phân giải DNS mới, một bắt tay TCP mới, và nếu là HTTPS thì cả bắt tay TLS mới — tức hai tới bốn vòng khứ hồi cho mỗi hop, không phải một.

Bài học đo lường: một con số "trung bình" che giấu điều kiện tạo ra nó. "Một chuyển hướng = một vòng" đúng cho chuyển hướng cùng host với kết nối tái dùng, nhưng sai gấp mấy lần cho chuyển hướng đổi host. Tôi suýt viết một kết luận gọn gàng mà bỏ mất chính cái yếu tố quyết định — kết nối có được dùng lại không. Khi đo một chi phí, phải hỏi rõ nó được đo trong điều kiện nào, và điều kiện đó có khớp với thực tế mình quan tâm không.

301/302 có thể đổi method; 307/308 thì không

Có một cái bẫy nữa với chuyển hướng, không phải về tốc độ mà về tính đúng. Theo lịch sử và cách trình duyệt cư xử thực tế, khi gặp 301 hoặc 302 cho một request POST, client thường đổi method thành GET cho bước tiếp theo và bỏ luôn thân request. Với một chuyển hướng trang web bình thường thì vô hại, nhưng với một POST mang dữ liệu — gửi form, gọi API tạo tài nguyên — thì dữ liệu biến mất trên đường chuyển hướng, và lời gọi âm thầm hỏng.

HTTP về sau thêm hai mã để cứu chuyện này: 307 (Temporary Redirect)308 (Permanent Redirect) giữ nguyên method và thân. Nếu bạn cần chuyển hướng một POST mà không mất dữ liệu, dùng 307/308 chứ không phải 302/301. Đây là kiểu lỗi chỉ lộ ra với các request có thân đi qua một chuyển hướng — dễ bỏ sót khi chỉ thử bằng trình duyệt với các GET.

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

Hệ quả đầu tiên: cắt ngắn chuỗi chuyển hướng. Mỗi hop là độ trễ thuần túy mà người dùng cảm nhận trước khi thấy nội dung. Một cấu hình phổ biến vô tình xếp chồng nhiều hop — http://www.site.comhttps://www.site.comhttps://site.com là ba request cho một lần truy cập. Gộp lại thành một chuyển hướng thẳng tới đích cuối (http://www.site.comhttps://site.com một bước) cắt được các vòng thừa.

Hệ quả thứ hai: dùng 301 cho cái gì vĩnh viễn, 302 cho cái gì thật sự tạm. 301 được cache nên lần sau miễn phí, và các công cụ tìm kiếm cũng chuyển "uy tín" của URL cũ sang URL mới — quan trọng cho SEO. Dùng nhầm 302 cho một chuyển hướng vĩnh viễn là bắt mọi khách trả lại cái vòng khứ hồi mỗi lần, mãi mãi. Ngược lại, dùng 301 cho một chuyển hướng thật ra chỉ tạm thời sẽ khiến trình duyệt "kẹt" ở đích cũ khó gỡ.

Hệ quả thứ ba: đặt chuyển hướng càng gần người dùng càng tốt. Một chuyển hướng xử lý ở tầng CDN hay máy chủ biên trả về ngay sau một vòng ngắn; một chuyển hướng phải đi hết vào ứng dụng backend rồi mới trả 3xx thì cái vòng đó dài hơn nhiều. Con số mang theo: mỗi chuyển hướng thêm ít nhất một vòng khứ hồi, và thêm hẳn một bắt tay đầy đủ nếu đổi host; 301 được cache nên chỉ trả giá một lần, 302 thì trả mỗi lần. Chuyển hướng là công cụ cần thiết, nhưng mỗi cái là một khoản độ trễ có thật — đếm chúng, rút ngắn chúng, và chọn đúng mã.

Thử ba mươi giây

Chạy curl -sIL https://mot-trang-web 2>&1 | grep -iE 'HTTP/|location' — cờ -L bảo curl đi theo chuyển hướng, và bạn sẽ thấy từng bước: mỗi dòng HTTP/… 301 hay 302 kèm một location: là một hop, một vòng khứ hồi. Đếm số dòng đó là đếm số chuyển hướng trước khi tới đích. Thêm -w 'tong: %{time_total}s, so hop: %{num_redirects}\n' -o /dev/null để curl in thẳng tổng thời gian và số lần chuyển hướng — thử trên một link rút gọn (thường nhiều hop, đổi host) so với một trang thẳng để thấy khác biệt bài này đo.