第2章:TLS/SSL 协议基础回顾

聊优化之前,咱们得先把地基夯实。TLS 会话复用和 OCSP Stapling 说白了都是在标准握手流程上做文章。你不理解正常的握手,就很难明白优化到底省了哪些步骤。

我在好几年前接手过一个电商平台的项目,当时线上偶尔报 SSL 握手超时。我一开始也蒙了,以为是证书配置有问题。后来抓包一看——嗯,原来是每次请求都走完整握手,客户端和服务器来回折腾了好几个 RTT,网络稍一抖动就超时了。那是我第一次深刻体会到,理解握手细节有多重要。

2.1 握手协议的核心流程

咱们先回顾最经典的 TLS 1.2 完整握手。一个连接建立,客户端和服务器要交换不少东西。

  1. ClientHello:客户端发个问候,带上支持的 TLS 版本、密码套件列表、一个随机数。
  2. ServerHello:服务器选好版本和套件,也发回一个随机数。
  3. Certificate:服务器把自己的证书链发给客户端。我建议你多关注这个步骤,证书的尺寸有时候能拖慢整个流程。
  4. ServerKeyExchange:根据密码套件不同,可能需要额外的密钥交换参数。
  5. ServerHelloDone:服务器表示“我说完了”。
  6. ClientKeyExchange:客户端生成预主密钥,用服务器的公钥加密后发过去。
  7. ChangeCipherSpec + Finished:双方切换加密方式,并验证握手消息的完整性。

这个流程下来,通常需要 2 个 RTT(往返时间)。如果客户端和服务器物理距离很远,比如跨国访问,光握手就得几百毫秒。

核心痛点: 完整握手的 2-RTT 延迟 + 证书传输开销,是后续优化的主要目标。

到了 TLS 1.3,握手被压缩到了 1 个 RTT。我记得第一次在实验室验证 1-RTT 流程时,确实觉得很爽。它把 ServerHello 和证书合并了,真正的密钥交换也提前了。但即便如此,在弱网环境下,1 个 RTT 也很宝贵。

2.2 会话复用:省掉重复握手

你想想看,每次重新连接都走一遍完整握手,是不是很浪费?尤其是移动端 APP,频繁切换网络、断线重连,简直就是噩梦。

会话复用就是解决这个问题的。它允许客户端和服务器在后续连接中,复用之前协商好的会话参数。主要有两种机制:

  • Session ID(会话标识):服务器端保存会话状态,客户端保存一个 ID。重连时发这个 ID,服务器查到缓存就直接恢复。
  • Session Ticket(会话票据):服务器把会话状态加密成票据发给客户端,由客户端保存。重连时客户端提交票据,服务器解密后恢复。
对比维度 Session ID Session Ticket
状态存储 服务器内存(有上限) 客户端(票据本身)
负载均衡场景 需要会话亲和性(Sticky Session) 无状态,天然支持
安全性 ID 泄露需要绑定服务器 票据被加密,但密钥需妥善保管
恢复速度 0-RTT(服务端同步) 0-RTT(服务端需解密)

我曾经在生产环境遇到过一个问题:用了 Session ID,但服务器是集群部署,第一次请求落在 A 机器,第二次请求落在 B 机器,结果无法恢复会话,又走了一次完整握手。后来切到 Session Ticket,配合统一的票据加密密钥,彻底解决了。这就是我为什么偏爱 Session Ticket 的原因。

避坑提示: Session Ticket 的加密密钥建议定期轮换,同时保留旧密钥一小段时间用于解密存量票据。我一般设定轮换周期为 24 小时,保留窗口 1 小时。实现上可以用 Nginx 的 ssl_session_ticket_key 指令。

但会话复用也有缺点。它只能复用连接,不能解决证书验证的开销。每次连接,客户端依然需要验证服务器的证书链。而证书链验证包含 CRL 或 OCSP 查询,这又是一个潜在的延迟点。

2.3 OCSP:在线证书状态查询

