Gần như mọi hệ thống web nghiêm túc đều có một proxy ngược (reverse proxy) đứng trước: nginx, HAProxy, hay một lớp cân bằng tải của nhà cung cấp. Client không nói chuyện trực tiếp với ứng dụng, mà với proxy; proxy chuyển tiếp request tới backend rồi trả kết quả về. Câu hỏi tự nhiên: lớp trung gian đó thêm bao nhiêu độ trễ? Tôi dựng một nginx thật trước một backend của riêng mình trong container để đo — và câu trả lời trái với điều tôi tưởng.
Proxy ngược làm gì
Một proxy ngược nhận request từ client rồi thay mặt client gọi tới một hoặc nhiều backend. Nó gánh rất nhiều việc mà backend không phải lo: kết thúc TLS một lần ở lớp ngoài (thay vì mỗi backend tự làm), cân bằng tải giữa nhiều backend, cache các phản hồi, gộp và tái dùng kết nối tới backend, và đệm các client chậm để backend không bị giữ chân. Đổi lại tất cả những lợi ích đó, nó chèn thêm một chặng vào đường đi của mỗi request. Câu hỏi là chặng đó đắt cỡ nào.
Đo: chặng thêm gần như miễn phí — khi cùng máy
Tôi đặt backend và nginx trên cùng một host, client ở xa với RTT giả lập, rồi so thời gian một request đi thẳng tới backend với đi qua nginx:
| Tình huống | Trực tiếp | Qua proxy | Thêm |
|---|---|---|---|
| Proxy & backend cùng máy (loopback) | 66 ms | 58 ms | ~0 (trong nhiễu) |
Con số làm tôi bất ngờ: đi qua proxy không chậm hơn đi thẳng — thậm chí còn nhỉnh hơn một chút, nằm trọn trong sai số đo. Chặng thêm mà nginx chèn vào (nhận request, mở/tái dùng kết nối tới backend qua loopback, chuyển tiếp) gần như bằng không. Lý do: khi backend nằm ngay trên cùng máy, chặng proxy↔backend đi qua loopback — không có độ trễ mạng thật — và bản thân nginx là code C được tối ưu tới mức phần xử lý của nó nhỏ hơn cả mức nhiễu của phép đo.
Một lần tôi đo hớ: không có "phí proxy cố định"
Tôi vào bài này với một niềm tin gọn gàng: "proxy thêm một chặng, nên nó phải tốn thêm độ trễ — mình sẽ đo ra con số đó". Nhưng con số ~0 ở trên cho thấy niềm tin đó sai về bản chất. Không có một "phí proxy" cố định; cái tôi định đo không tồn tại như một hằng số.
Độ trễ thêm thật sự bằng đúng một vòng khứ hồi giữa proxy và backend — và giá trị đó phụ thuộc hoàn toàn vào việc chúng cách nhau bao xa. Để chứng minh, tôi thêm 20 mili giây độ trễ vào chính chặng loopback giữa nginx và backend (mô phỏng backend nằm ở một máy khác):
| Tình huống | Trực tiếp | Qua proxy | Thêm |
|---|---|---|---|
| Proxy & backend khác máy (~20 ms) | 60 ms | 89 ms | ~29 ms (một vòng) |
Bây giờ proxy thêm khoảng 29 mili giây — đúng bằng vòng khứ hồi proxy↔backend cộng chút xử lý. Cùng một nginx, cùng một backend, chỉ khác khoảng cách giữa hai lớp, mà độ trễ thêm nhảy từ ~0 lên ~29 ms.
Bài học đo lường: trực giác "thêm một chặng thì thêm chi phí" ước lượng sai khi chặng đó là loopback. Chi phí của một proxy không phải một con số dán sẵn, mà là độ trễ mạng giữa nó và backend — bằng không khi cùng máy, bằng RTT khi khác máy. Câu hỏi "proxy thêm bao nhiêu độ trễ?" không có một đáp án; nó phụ thuộc vào topo triển khai. Tôi cũng đã đoán nhầm một chuyện thứ hai: tưởng nginx sẽ đệm và làm chậm một phản hồi dạng streaming, nhưng đo ra nó chuyển tiếp từng khối gần như tức thì (TFB qua proxy 168 ms so với trực tiếp 165 ms). Hai giả định, cả hai đều bị phép đo bác bỏ.
Proxy cũng là một tầng lỗi mới
Đổi lại những lợi ích, proxy thêm một tầng nơi mọi thứ có thể hỏng theo cách riêng của nó — và biết các mã lỗi của nó giúp gỡ rối nhanh hơn nhiều. Hai mã quen thuộc là của chính proxy, không phải backend: 502 Bad Gateway nghĩa là proxy có liên lạc được với backend nhưng nhận về một phản hồi hỏng (hoặc backend chết giữa chừng), còn 504 Gateway Timeout nghĩa là backend không trả lời trong thời gian proxy cho phép. Thấy 502/504 là dấu hiệu vấn đề nằm ở phía sau proxy, không phải ở proxy.
Điều này cũng nghĩa là proxy có thời gian chờ (timeout) riêng, tách biệt với timeout của ứng dụng. Một request mà backend xử lý mất 60 giây có thể bị proxy cắt ở giây thứ 30 với một 504, dù backend vẫn đang chạy — một sự cố kinh điển với các request chậm mà nhiều người đổ lỗi nhầm cho ứng dụng. Khi đặt một proxy, luôn chỉnh timeout của nó cho khớp với các request chậm nhất mà backend thật sự cần, nếu không proxy sẽ âm thầm giết những request dài mà backend đang phục vụ đúng.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên là về topo triển khai. Vì độ trễ thêm của proxy bằng khoảng cách tới backend, hãy đặt chúng gần nhau — cùng máy, cùng rack, cùng vùng khả dụng. Một cấu hình đặt proxy ở một vùng và backend ở vùng khác vô tình cộng một vòng khứ hồi liên vùng vào mỗi request. Ngược lại, một proxy đặt sát backend gần như không tốn gì, nên nỗi sợ "thêm lớp proxy làm chậm" thường là vô căn cứ nếu triển khai đúng.
Hệ quả thứ hai là cái giá tí hon đó mua rất nhiều. Với vài chục micro giây (khi cùng máy), proxy cho bạn: kết thúc TLS tập trung (backend chạy HTTP thường, đỡ gánh mã hóa), một điểm để cân bằng tải và rút một backend hỏng ra khỏi vòng, cache tầng ngoài, và — quan trọng — một bể kết nối keep-alive tới backend, nên các backend không phải trả thuế bắt tay cho mỗi request client mới (đúng cái đã đo ở bài keep-alive). Proxy còn đệm các client chậm: nó đọc trọn request chậm rề từ một client di động yếu rồi mới chuyển cho backend trong một nhịp nhanh, giải phóng backend khỏi việc ngồi chờ.
Hệ quả thứ ba là một bài học đo lường chung: đừng đo một chi phí bằng trực giác; hãy đo nó trong đúng điều kiện triển khai của mình. "Proxy chậm" hay "proxy nhanh" đều vô nghĩa nếu không nói proxy cách backend bao xa. Con số mang theo: một lớp proxy ngược thêm đúng một vòng khứ hồi giữa nó và backend — gần như 0 khi cùng máy, bằng RTT khi khác máy — và cái giá nhỏ đó đổi lấy TLS tập trung, cân bằng tải, cache và gộp kết nối. Proxy ngược gần như luôn đáng, miễn là bạn đặt nó sát backend.
Thử ba mươi giây
Nếu bạn có một dịch vụ sau nginx, so hai con số bằng curl: gọi thẳng backend (nếu truy cập được) và gọi qua proxy, mỗi cái với -w 'ttfb=%{time_starttransfer}s\n' -o /dev/null -s. Hiệu hai thời gian TTFB chính là độ trễ proxy thêm vào — và nếu proxy với backend cùng máy, hiệu đó sẽ nhỏ tới mức khó đo. Muốn thấy nó lớn lên, hình dung (hoặc thử) đặt backend ở một vùng khác: mỗi mili giây RTT giữa proxy và backend cộng thẳng vào mỗi request. Đó là toàn bộ "chi phí proxy" — không hơn, không kém.