Phần 27 đã ghi bắt tay TLS là "1 vòng khứ hồi" và nói rõ đó là con số theo đặc tả chứ không phải phép đo. Bài này đo, và con số thật khác xa.

Chi phí TLS: bắt tay, mỗi vòng, và thông lượng

Bảng

Cùng máy chủ, cùng chương trình, chỉ khác cổng — một cổng TLS 1.3 (TLS_AES_256_GCM_SHA384), một cổng TCP trần:

Phép đo TCP trần TLS 1.3 Chênh
Kết nối mới + một vòng gửi nhận 261,7 µs 1.256,0 µs 4,8×
Vòng gửi nhận trên kết nối có sẵn 29,7 µs 47,9 µs 1,6×
Thông lượng (gửi liên tục) 8.828–12.125 MB/s 2.425–2.435 MB/s 4,3×

Hai dòng đầu kể toàn bộ câu chuyện: bắt tay thêm 995 µs, mỗi vòng sau đó thêm 18 µs.

Chia ra: phải gửi nhận hơn 55 lần trên cùng một kết nối thì tổng chi phí mã hoá mới bằng chi phí một lần bắt tay.

Đó là lý do dùng lại kết nối quan trọng với HTTPS hơn nhiều so với HTTP thuần. Kết luận của phần 27 — bể kết nối tiết kiệm một vòng khứ hồi — với TLS trở thành: bể kết nối tiết kiệm một vòng khứ hồi cộng một mili giây CPU.

995 µs đó là CPU, không phải mạng

Đây là điều tôi muốn nhấn mạnh, vì cách nói thông thường ("TLS 1.3 tốn thêm một vòng khứ hồi") khiến người ta nghĩ đây là chi phí mạng.

Suy ra từ chính hai dòng đầu bảng: một vòng khứ hồi trên đường truyền này là khoảng 30 µs (dòng thứ hai, TCP trần). Nếu bắt tay TLS chỉ tốn thêm một vòng, nó phải thêm khoảng 30 µs.

Nó thêm 995 µs. Gấp 33 lần.

openssl speed trên cùng máy giải thích phần chênh:

RSA-2048 ký      346 µs mỗi lần    (phía máy chủ)
RSA-2048 kiểm      9 µs mỗi lần    (phía máy khách)
AES-256-GCM    7,4 GB/s ở khối 16 KB

Chữ ký RSA-2048 một mình đã chiếm hơn một phần ba phần thêm. Phần còn lại là trao đổi khoá, dựng và phân tích thông điệp bắt tay, kiểm tra chứng chỉ.

Hai hệ quả thực dụng:

Máy chủ TLS bị giới hạn bởi CPU, không bởi băng thông. Với 346 µs mỗi chữ ký, một nhân xử lý được khoảng 2.900 lần bắt tay mỗi giây. Một đợt tải mà mọi máy khách đều mở kết nối mới sẽ làm cạn CPU trước khi chạm giới hạn mạng.

Đổi sang khoá ECDSA giúp rất nhiều. ECDSA P-256 ký nhanh hơn RSA-2048 khoảng một bậc độ lớn. Tôi không đo số cụ thể trong bài này, nhưng chênh lệch nằm ở đúng chỗ đắt nhất.

Mã hoá không phải chỗ tắc nghẽn

Thông lượng TLS đo được là 2,4 GB/s, thấp hơn TCP trần 4,3 lần. Trực giác đầu tiên là "AES tốn 4 lần".

openssl speed bác bỏ điều đó: AES-256-GCM chạy 7,4 GB/s ở khối 16 KB trên chính máy này — nhanh gấp ba lần thông lượng TLS tôi đo được.

Vậy chỗ mất không phải mã hoá. Nó là các lần chép thêm và việc đóng gói từng bản ghi TLS (mỗi bản ghi tối đa 16 KB, mỗi bản ghi một tiêu đề và một thẻ xác thực), cộng với chi phí của lớp TLS trong Python.

