HTTPS 握手慢 380ms 排查:OCSP Stapling 救场的全过程

用户反馈网站慢 300ms,DevTools 看是 SSL 握手阶段。本文复盘 OCSP 在线校验阻塞握手的真相、Nginx 启用 OCSP Stapling 的完整配置、resolver 漏配的坑、TLS 1.3 + Session Cache 叠加优化,以及 6 行验证命令。

用户反馈我们的网站打开慢了 300-400ms,主要慢在 TLS,在 HTTP 之上加一层 TLS 加密,防止中间人窃听和篡改。">HTTPS 握手。Chrome DevTools 看 SSL 阶段时间从 80ms 涨到 380ms,但服务器 CPU 没飙、网络也没拥堵。最后定位是 OCSP 在线校验,客户端阻塞等 CA 服务器响应。本文讲清楚 OCSP 是什么、为什么慢、用 OCSP stapling 修复的全过程。

HTTPS 握手到底慢在哪

openssl s_client 模拟一次握手,看每一步耗时:

# 看完整握手时序
openssl s_client -connect blog.biekanle.com:443 -servername blog.biekanle.com \
    -showcerts < /dev/null 2>&1 | head -50

# 用 curl 看分阶段耗时
curl -w "@curl-format.txt" -o /dev/null -s "https://blog.biekanle.com"
# curl-format.txt 内容:
#     time_namelookup:  %{time_namelookup}\n
#        time_connect:  %{time_connect}\n
#     time_appconnect:  %{time_appconnect}\n     ← TLS 握手完成时刻
#    time_pretransfer:  %{time_pretransfer}\n
#       time_redirect:  %{time_redirect}\n
#  time_starttransfer:  %{time_starttransfer}\n
#                ----:
#          time_total:  %{time_total}\n

# 真实输出
#     time_namelookup:  0.012345    # DNS 解析,12ms
#        time_connect:  0.045678    # TCP 三次握手,33ms
#     time_appconnect:  0.428901    # TLS 握手完成,这一步 383ms!
#    time_pretransfer:  0.429012
#  time_starttransfer:  0.512345    # TTFB
#          time_total:  0.523456

TLS 握手 383ms,占整个请求时间的 73%。问题肯定出在 SSL 那段。

OCSP 是什么

HTTPS 证书可能被吊销(私钥泄漏 / CA 主动吊销 / Heartbleed 这种事故)。浏览器拿到服务器证书后,会向 CA 发起 OCSP(Online Certificate Status Protocol)查询,问"这个证书还有效吗?"

整个流程:

问题在第三步:浏览器要等 OCSP 响应才能继续握手。如果用户在中国,而 CA 的 OCSP 服务器在欧洲,RTT 200ms+。再加上 CA 服务器自己的处理时间,整个 OCSP 查询轻松 300+ ms。

更糟的情况:OCSP 服务器抽风,响应 2 秒,所有用户的 TLS 握手都慢 2 秒。我们 2024 年 9 月遇到过 Let's Encrypt OCSP 抽风,全站握手时间从 80ms 飙到 1.8s,持续 40 分钟。

OCSP Stapling 怎么救场

OCSP Stapling(中文叫"OCSP 装订"):服务器自己定期去 CA 查 OCSP 响应,把响应缓存起来,在 TLS 握手时直接"附带"给浏览器。浏览器不再需要单独去问 CA,握手时间立刻降回原值。

Nginx 启用 OCSP stapling 只要几行配置:

server {
    listen 443 ssl http2;
    server_name blog.biekanle.com;

    ssl_certificate     /etc/letsencrypt/live/blog.biekanle.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.biekanle.com/privkey.pem;

    # 关键 3 行
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/blog.biekanle.com/chain.pem;

    # DNS 解析 OCSP 服务器域名(用 8.8.8.8 或者本地 DNS)
    resolver 8.8.8.8 1.1.1.1 valid=60s;
    resolver_timeout 5s;

    # 其他 SSL 配置...
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
}

验证 OCSP stapling 是否生效:

openssl s_client -connect blog.biekanle.com:443 -servername blog.biekanle.com \
    -status < /dev/null 2>&1 | grep -A 17 'OCSP response'

