Mười một phần WebSocket trước đều kết thúc bằng một dòng "chưa đo TLS". Sê-ri này đo nó. TLS là lớp mà mọi backend đều dùng và hầu như không ai nhìn vào: nó "cứ chạy", cho tới khi độ trễ của trang đầu tiên gấp đôi trên mạng di động, chứng chỉ hết hạn lúc 3 giờ sáng, hay một proxy từ chối bắt tay. Phần 1 bắt đầu từ thứ cơ bản nhất: bắt tay TLS 1.3 trông thế nào từng record một, nó tốn bao nhiêu byte và bao nhiêu lượt đi-về so với TLS 1.2, session ticket tiết kiệm được gì, và 0-RTT thật sự cắt được gì. Mọi con số đo bằng một client Go đọc từng record trên dây, dưới độ trễ mạng 50 ms thêm bằng netem để đếm được RTT bằng mili giây.
Record, flight và lượt đi-về

Hình 1: Cấu trúc record TLS, bốn kiểu bắt tay đếm theo lượt đi-về, và cách đo từng kiểu.
Mọi thứ TLS gửi đều nằm trong record: 5 byte header gồm loại (20 ChangeCipherSpec, 21 Alert, 22 Handshake, 23 ApplicationData), hai byte phiên bản luôn ghi 0303 tức "TLS 1.2" kể cả trong TLS 1.3 vì các middlebox cũ sẽ chặn giá trị lạ, và độ dài tối đa 16 KB. Client Go trong bài bọc net.Conn để cắt từng record ở cả hai chiều, ghi loại, độ dài, thời điểm, rồi mới đưa cho crypto/tls xử lý. Khác biệt lớn nhất giữa hai phiên bản thấy ngay ở cột loại: trong TLS 1.3, mọi record sau ServerHello đều là ApplicationData, kể cả Certificate và Finished, vì chúng đã được mã hoá bằng khoá bắt tay; người nghe lén chỉ thấy độ dài. Trong TLS 1.2, Certificate, ServerKeyExchange và ServerHelloDone đi plaintext, và phải có một lượt đi-về nữa trước khi client được gửi dữ liệu.
Bốn kiểu bắt tay, đếm theo lượt đi-về chứ không phải mili giây, vì mili giây phụ thuộc mạng còn lượt đi-về thì không. TLS 1.3 đầy đủ: client gửi ClientHello kèm key share; server trả ServerHello cùng toàn bộ phần còn lại đã mã hoá; client gửi Finished và dữ liệu ngay trong cùng flight: 1 RTT. TLS 1.3 resumption: client gửi pre-shared key từ ticket lần trước, server không gửi Certificate và không ký gì, nhưng vẫn 1 RTT. TLS 1.3 0-RTT: client gửi dữ liệu ngay cùng ClientHello, server nhận request trước khi bắt tay xong, giá là request đó có thể bị phát lại. TLS 1.2 đầy đủ: 2 RTT vì client phải chờ server Finished. Go 1.23 không hỗ trợ 0-RTT trên TCP, nên phần đó đo bằng openssl s_server -early_data, với một chứng chỉ ECDSA P-256 tự ký dùng chung cho mọi phép đo.
Đo thật: 1.474 byte, 172 ms, 222 ms, 50,7 ms