Điều đáng chú ý: TLS ổn định hơn TCP trần. Ba lần đo TLS cho 2.425,1 / 2.426,3 / 2.434,5 MB/s — dao động 0,4%. TCP trần cho 8.827,8 / 11.451,3 / 12.125,1 — dao động 37%. TLS bị giới hạn bởi một chi phí cố định trên mỗi byte, nên nó đều; TCP trần chạm trần bộ nhớ và thay đổi theo trạng thái máy.

Với CPU có AES-NI — mọi CPU máy chủ từ 2010 — mã hoá gần như miễn phí. Kiểm tra:

grep -o -m1 -E 'aes|vaes' /proc/cpuinfo | head -2
openssl speed -evp aes-256-gcm 2>/dev/null | tail -2

Nếu con số ở khối 16 KB dưới 1 GB/s thì CPU của bạn không có tăng tốc phần cứng, và lúc đó mã hoá mới là chỗ đáng lo.

Nối lại phiên: gần như không giúp gì ở đây

Trung vị
Bắt tay đầy đủ 1.248,6 µs
Nối lại phiên 1.135,9 µs

Chỉ bớt 9%.

Nối lại phiên tiết kiệm một vòng khứ hồi. Vòng khứ hồi ở đây là 30 µs, nên gần như không thấy gì — phần đắt là CPU, và nối lại phiên vẫn phải làm phần lớn công việc mã hoá đối xứng.

Qua Internet thì hoàn toàn khác: với vòng khứ hồi 50 ms, nối lại phiên bỏ được 50 ms trong khi phần CPU không đổi. Đó là chỗ 0-RTT của TLS 1.3 trở thành thay đổi lớn.

Nói cách khác: giá trị của nối lại phiên tỷ lệ với độ trễ, còn giá trị của dùng lại kết nối thì cộng thêm cả phần CPU. Dùng lại kết nối luôn thắng.

Chỗ tôi không đo được

Tôi định so TLS_AES_256_GCM_SHA384 với TLS_CHACHA20_POLY1305_SHA256 — ChaCha20 là lựa chọn cho CPU không có AES-NI. Nhưng set_ciphers() của Python không điều khiển bộ mã của TLS 1.3 (chúng nằm ở một danh sách riêng), nên cả hai lần chạy đều dùng AES-256-GCM và tôi không có số liệu để so.

Ghi lại thay vì đăng hai con số thực ra đo cùng một thứ.

Ba việc đáng làm

Giữ kết nối sống. Vẫn là điều quan trọng nhất, và với TLS thì giá trị gấp đôi so với HTTP thuần.

Dùng chứng chỉ ECDSA thay vì RSA-2048 nếu máy khách của bạn hỗ trợ. Nó tấn công thẳng vào 346 µs đắt nhất.

Bật nối lại phiên — mặc định đã bật ở nginx và phần lớn máy chủ, nhưng kiểm tra rằng bộ đệm phiên đủ lớn và được chia sẻ giữa các tiến trình worker:

ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets on;

Với nhiều máy chủ sau một bộ cân bằng tải, khoá vé phiên phải giống nhau trên mọi máy — nếu không, mỗi lần máy khách rơi vào máy chủ khác là một lần bắt tay đầy đủ, và bạn mất toàn bộ lợi ích mà vẫn nghĩ mình đã bật.

Thử ba mươi giây

Đo chi phí TLS của chính dịch vụ bạn:

for i in 1 2 3; do
  curl -o /dev/null -s -w "tcp %{time_connect}  tls %{time_appconnect}  chenh %{time_appconnect}\n" \
    https://dich-vu-cua-ban/health
done

echo "--- CPU co tang toc AES khong ---"
grep -o -m1 aes /proc/cpuinfo || echo "KHONG co AES-NI"
openssl speed -evp aes-256-gcm -seconds 1 2>/dev/null | tail -1

echo "--- toc do ky cua khoa may chu ---"
openssl speed -seconds 1 rsa2048 2>/dev/null | tail -1

time_appconnect trừ time_connect là chi phí bắt tay TLS của bạn. Nhân nó với số kết nối TLS mới mà hệ thống mở mỗi giây — con số đó là lượng CPU đang dành cho việc bắt tay lại những kết nối lẽ ra nên được giữ.

Phần sau: HTTP — keep-alive, nén, và chỗ thời gian thật sự đi.