"Dùng bể kết nối" là lời khuyên ai cũng biết. Bài này đo xem nó đáng bao nhiêu, và đo bằng đơn vị duy nhất mang đi được: vòng khứ hồi.

Chi phí bắt tay TCP theo từng chặng

Ba phép đo, hai mức độ trễ

Hai container. Đo bằng cách lặp 200 lần và lấy trung vị.

Phép đo Cùng máy Thêm 20 ms độ trễ
Bắt tay TCP (connect + close) 762,3 µs 21.564 µs = 1 RTT
connect + một vòng gửi nhận 1.836,0 µs 43.492 µs = 2 RTT
Vòng gửi nhận trên kết nối có sẵn 262,2 µs 21.113 µs = 1 RTT

Cột thứ hai là cột đáng đọc. Với độ trễ đủ lớn để lấn át mọi thứ khác, ba con số rơi vào đúng bội số nguyên của vòng khứ hồi.

Mở kết nối mới rồi hỏi một câu tốn hai vòng. Hỏi cùng câu đó trên kết nối đã mở tốn một vòng. Đúng một nửa, chính xác, và lặp lại được.

Vì sao đúng một vòng thêm

Bắt tay ba bước:

may khach  --- SYN ------------>  may chu
may khach  <-------- SYN+ACK ---  may chu
may khach  --- ACK ------------>  may chu     <- luc nay moi gui duoc du lieu

connect() trả về sau bước hai — nghĩa là sau một vòng khứ hồi. Gói ACK ở bước ba đi cùng dữ liệu đầu tiên, nên nó không tốn thêm vòng nào.

Vậy trước khi byte dữ liệu đầu tiên rời máy, bạn đã tiêu một vòng. Rồi câu trả lời cần một vòng nữa. Tổng: hai.

Con số đó có nghĩa gì tuỳ vào bạn ở đâu

Khoảng cách Một vòng khứ hồi Tiết kiệm được nếu dùng lại kết nối
Cùng máy 0,26 ms 0,26 ms
Cùng vùng sẵn sàng ~0,5 ms 0,5 ms
Khác vùng sẵn sàng 1–2 ms 1–2 ms
Khác châu lục 21 ms (đo được) 21 ms mỗi lần gọi

Đây là lý do bể kết nối trông như một tối ưu nhỏ trên máy lập trình viên và là thay đổi lớn nhất trong sản phẩm. Không thể đánh giá nó bằng cách chạy ở local — cột "cùng máy" nói rằng bạn tiết kiệm được một phần tư mili giây.

Một dịch vụ gọi 20 lượt tới cơ sở dữ liệu ở vùng khác cho mỗi yêu cầu: mở kết nối mới mỗi lần cộng thêm 20 × 21 ms = 420 ms, không cần máy chủ chậm chút nào.

Các chặng của một yêu cầu HTTPS mới

Chặng Vòng khứ hồi
Tra DNS 0 (đã cache) hoặc 1
Bắt tay TCP 1 (đo được)
Bắt tay TLS 1.3 1
Gửi yêu cầu, nhận trả lời 1
Tổng 3–4 thay vì 1

Giữ kết nối sống bỏ được ba chặng đầu.

Tôi không đo được chặng TLS — máy chủ thử nghiệm tôi dựng không nhận kết nối và tôi không truy ra được nguyên nhân trong khuôn khổ bài này. Con số "1 RTT" cho TLS 1.3 là theo đặc tả (TLS 1.2 tốn 2 RTT), không phải phép đo của tôi. Ghi rõ để phân biệt với những con số còn lại.

Ba cách bỏ bớt chặng

Giữ kết nối sống. Cách hiệu quả nhất và không có nhược điểm nào đáng kể:

Connection: keep-alive

Trong bể kết nối, hai tham số cần chú ý là kích thước tối thiểu (đừng để bể co về 0 lúc rảnh rồi phải bắt tay lại khi tải quay lại) và tuổi thọ tối đa (đừng giữ kết nối lâu hơn thời gian sống của bản ghi DNS phía sau).

TCP Fast Open — gửi dữ liệu ngay trong gói SYN:

sysctl -w net.ipv4.tcp_fastopen=3     # 1=may khach, 2=may chu, 3=ca hai

Bỏ được một vòng ở lần kết nối thứ hai trở đi (lần đầu phải lấy cookie). Nhược điểm: nhiều tường lửa và bộ cân bằng tải giữa đường bỏ hoặc làm hỏng gói SYN có dữ liệu, và triệu chứng khi đó là kết nối treo chứ không phải lỗi rõ ràng.

QUIC / HTTP/3 — gộp bắt tay vận chuyển và bắt tay mã hoá làm một, còn 1 RTT, và 0 RTT khi nối lại phiên.

Chỗ hàng đợi kết nối tràn

Bắt tay có hai hàng đợi phía máy chủ, và cả hai đều tràn được:

ss -lnt                                    # cot Recv-Q = dang cho, Send-Q = tran backlog
netstat -s | grep -iE 'listen queue|SYNs to LISTEN'
Hàng đợi Đầy thì sao Chỉnh ở đâu
SYN queue (bắt tay chưa xong) Gói SYN bị bỏ, máy khách thử lại sau 1 giây net.ipv4.tcp_max_syn_backlog
Accept queue (đã bắt tay, chờ accept) Kết nối bị bỏ im lặng net.core.somaxconn đối số của listen()

Dòng đầu là nguồn gốc của những độ trễ "đúng 1 giây" hoặc "đúng 3 giây" trong biểu đồ — đó là bộ hẹn giờ thử lại SYN, không phải máy chủ chậm.

Dòng thứ hai đòi hỏi sửa hai chỗ: somaxconn của hệ thống và tham số listen() trong mã nguồn. listen() bị cắt xuống theo somaxconn, nên tăng một bên là vô ích.

Còn TIME_WAIT

Bên nào đóng kết nối trước sẽ giữ trạng thái TIME_WAIT khoảng 60 giây. Với dịch vụ mở nhiều kết nối ngắn tới cùng một đích, số cổng nguồn có hạn (khoảng 28.000 theo mặc định) sẽ cạn:

ss -tan state time-wait | wc -l
sysctl net.ipv4.ip_local_port_range

Cách chữa đúng là dùng lại kết nối — cùng một cách chữa cho mọi thứ trong bài này. net.ipv4.tcp_tw_reuse=1 giúp phía máy khách và an toàn; còn tcp_tw_recycle đã bị gỡ khỏi nhân vì làm hỏng kết nối từ máy khách sau NAT, nên mọi hướng dẫn còn nhắc tới nó đều đã lỗi thời.

Thử ba mươi giây

Đo chi phí bắt tay tới một dịch vụ thật của bạn:

for i in 1 2 3 4 5; do
  curl -o /dev/null -s -w "dns %{time_namelookup}  tcp %{time_connect}  tls %{time_appconnect}  dau-byte %{time_starttransfer}  tong %{time_total}\n" \
    https://dich-vu-cua-ban/health
done

echo "--- so ket noi TIME_WAIT ---"
ss -tan state time-wait 2>/dev/null | wc -l

echo "--- hang doi accept co tran khong ---"
ss -lnt 2>/dev/null | awk 'NR==1 || $2>0 || $3>0'

time_connect trừ time_namelookup chính là một vòng khứ hồi của bạn. Nhân nó với số kết nối mới mà dịch vụ mở cho mỗi yêu cầu — con số đó là thời gian bạn lấy lại được, miễn phí, chỉ bằng việc dùng lại kết nối.

Phần sau: Nagle và delayed ACK — đo khi hai cái gặp nhau.