# 正常输出:
# OCSP response:
# ======================================
# OCSP Response Data:
#     OCSP Response Status: successful (0x0)
#     Response Type: Basic OCSP Response
#     Version: 1 (0x0)
#     Responder Id: ...
#     Produced At: Nov 25 09:00:00 2024 GMT
#     Responses:
#     Certificate ID:
#       ...
#     Cert Status: good          ← 这个状态是关键
#     This Update: Nov 25 09:00:00 2024 GMT
#     Next Update: Dec  1 09:00:00 2024 GMT

# 没生效会显示:
# OCSP response: no response sent

第一次踩坑:resolver 没配,stapling 不生效

开了 ssl_stapling onopenssl s_client -status 还是显示 no response。Nginx 错误日志:

2024/09/15 10:23:11 [warn] 12345#12345: no resolver defined to resolve r3.o.lencr.org while requesting certificate status

OCSP 需要 Nginx 主动去 CA 服务器查询,这要解析域名。resolver 这一行必须配。我们之前的配置漏了,导致 stapling 配了等于没配。

另一个坑:ssl_trusted_certificate 要指向颁发该证书的 CA 的证书链(不是你自己证书的 fullchain.pem)。Let's Encrypt 提供的 chain.pem 就是这个文件。

第二次踩坑:Nginx 第一个请求 stapling 还是空

Nginx 不会在启动时立即去拉 OCSP 响应,而是在第一个用户请求来了之后才异步去拉。所以你的服务刚启动,第一批用户的握手依然慢。

修法:让 Nginx 启动后主动预热一下:

# 在 reload Nginx 后,本机自己请求一次,触发 OCSP 缓存填充
sudo systemctl reload nginx
sleep 2
curl -s --resolve blog.biekanle.com:443:127.0.0.1 https://blog.biekanle.com -o /dev/null
sleep 5
# 再验证一次
openssl s_client -connect blog.biekanle.com:443 -status < /dev/null 2>&1 | grep 'Cert Status'

更彻底的方案:用 ssl_stapling_file 提前准备 OCSP 响应文件,Nginx 启动就能用:

# 用 openssl ocsp 拉一次响应,存成文件
CERT=/etc/letsencrypt/live/blog.biekanle.com/cert.pem
CHAIN=/etc/letsencrypt/live/blog.biekanle.com/chain.pem
OCSP_URL=$(openssl x509 -in $CERT -text -noout | grep -i 'OCSP - URI' | awk '{print $NF}')

openssl ocsp -no_nonce \
    -respout /etc/nginx/ocsp/blog.biekanle.com.der \
    -issuer $CHAIN \
    -cert $CERT \
    -url $OCSP_URL \
    -header "Host=$(echo $OCSP_URL | awk -F/ '{print $3}')"

# Nginx 配置改为
# ssl_stapling_file /etc/nginx/ocsp/blog.biekanle.com.der;

这种方式 Nginx 启动立刻能用,响应来自文件而不是 Nginx 内部缓存。代价是要写一个 cron 每 24 小时刷新这个文件:

cat > /etc/cron.d/refresh-ocsp <<'EOF'
0 */6 * * * root /usr/local/bin/refresh-ocsp.sh > /var/log/refresh-ocsp.log 2>&1
EOF

cat > /usr/local/bin/refresh-ocsp.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

for domain in blog.biekanle.com api.biekanle.com; do
    CERT=/etc/letsencrypt/live/$domain/cert.pem
    CHAIN=/etc/letsencrypt/live/$domain/chain.pem
    OCSP_URL=$(openssl x509 -in "$CERT" -text -noout | grep -i 'OCSP - URI' | awk '{print $NF}')
    OUT=/etc/nginx/ocsp/$domain.der
    NEW=$OUT.new

    openssl ocsp -no_nonce \
        -respout "$NEW" \
        -issuer "$CHAIN" \
        -cert "$CERT" \
        -url "$OCSP_URL" \
        -header "Host=$(echo $OCSP_URL | awk -F/ '{print $3}')" \
        2>&1

    # 验证新文件有效再替换
    if openssl ocsp -respin "$NEW" -text -CAfile "$CHAIN" -noverify > /dev/null 2>&1; then
        mv "$NEW" "$OUT"
        echo "$(date) refreshed $domain ocsp"
    else
        echo "$(date) invalid response for $domain, keeping old"
        rm -f "$NEW"
    fi