Hình 2: Từng record của một bắt tay TLS 1.3, bốn kiểu bắt tay dưới netem 50 ms, và thời điểm server nhận request với và không có early data.
| phép đo | kết quả |
|---|---|
| TLS 1.3 đầy đủ: gửi / nhận / record | 1.583 B / 2.075 B / 12 |
| ClientHello của Go 1.23 | 1.474 B (X25519 + Kyber768) |
| TLS 1.3 ticket: nhận / record | 1.536 B / 10, không Certificate |
| TLS 1.2 đầy đủ: nhận / record | 893 B / 13, Certificate plaintext |
| netem 50 ms, phản hồi đầu: 1.3 / 1.3 ticket | 172 / 173 ms |
| netem 50 ms, phản hồi đầu: 1.2 / 1.2 ticket | 222 / 171 ms |
| server nhận request: resumed thường / early data | 131,7 ms / 81,0 ms |
| loopback, mọi kiểu | 0,6 ms |
Một bắt tay TLS 1.3 là 12 record, 1.583 byte đi, 2.075 byte về, và ClientHello to bất ngờ. Record đầu tiên client gửi là ClientHello 1.474 byte. Lý do: Go 1.23 bật mặc định trao đổi khoá lai X25519 cộng Kyber768, và key share của Kyber là hơn 1 KB; ServerHello trả về cũng 1.210 byte vì cùng lý do. Một ClientHello của curl hay OpenSSL 3.0 không Kyber chỉ khoảng 300 byte. Sau ServerHello, mọi thứ là ApplicationData: 23 byte EncryptedExtensions, 438 byte Certificate, 97 byte CertificateVerify, 53 byte Finished, rồi 139 byte NewSessionTicket mà server gửi ngay sau Finished của mình. Client trả Finished 53 byte và request 35 byte đi liền sau trong cùng flight, không chờ gì nữa. Đó là 1 RTT bắt tay nhìn thấy được trên dây.
Dưới 50 ms độ trễ, TLS 1.3 có phản hồi sau 172 ms, TLS 1.2 sau 222 ms: đúng một lượt chờ server chênh lệch. netem đặt ở chiều server về client, nên mỗi lần client phải chờ server trả lời là 50 ms. TLS 1.3 đầy đủ chờ ba lần: SYN-ACK của TCP, flight ServerHello, và phản hồi ứng dụng, cộng chi phí xử lý ra 172 ms. TLS 1.2 đầy đủ chờ bốn lần vì sau khi client gửi ClientKeyExchange và Finished, nó phải nhận Finished của server mới được gửi request: 222 ms. Trên loopback không netem cả hai đều 0,6 ms; khoản 50 ms chênh lệch là RTT thuần tuý, không phải CPU. Với RTT 100 ms của mạng di động, TLS 1.2 trả thêm 100 ms cho mỗi kết nối mới, và với 200 ms thì 200.
Session ticket cắt 539 byte và một chữ ký, không cắt RTT, ở cả 1.3 lẫn 1.2. TLS 1.3 resumption: 10 record thay vì 12, nhận 1.536 byte thay vì 2.075, không có Certificate và CertificateVerify, server không phải ký gì, nhưng phản hồi vẫn 173 ms vì vẫn ba lần chờ. Lợi ích của ticket trong TLS 1.3 là CPU và byte, thứ phần 2 sẽ đo trên 10.000 kết nối, không phải độ trễ. Một kết quả đáng chú ý: TLS 1.2 resumption cũng về 171 ms, tức bắt tay rút gọn của 1.2 cũng là 1 RTT; khoản thua của 1.2 nằm ở kết nối đầu tiên của mỗi client, và ở việc Certificate đi plaintext.
0-RTT đưa request tới server sớm hơn đúng 50,7 ms, và Go không làm được. Đo bằng OpenSSL vì Go 1.23 không hỗ trợ early data trên TCP. Với resumption thường, server in dòng GET /early sau 131,7 ms kể từ lúc client bắt đầu: TCP, rồi ServerHello, rồi request. Với -early_data, request nằm trong cùng gói với ClientHello và server in Early data received: GET /early sau 81,0 ms, kèm phía client Early data was accepted. Chênh lệch 50,7 ms là một lần chờ server, tức một RTT. Cái giá nằm ngoài phép đo: dữ liệu 0-RTT có thể bị phát lại bởi kẻ nghe lén, nên chỉ dành cho request idempotent, và server phải tự bảo vệ. Một chi tiết đo được khác: lần đầu client phải ở lại chờ NewSessionTicket thì mới có gì để resume; phiên bản đầu của script đóng kết nối ngay sau request và không bao giờ có ticket.
Đánh đổi cần cân nhắc
TLS 1.3 là mặc định đúng, và lý do không phải "mới hơn". Một RTT ít hơn cho mọi kết nối mới, Certificate được mã hoá, và không còn các cipher yếu để cấu hình sai. Giữ TLS 1.2 chỉ khi có client cũ thật sự, và khi đó đo xem bao nhiêu phần trăm kết nối đi qua nó, vì mỗi kết nối đó trả thêm một RTT.
Kyber làm ClientHello to gấp 5 nhưng vẫn 1 RTT; chỗ nó gây chuyện là middlebox. 1.474 byte vẫn nằm trong một gói TCP với MTU 1.500 ở biên, nhưng một số thiết bị mạng cũ từ chối ClientHello lớn hoặc bị cắt làm hai gói. Go cho tắt bằng GODEBUG=tlskyber=0 nếu bạn thấy bắt tay thất bại ở một vài mạng; nên đo tỉ lệ lỗi bắt tay theo mạng trước khi quyết định.
0-RTT là tối ưu cho request đầu tiên, không phải cho kết nối. Nó chỉ có nghĩa khi client quay lại trong thời hạn ticket và request đầu tiên idempotent. Phạm vi đo: chứng chỉ tự ký 438 byte, chuỗi thật với RSA và intermediate là 2 đến 4 KB và có thể vượt một gói, chủ đề của phần 3; netem một chiều nên "một lần chờ" là 50 ms chứ không phải RTT 100 ms như cách đặt thông thường.
Ba ý mang về
- Bắt tay TLS 1.3 là 1 RTT và 12 record, mọi thứ sau ServerHello đều mã hoá: client gửi request cùng flight với Finished. TLS 1.2 cần thêm một lượt chờ server, đo được là thêm đúng 50 ms trên mạng 50 ms.
- Session ticket tiết kiệm byte và chữ ký, không tiết kiệm RTT: 1.536 thay vì 2.075 byte, không Certificate, vẫn 173 ms. Lợi ích của nó nằm ở CPU server khi có nhiều kết nối, không ở độ trễ.
- 0-RTT đưa request đến sớm một RTT, Go không hỗ trợ trên TCP, và client phải chờ ticket lần đầu: 81,0 thay vì 131,7 ms, đổi lấy nguy cơ phát lại, nên chỉ cho request idempotent.
Nguồn
- RFC 8446 — TLS 1.3, mục 2 Protocol Overview (1-RTT, resumption, 0-RTT) và 5.1 Record Layer: https://www.rfc-editor.org/rfc/rfc8446#section-2
- RFC 5246 — TLS 1.2, mục 7.3 Handshake Protocol Overview: https://www.rfc-editor.org/rfc/rfc5246#section-7.3
- Go 1.23 Release Notes — X25519Kyber768Draft00 bật mặc định,
GODEBUG=tlskyber: https://go.dev/doc/go1.23#cryptotlspkgcryptotls - Go
crypto/tls—ClientSessionCache,ConnectionState.DidResume: https://pkg.go.dev/crypto/tls - OpenSSL —
s_client -early_data,s_server -early_data: https://docs.openssl.org/3.0/man1/openssl-s_client/ - Cloudflare — Introducing Zero Round Trip Time Resumption (rủi ro replay của 0-RTT): https://blog.cloudflare.com/introducing-0-rtt/
Phần sau đặt TLS lên 10.000 kết nối: mỗi kết nối TLS tốn thêm bao nhiêu RAM so với plaintext, một lõi chịu được bao nhiêu bắt tay mỗi giây, và session ticket cắt được bao nhiêu CPU khi 10.000 client cùng quay lại.