Đang tải bài viết...
TCP vs UDP: Gửi bảo đảm hay gửi thật nhanh?
"TCP tin cậy, UDP nhanh." Câu này ai cũng thuộc, nhưng chữ "nhanh" dễ gây hiểu lầm. UDP không làm dữ liệu chạy nhanh hơn trên dây. Nó nhanh vì không chờ: không chờ bắt tay, không chờ xác nhận, không chờ gửi lại. Hiểu đúng điều này thì mới trả lời được hai câu hỏi thực tế: khi nào cần sự chính xác, khi nào cần sự kịp thời, và vì sao HTTP/3 lại chạy trên UDP.
TCP: tin cậy, đúng thứ tự
TCP coi dữ liệu là một luồng byte liên tục. Mỗi segment mang số thứ tự, và bên nhận gửi ACK ngược về để báo đã nhận đủ tới đâu. ACK có tính tích lũy: một ACK xác nhận mọi byte tới một vị trí, chứ không xác nhận riêng từng gói. Linux còn dùng delayed ACK, tức là thường gom hai segment rồi mới ACK một lần.
Vì thế trong hình minh họa, khi các segment vẫn liên tục được gửi đi, B chỉ gửi về vài ACK cho phần dữ liệu đã nhận, không có ACK riêng cho từng gói. TCP không cần chờ ACK mới gửi tiếp: nó gửi liên tục trong phạm vi cửa sổ cho phép.
Segment nào không được ACK sẽ được gửi lại. Bên nhận chỉ giao dữ liệu lên ứng dụng khi đã đủ và đúng thứ tự. TCP cũng tự giảm tốc khi mạng nghẽn (congestion control).
Cái giá của sự tin cậy này là: khi mất một segment, mọi dữ liệu phía sau phải chờ segment đó được gửi lại. Hiện tượng này gọi là head-of-line blocking.
UDP: nhanh, không chờ
UDP gửi từng datagram độc lập, với header chỉ 8 byte gồm port nguồn, port đích, độ dài và checksum. Không có số thứ tự, không có ACK, không gửi lại. Như trong hình, các gói có thể đến lộn xộn (1, 3, 2, 5), còn gói 4 mất là mất hẳn, không ai gửi lại.
Nghe thì tệ, nhưng có một điểm quan trọng: gói 5 vẫn đến ngay, không phải chờ gói 4. Với nhiều loại dữ liệu, đó chính là điều cần thiết.
Khi nào cần chính xác, khi nào cần kịp thời?
Câu hỏi quyết định là: dữ liệu đến muộn có còn giá trị không?
Cần chính xác > TCP. Thiếu một byte là hỏng kết quả: truy vấn và replication database, API nội bộ, SSH, rsync, backup, kéo image từ container registry, log cần lưu cho kiểm toán (syslog qua TCP 6514 có TLS thay vì UDP 514).
Cần kịp thời > UDP. Dữ liệu cũ thì vô dụng: VoIP và gọi video (mất một khung hình thì bỏ qua), NTP, metrics kiểu StatsD, và tunnel như WireGuard hay VXLAN. Với tunnel, giao thức bên trong đã tự lo độ tin cậy. Bọc TCP trong TCP sẽ khiến retransmit chồng lên nhau (TCP meltdown).
Trường hợp lai giữa hai giao thức: DNS.
DNS mặc định dùng UDP vì truy vấn ngắn, mất thì hỏi lại còn rẻ hơn mở kết nối TCP. Nhưng khi phản hồi lớn, ví dụ có DNSSEC hoặc khi đồng bộ zone, DNS chuyển sang TCP 53. Firewall chỉ mở UDP 53 là cấu hình sai.
Cái giá của "không chờ" mà đội infra phải gánh.
UDP không có trạng thái kết nối, nên conntrack trên firewall hoặc NAT chỉ đoán được bằng timeout. Trên Linux, mặc định khoảng 30 giây với luồng một chiều và khoảng 120 giây khi đã có gói hai chiều. Ứng dụng im lặng lâu hơn mức này sẽ mất mapping NAT mà không có lỗi nào. Mất gói UDP cũng không để lại dấu vết ở ứng dụng, nên phải chủ động theo dõi:
# UDP: drop vì buffer nhận đầy
nstat -az UdpRcvbufErrorsUdpInErrors
# TCP: mất gói lộ ra qua retransmit
nstat -az TcpRetransSegs
Vì sao HTTP/3 lại chạy trên UDP?
Nói chính xác, HTTP/3 chạy trên QUIC (RFC 9000), còn QUIC chạy trên UDP. QUIC làm lại những gì TCP làm (gửi lại, sắp thứ tự, kiểm soát tắc nghẽn) nhưng ở userspace. HTTP/3 không từ bỏ sự chính xác, nó chỉ chuyển trách nhiệm đảm bảo độ tin cậy lên một tầng khác.
Có ba lý do:
1. Gỡ head-of-line blocking.
HTTP/2 ghép nhiều request vào một kết nối TCP, nhưng TCP không biết đến các stream đó, nên mất một segment là mọi request cùng kẹt. QUIC cho mỗi stream độc lập ở tầng giao vận: gói mất của stream này không chặn stream khác.
2. Bớt vòng chờ.
TCP cộng TLS 1.3 cần 2 RTT trước khi gửi được dữ liệu. QUIC gộp bắt tay và mã hóa làm một nên chỉ cần 1 RTT, và 0-RTT khi kết nối lại. Với RTT 200ms từ Việt Nam sang Mỹ, mỗi vòng bớt đi là 200ms người dùng cảm nhận được.
3. Lý do thật sự: Internet không cho sửa TCP.
TCP nằm trong kernel của hàng tỷ thiết bị. Firewall, NAT và DPI đọc và sửa TCP header, rồi drop gói có option lạ. TCP Fast Open hoạt động tốt về kỹ thuật nhưng chưa bao giờ phổ biến được vì middlebox xử lý sai. SCTP có sẵn multi-stream từ năm 2000 nhưng không qua được phần lớn NAT. UDP là thứ duy nhất đi qua mọi nơi, nên QUIC dùng nó làm lớp vỏ, rồi mã hóa gần như toàn bộ header của mình để middlebox không thể bám vào mà làm giao thức "hóa thạch" lần nữa.
Ngoài ra, QUIC định danh kết nối bằng connection ID thay vì 4-tuple. Nhờ vậy, điện thoại chuyển từ Wi-Fi sang 4G vẫn giữ được kết nối.
Tác động tới infra:
- UDP 443: Trình duyệt kết nối lần đầu qua TCP, thấy Alt-Svc: h3 (hoặc bản ghi DNS HTTPS) rồi mới thử QUIC. Nếu UDP 443 bị chặn, trình duyệt lặng lẽ quay về TCP. Nhiều tổ chức cố ý chặn để giữ TLS inspection, vì QUIC đi vòng qua DPI.
- Load balancer L4: hash theo 4-tuple sẽ làm hỏng connection migration. Muốn giữ tính năng này, LB phải định tuyến theo connection ID.
- CPU và debug: QUIC ở userspace thường tốn CPU hơn TCP đã được offload phần cứng. tcpdump chỉ thấy UDP mã hóa, nên cần qlog hoặc SSLKEYLOGFILE để giải mã.
- Bảo mật: Dữ liệu 0-RTT có thể bị replay, nên chỉ cho request idempotent đi qua. QUIC chống khuếch đại bằng cách giới hạn server chỉ gửi tối đa 3 lần lượng đã nhận cho tới khi xác thực được client.
# server có quảng bá h3 không
curl -sI https://example.com | grep -i alt-svc
# cần curl build có HTTP/3
curl --http3 -I https://example.com
Kết
TCP chọn chính xác: chờ cho đủ rồi mới giao.
UDP chọn kịp thời: có gì giao nấy.
HTTP/3 cho thấy hai lựa chọn này không loại trừ nhau. QUIC giữ độ chính xác của TCP, nhưng dựng trên UDP để thoát khỏi một Internet đã đóng băng. Với người làm infra, điều cần nắm không phải giao thức nào "tốt hơn", mà là khi mất gói, hệ thống của bạn sẽ báo hiệu qua đâu: qua latency, qua retransmit, hay chỉ im lặng thiếu dữ liệu.
Bình luận (0)
Vui lòng để bình luận.