done

# 不需要 reload Nginx,它会自动重新读 stapling 文件
EOF

chmod +x /usr/local/bin/refresh-ocsp.sh

TLS 1.3 + 0-RTT 是不是更香

TLS 1.3 把握手从 2-RTT 缩到 1-RTT,0-RTT(resume 时)甚至 0 个往返就发数据。和 OCSP stapling 是独立优化,两者叠加效果最好。

# 启用 TLS 1.3 + Session Resume
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:50m;       # 50MB = 约 20 万 session
ssl_session_timeout 1d;                  # session 缓存 24h
ssl_session_tickets on;                  # 启用 session tickets(老客户端用)

# 0-RTT(谨慎用,有 replay 风险)
ssl_early_data on;     # 只对幂等请求开,GET 类没事,POST 类要在应用层防 replay

0-RTT 有 replay 攻击风险:第一次发送的请求可能被攻击者拦下来反复重放。所以只能用于幂等 GET。如果应用混合了状态修改的 POST,要在应用层做幂等保护(请求 ID + Redis 去重)。

实测对比数据

同一台机器(腾讯云华东),Chrome 实测连续访问 10 次取平均:

配置                        TLS 握手时间    总响应时间
裸 HTTPS,无优化              382ms          512ms
+ OCSP stapling              78ms           208ms
+ TLS 1.3                    52ms           181ms
+ Session Resume(2nd req)    8ms            38ms
+ HTTP/2 复用                0ms (复用)     12ms

从 512ms 降到 12ms,对用户感知是天差地别。

SSL 性能优化清单

  1. 开 OCSP stapling:握手时间立刻降 70%
  2. 用 TLS 1.3:握手从 2-RTT 到 1-RTT
  3. 开 Session Cache + Session Tickets:同用户二次握手免 key 交换
  4. HTTP/2 keepalive:同用户多个请求复用一条连接
  5. 选轻量 cipher suite:ECDSA 比 RSA 快,256 位足够安全(别上 4096 bit RSA)
  6. CDN 边缘卸载 SSL:如果用 CDN,SSL 在边缘节点解决,源站只走 HTTP

把这 6 条做齐,你的网站 TLS 性能不会成为瓶颈。

验证一切都生效的 6 行命令

DOMAIN=blog.biekanle.com

# 1. OCSP stapling 验证
openssl s_client -connect $DOMAIN:443 -status < /dev/null 2>&1 | grep 'Cert Status'

# 2. TLS 1.3 协议
openssl s_client -connect $DOMAIN:443 -tls1_3 < /dev/null 2>&1 | grep 'Protocol'

# 3. Session Reuse
openssl s_client -connect $DOMAIN:443 -reconnect < /dev/null 2>&1 | grep 'Reused'

# 4. HTTP/2
curl -I --http2 https://$DOMAIN/ 2>&1 | head -1

# 5. 完整 SSL 评级(在线工具,但要等几分钟)
echo "看 https://www.ssllabs.com/ssltest/analyze.html?d=$DOMAIN"

# 6. 简单测握手延迟(10 次取平均)
for i in {1..10}; do
    curl -w "%{time_appconnect}\n" -o /dev/null -s https://$DOMAIN/
done | awk '{sum+=$1} END {print "平均握手:", sum/NR*1000, "ms"}'

这次优化后,我们在 SSL Labs 评测从 B 升到 A+,Lighthouse 评分上首屏速度 +12 分。OCSP stapling 是 SSL 性能优化里 ROI 最高的一件事 —— 配置 5 分钟,效果立竿见影。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

React useState 连点 3 次只 +1 的真相:批量更新 + 函数式 setter 完全指南

2026-5-19 10:33:10

技术教程

Docker 镜像从 1.2GB 瘦到 80MB:我们做的 7 步优化和 10 条黄金法则

2026-5-19 10:41:15

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索