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ề

Ảnh chụp đoạn mã nền tối bắt tay TLS 1.3 một lượt đi-về rồi mọi thứ đều mã hoá với Go 1.23 crypto tls OpenSSL 3.0.16 server dist-lab 9443 1.3 9444 1.2 9445 openssl client go-lab netem 50 ms, record TLS 5 byte header cộng payload client Go bọc net.Conn để đọc từng cái gồm byte 0 type 20 ChangeCipherSpec 21 Alert 22 Handshake 23 ApplicationData, 1 tới 2 version 0303 bằng TLS 1.2 kể cả trong TLS 1.3 vì middlebox phiên bản thật nằm ở extension, 3 tới 4 length nhỏ hơn hoặc bằng 16 KB, TLS 1.3 sau ServerHello mọi record đều là 23 ApplicationData dù bên trong là Certificate Finished, TLS 1.2 Certificate ServerKeyExchange ServerHelloDone đi plaintext và thêm một lượt đi-về trước dữ liệu, ba kịch bản đếm lượt đi-về RTT thay vì mili giây gồm 1.3 đầy đủ ClientHello key_share ServerHello cộng EncryptedExt Cert CertVerify Finished rồi Finished cộng dữ liệu ngay bằng 1 RTT bắt tay server gửi thêm NewSessionTicket sau, 1.3 resumption ClientHello pre_shared_key ServerHello cộng EE Finished không Cert không chữ ký vẫn 1 RTT tiết kiệm CPU và byte không tiết kiệm RTT, 1.3 0-RTT ClientHello cộng dữ liệu early ServerHello server đã có request bằng 0 RTT Go crypto tls không hỗ trợ trên TCP đo bằng openssl early_data cái giá replay được, 1.2 đầy đủ ClientHello SH Certificate SKE Done CKE CCS Finished CCS Finished bằng 2 RTT, đo gì A Go client ghi từng record chiều loại độ dài thời điểm tổng byte gửi nhận 1.3 đầy đủ 1.3 ticket 1.2, B netem 50 ms chiều server về client thời gian tới phản hồi đầu cho 1.3 1.3 ticket 1.2 đếm RTT bằng ms, C openssl full rồi resumed cộng early_data cùng netem server nhận request sớm hơn bao nhiêu nó nói gì

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

Ảnh chụp output thật nền tối 1.3 172 ms 1.2 222 ms 0-RTT server có request sớm 50,7 ms với Go 1.23 client server chứng chỉ ECDSA P-256 tự ký netem 50 ms chiều server về client openssl 3.0.16 cho 0-RTT, A TLS 1.3 đầy đủ từng record client Go nhìn thấy loopback ms kể từ connect gồm 0,4 Handshake ClientHello 1474 B key_share X25519 cộng Kyber768 hybrid Go 1.23 bật mặc định nên to gấp 5 lần xưa, 0,7 Handshake ServerHello 1210 B key_share Kyber trả về khoảng 1 KB, 0,7 CCS 1 B AppData 23 B EncryptedExtensions 438 B Certificate 97 B CertificateVerify 53 B Finished, 0,8 AppData 139 B NewSessionTicket server gửi ticket ngay sau Finished của nó, 0,9 CCS 1 B AppData 53 B Finished AppData 35 B request cùng flight với Finished, 1,0 AppData 74 B phản hồi, tổng gửi 1583 B nhận 2075 B 12 record 1 RTT bắt tay client gửi dữ liệu ngay sau khi nhận ServerHello flight, B bốn kiểu bắt tay cùng netem 50 ms 10 lần p50 phản hồi đầu tiên kể từ connect gồm TLS 1.3 đầy đủ 12 record nhận 2075 B Certificate có 438 B phản hồi p50 172 ms số lần chờ server 3 SYN-ACK SH resp, TLS 1.3 ticket 10 record 1536 B không Certificate 173 ms 3 cùng RTT bớt 539 B cộng chữ ký, TLS 1.2 đầy đủ 13 record 893 B có plaintext 222 ms 4 cộng Finished server cộng 1 RTT bằng cộng 50 ms, TLS 1.2 ticket 9 record 373 B không 171 ms 3 1.2 resumed cũng 1 RTT, mốc loopback không netem 0,6 ms cho mọi kiểu khác biệt giữa chúng là RTT không phải CPU, C 0-RTT bằng openssl s_server early_data netem 50 ms đo lúc server nhận được request gồm full sess_out server in GET early sau 131,7 ms kể từ lúc client bắt đầu SYN-ACK 50 cộng ServerHello 50, resumed cộng early_data server in Early data received GET early sau 81,0 ms sớm hơn 50,7 ms bằng đúng 1 RTT, client Reused TLSv1.3 Early data was accepted Go 1.23 crypto tls không có early data trên TCP chỉ QUIC, điều kiện lần đầu phải chờ NewSessionTicket mới có sess.pem client đóng ngay sau request thì không có gì để resume, nói thẳng netem đặt ở egress server nên một lần chờ server bằng 50 ms mạng thật thì RTT là RTT chứng chỉ ECDSA tự ký 438 B chuỗi thật với RSA và intermediate là 2 tới 4 KB phần 3 ClientHello 1,47 KB là của Go 1.23 với Kyber curl openssl 3.0 gửi khoảng 300 B Kyber làm bắt tay to hơn nhưng vẫn 1 RTT

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ề

  1. 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.
  2. 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ễ.
  3. 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

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.