一、为什么 TLS 握手值得你花一篇文章的时间去搞懂
先看一组实测数据。同样一个网站,从北京电信发起测速:
| 阶段 | 优化前 | 优化后 |
|---|---|---|
| DNS Lookup | 32ms | 32ms |
| TCP Connect | 48ms | 48ms |
| TLS Handshake | 890ms | 145ms |
| TTFB | 180ms | 180ms |
| Content Download | 92ms | 92ms |
| 总耗时 | 1242ms | 497ms |
什么都没改,只调了 TLS 配置,总耗时直接砍掉 60%。这种"高 ROI"的优化点,在性能优化里其实非常少见。
而且 TLS 握手有一个让人头疼的特性:它每建立一个新连接都要重来一次。用户每点开一个新页面、每打开一个新标签、CDN 每对接一个新源站——只要冷启动了,TLS 就得重新握手。所以这一段的优化,是真正的"每个用户都受益"。
下面我们从底层协议开始拆。
二、TLS 握手到底在做什么?
TLS 握手要解决三件事:
- 身份认证——客户端怎么确认服务器真的是它声称的那个?(防止中间人)
- 密钥协商——双方在不安全的网络上,怎么协商出一把只有彼此知道的加密钥匙?
- 参数协商——用什么版本、什么加密算法、什么压缩方式?
解决这三件事,TLS 1.2 需要 2 个 RTT,TLS 1.3 只需要 1 个 RTT。这就是为什么升级版本是最容易的性能提升。
TLS 1.2 完整握手(4 步)
客户端 服务器
│ │
│ ──── ClientHello ──────────────────────▶ │ ← RTT #1 开始
│ (支持的 TLS 版本、加密套件、随机数) │
│ │
│ ◀──── ServerHello ────────────────────── │
│ Certificate │
│ ServerKeyExchange │
│ ServerHelloDone │ ← RTT #1 结束
│ │
│ ──── ClientKeyExchange ────────────────▶ │ ← RTT #2 开始
│ ChangeCipherSpec │
│ Finished │
│ │
│ ◀──── ChangeCipherSpec ────────────────── │
│ Finished │ ← RTT #2 结束
│ │
│ 开始传输加密数据 │
整个过程需要 2 次完整的网络往返。假设 RTT 是 50ms,光 TLS 就要 100ms 起步。
TLS 1.3 完整握手(2 步)
客户端 服务器
│ │
│ ──── ClientHello ──────────────────────▶ │ ← RTT #1 开始
│ (key_share, 直接带上密钥协商参数) │
│ │
│ ◀──── ServerHello ────────────────────── │
│ (EncryptedExtensions) │
│ Certificate │
│ CertificateVerify │
│ Finished │ ← RTT #1 结束
│ │
│ ──── Finished ─────────────────────────▶ │
│ 开始传输加密数据 (可以同包发送) │
TLS 1.3 把密钥协商的关键信息合并到了第一次往返里,少了一整个 RTT。同样 50ms 的 RTT,TLS 1.3 只要 50ms。
更进一步,TLS 1.3 还支持 0-RTT 模式:如果客户端之前连接过这个服务器,下次可以直接在 ClientHello 里塞加密数据,实现"零等待"。这个后面会讲。
三、让 TLS 变慢的 5 个常见原因
原因 1:服务器还在用 TLS 1.2 甚至更老的版本
这是最常见、也是最容易解决的问题。
检测方法:
openssl s_client -connect www.example.com:443 -servername www.example.com < /dev/null 2>/dev/null | grep "Protocol"
# 输出示例:
# Protocol : TLSv1.2 ← 还在 1.2
# Protocol : TLSv1.3 ← 已经升级
或者用浏览器开发者工具查看:F12 → Security → Connection。
原因 2:OCSP 验证阻塞了握手
这个是隐藏最深的性能杀手。
浏览器在验证服务器证书时,需要确认这张证书没有被 CA 提前吊销(比如私钥泄露了)。验证方式叫 OCSP(Online Certificate Status Protocol)。
默认情况下,浏览器会自己向证书颁发机构(CA)的 OCSP 服务器发起一次查询。这就有几个问题:
- CA 的 OCSP 服务器大多在海外,单次查询 RTT 可能就 200~500ms
- CA 服务器偶尔会卡顿,最坏情况下要超时几秒
- 这次查询是串行在 TLS 握手之后的,用户感知就是"HTTPS 慢"
解决方案:OCSP Stapling。让服务器自己提前定期查询 OCSP 状态,把签名后的结果"装订(staple)"到 TLS 握手响应里。浏览器拿到证书的同时就拿到了吊销状态证明,省掉了那次查询。
原因 3:没启用会话恢复
同一个用户多次访问同一个网站时,理论上不需要每次都完整握手。TLS 提供了两种会话恢复机制:
- Session ID(TLS 1.2 老方案):服务器在内存里维护一个会话状态表,客户端下次带上 ID 就能恢复
- Session Ticket(推荐):服务器把会话状态加密后发给客户端,下次客户端带回来,服务器解密恢复——好处是服务器无状态,扩容方便
会话恢复后,TLS 1.2 握手从 2 RTT 降到 1 RTT,TLS 1.3 从 1 RTT 降到 0 RTT。
这个功能默认是不开的,需要手动配置。
原因 4:证书链过长或包含不必要的中间证书
服务器在握手时会把整个证书链发给客户端。如果链路里有 3~4 张中间证书,每张都是 1~2KB,加起来就要传几 KB 的数据。
更糟的是,如果服务器只发了叶子证书没发中间证书,客户端就要自己去取,又是几百毫秒。
检测方法:
echo | openssl s_client -showcerts -connect www.example.com:443 -servername www.example.com 2>/dev/null | grep -c "BEGIN CERTIFICATE"
# 输出数字 = 证书链长度,>= 3 就值得优化
原因 5:密码套件选择不当
典型问题:
- 用 RSA 做密钥交换(应该用 ECDHE)——慢,且不支持前向安全
- 启用了 CBC 模式的加密套件——比 GCM 慢,且有历史漏洞
- 支持了过老的 SHA1、3DES——浏览器要花更多时间协商
四、可直接复制的 Nginx 优化配置
下面这份是经过实战检验的 Nginx TLS 配置模板。直接用在 server 块里即可:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.example.com;
# ============ 证书路径 ============
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# ============ 协议版本 ============
# 只保留 TLS 1.2 和 1.3,老版本一律砍掉
ssl_protocols TLSv1.2 TLSv1.3;
# ============ 密码套件 ============
# TLS 1.3 用默认的就好,TLS 1.2 推荐 ECDHE + GCM
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
# ============ 会话恢复 ============
ssl_session_cache shared:SSL:50m; # 50MB 内存,约能存 20 万个会话
ssl_session_timeout 1d; # 会话保留 1 天
ssl_session_tickets on; # 启用 Session Ticket
# ============ OCSP Stapling ============
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/fullchain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s; # 用于查询 OCSP 服务器的 DNS
resolver_timeout 5s;
# ============ HSTS(可选但推荐) ============
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# ============ 其他业务配置 ============
# ...
}
配置应用后,强烈建议用下面这些命令验证:
# 检查 TLS 1.3 是否生效
openssl s_client -connect www.example.com:443 -tls1_3 < /dev/null 2>/dev/null | grep "Protocol"
# 检查 OCSP Stapling 是否生效(看到 "OCSP Response Status: successful" 就对了)
openssl s_client -connect www.example.com:443 -status < /dev/null 2>/dev/null | grep -A 5 "OCSP response"
# 检查会话恢复是否生效(第二次连接看到 "Reused, TLSv1.3" 就对了)
openssl s_client -connect www.example.com:443 -reconnect < /dev/null 2>/dev/null | grep -E "(New|Reused)"
五、进阶优化:证书本身的优化
ECDSA 证书:又快又小的选择
大多数人申请的是 RSA 证书(2048 位)。但其实现在主流 CA 都支持 ECDSA 证书(P-256 曲线),它的优势很明显:
| 对比项 | RSA 2048 | ECDSA P-256 |
|---|---|---|
| 证书大小 | ~1.2 KB | ~400 B |
| 握手时签名计算 | 慢 | 快 2~4 倍 |
| 等效安全强度 | 112 位 | 128 位 |
| 浏览器支持 | 所有 | 所有现代浏览器 |
Let's Encrypt、ZeroSSL、阿里云、腾讯云的免费证书都支持 ECDSA,申请时选 ECC 即可。
稳妥起见,可以同时部署 RSA + ECDSA 双证书,Nginx 自动根据客户端能力选择:
ssl_certificate /etc/nginx/ssl/ecdsa-fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/ecdsa-privkey.pem;
ssl_certificate /etc/nginx/ssl/rsa-fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/rsa-privkey.pem;
精简证书链
检查你的 fullchain.pem 里有几张证书:
grep -c "BEGIN CERTIFICATE" /etc/nginx/ssl/fullchain.pem
理想值是 2(你的叶子证书 + 一张中间证书)。如果是 3 张甚至更多,看看能否换一个证书链更短的 CA。
注意:千万不要把根证书也塞进去——根证书早就预装在浏览器里了,发送它纯粹是浪费流量。
六、HTTP/3 与 0-RTT:把握手优化到极致
HTTP/3 (QUIC) 的优势
HTTP/3 基于 UDP 上的 QUIC 协议,把 TLS 1.3 握手和传输层握手合并了。也就是说:
- HTTP/2 over TLS 1.3:建连要 2 RTT(TCP 1 RTT + TLS 1 RTT)
- HTTP/3 over QUIC:建连只要 1 RTT
- HTTP/3 + 0-RTT(已建立过会话):0 RTT,请求和握手同时发送
对于跨国访问场景(RTT 200ms+),这个差异极其明显。
Nginx 启用 HTTP/3
Nginx 1.25 起原生支持 HTTP/3,配置示例:
server {
listen 443 ssl;
listen 443 quic reuseport; # 启用 QUIC(UDP 443)
listen [::]:443 quic reuseport;
http3 on;
http3_hq on;
# 通知浏览器升级到 HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400';
# 其他 TLS 配置同上
ssl_protocols TLSv1.3; # HTTP/3 要求 TLS 1.3
# ...
}
别忘了在防火墙里放行 UDP 443 端口。
关于 0-RTT 的安全注意事项
0-RTT 虽然快,但有一个安全权衡:它对重放攻击没有完美的防护。如果攻击者抓到了 0-RTT 的数据包,可以重新发送一次。
所以原则上:
- 幂等的 GET 请求可以走 0-RTT
- 修改数据的 POST / PUT / DELETE 不要走 0-RTT
Nginx 配置:
ssl_early_data on; # 启用 0-RTT
proxy_set_header Early-Data $ssl_early_data; # 把标记传给后端,后端按需拒绝非幂等请求
七、实战案例:把 TLS 从 890ms 砍到 145ms
背景:某公司后台系统,用户反馈 HTTPS 打开慢。从北京电信发起 多节点网站测速,TLS 握手阶段平均 800~900ms。
第一步:定位症状
openssl s_client -connect admin.example.com:443 < /dev/null 2>&1 | grep -E "(Protocol|Cipher|OCSP)"
# 输出:
# Protocol : TLSv1.2 ← 还在 1.2
# Cipher : ECDHE-RSA-AES256-SHA384
# OCSP response: no response sent ← 没开 Stapling
两个明确问题:
- 还在 TLS 1.2,多用 1 个 RTT
- 没开 OCSP Stapling,浏览器要自己去查 CA
第二步:抓握手过程定位 OCSP 阻塞
用 Wireshark 抓包,看到浏览器在 TLS 握手完成后,立即向 ocsp.digicert.com 发了一次 HTTP 查询,整个过程多用了 480ms。
第三步:修改 Nginx 配置
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
- ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_session_cache shared:SSL:50m;
+ ssl_session_timeout 1d;
+ ssl_session_tickets on;
+ ssl_stapling on;
+ ssl_stapling_verify on;
+ ssl_trusted_certificate /etc/nginx/ssl/fullchain.pem;
+ resolver 1.1.1.1 8.8.8.8 valid=300s;
nginx -s reload 之后重测:
| 测试点 | 优化前 TLS 耗时 | 优化后 TLS 耗时 | 下降幅度 |
|---|---|---|---|
| 北京电信 | 890ms | 145ms | -84% |
| 广州移动 | 820ms | 165ms | -80% |
| 上海联通 | 760ms | 120ms | -84% |
| 新加坡(海外) | 1100ms | 220ms | -80% |
整个优化的工作量:修改 10 行配置,重载一次 Nginx。但每个用户每次新建连接都能省 600~900ms。
八、TLS 性能优化 8 条军规(速查表)
| 序号 | 优化项 | 预期收益 | 难度 |
|---|---|---|---|
| 1 | 启用 TLS 1.3 | 少 1 个 RTT(50~200ms) | ★ |
| 2 | 启用 OCSP Stapling | 免去 OCSP 查询(200~500ms) | ★ |
| 3 | 启用 Session Resumption | 重复访问省 1 个 RTT | ★ |
| 4 | 使用 ECDHE 而非 RSA 密钥交换 | 支持前向安全 + 略快 | ★ |
| 5 | 使用 ECDSA 证书 | 证书更小、签名更快 | ★★ |
| 6 | 精简证书链到 2 张 | 减少握手数据量 | ★★ |
| 7 | 启用 HTTP/2 | 多路复用,避免连接开销 | ★ |
| 8 | 启用 HTTP/3 + 0-RTT | 极限场景下 0 RTT 建连 | ★★★ |
难度 ★ 的优化基本上就是改几行配置。难度 ★★ 的可能要换证书、调整证书链。难度 ★★★ 的要升级 Nginx、调整防火墙。
建议的优化顺序:先把所有 ★ 全部启用,再考虑 ★★,最后才上 ★★★——前面三条已经能拿到 80% 的收益。
九、总结
HTTPS 慢从来不是"加密的代价",而是"配置不到位的代价"。一次正常的 TLS 握手应该在 100~200ms 之间完成,如果你的网站测速里 TLS 段超过 500ms,几乎可以肯定是上面 5 个原因里的一个或几个。
排查路径很简单:
- 用 多节点网站测速 看 TLS 阶段耗时
- 用
openssl s_client抓握手细节,确认 TLS 版本、OCSP、密码套件 - 对照本文八条军规,逐条修复
- 修复后再次测速验证
这个流程走一遍,大部分情况下能把网站的 HTTPS 性能提升 30~60%——而你只动了几行配置。