证书的有效期很长,但中间可能被吊销。浏览器必须确认证书当前有效。传统的做法是 CRL(证书吊销列表),但列表越来越大,下载太慢。于是有了 OCSP。

OCSP 协议很简单:客户端直接问 CA 的 OCSP 响应器,“这个证书还活着吗?”响应器回答“好”、“坏”或“未知”。

但我得说,传统 OCSP 有一个致命伤——它引入了额外的网络请求。浏览器需要去连接 OCSP 服务器,这又是一个 RTT。如果 OCSP 服务器挂了,浏览器可能还要超时等待,或者采用“软失败”策略(即允许连接,但风险自担)。

现实问题: 很多 OCSP 响应器存在可用性问题。我在前公司分析过线上数据,大约 0.3% 的 OCSP 请求会超时。0.3% 看起来不多,但每天几亿次请求,就有上百万次额外延迟。用户感知到就是“这个网站打开好慢”。

更糟糕的是,OCSP 请求的响应可能包含隐私信息——CA 能知道“哪个 IP 在什么时间查询了哪个证书”。所以有些隐私敏感的部署会考虑 OCSP Stapling 来解决。

2.4 OCSP Stapling:把状态塞进握手

OCSP Stapling 的思路很简单:不要客户端去问 CA,而是服务器自己定期去问 CA,拿回一个带时间戳的 OCSP 响应(称为“钉书钉”)。然后在 TLS 握手阶段(Certificate 之后),服务器把这个响应一起发给客户端。

客户端收到后,只需要验证这个响应上的 CA 签名和时效性,就无需再发起额外的网络请求。整个过程在握手内部完成,不增加 RTT。

我举个例子理解它:完整握手像你自己跑到政务大厅办证明,OCSP 是又跑了一趟另一个窗口。OCSP Stapling 则是单位统一给你办好,你直接出示就行。

服务器拿回 OCSP 响应需要缓存,不能每次客户端请求都去 CA 刷新。一般缓存周期是 4 小时到 7 天,由 CA 的 nextUpdate 字段决定。

2.5 本章知识图谱

下面这张图展示了 TLS 完整握手与会话复用、OCSP Stapling 的整体关系。你可以看到优化点主要集中在哪里。

TLS 握手与优化技术全景图 客户端 服务器 1. 完整握手 (1-2 RTT) 2. 会话复用 (0-RTT 恢复) Session ID / Session Ticket 3. OCSP 查询 客户端 → CA(额外RTT) 4. OCSP Stapling 服务器缓存 → 握手内推送(0 RTT) 优化效果对比 完整握手: 2 RTT + 证书验证 → 约 300-600ms 延迟 会话复用 + OCSP Stapling: 0 RTT → 约 10-50ms 延迟 优化空间: 延迟降低 80%-95%

从这张图可以看到,会话复用省掉了握手阶段的 1-2 个 RTT,OCSP Stapling 省掉了证书验证阶段的 1 个额外 RTT。两者叠加起来,效果非常可观。

2.6 为什么必须关注这些基础

我见过不少团队直接跳过基础,上来就调 Nginx 参数。结果调了半天,性能提升有限。为什么?因为你不知道瓶颈到底在哪。

比如你在 Nginx 里开启了 ssl_session_cache,但用的是 Session ID,而后端又是多台服务器且没有配置会话亲和性。那缓存几乎不会被命中,等于白开。

再比如你配置了 OCSP Stapling,但没有处理 CA 的更新策略,导致钉书钉过期,客户端再次回退到完整查询。这种配置错误在真实环境中非常常见。

所以,理解协议层面的基础,是后续配置和调优的前提。你不仅要知道怎么做,还要知道为什么这么做。

实用建议: 当你开始优化前,先抓一个 TLS 握手的完整包 (Wireshark 或 tshark),看看当前花了几个 RTT,证书多大,是否走了 OCSP 请求。建立基线数据,再开始调整。