广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何优化Nginx日志中的TLS握手时间?

Nginx里最常用的请求耗时字段是$request_time,它记录的是从读到HTTP请求首字节到响应发送完成的时间,并不包含前面的TCP三次握手和TLS协商。如果只看这个字段,很容易得出证书和密码套件没拖慢访问的结论。实际上,当TLS握手占用了额外300毫秒时,用户看到的是首字节延迟明显增加,而Nginx默认的combined日志根本看不到。优化TLS协商时间的第一步,是把$ssl_handshake_time和$ssl_session_reused单独写入日志,让每次握手都有据可查。

如何优化Nginx日志中的TLS握手时间?

先理清Nginx日志里的TLS时间变量

Nginx从1.19.4版本开始提供$ssl_handshake_time变量,它以秒为单位、毫秒精度返回SSL/TLS握手耗时。这个值在完整握手时会包含证书传输、密钥交换、加密套件协商等多个步骤;当连接通过会话恢复复用时,握手只需要很少的往返,$ssl_handshake_time会明显小于完整握手,有时接近单次网络RTT。为了区分这两种情况,可以同时记录$ssl_session_reused:该变量在复用时输出字母r,不复用时输出点号.。

一个常见的误解是$request_time已经包含了TLS握手。其实$request_time从HTTP解析开始计时,所以它不会反映TLS协商。另一个变量$connection_time虽然包括TCP连接建立,但通常也不覆盖证书握手阶段。因此,单独记录$ssl_handshake_time才能准确衡量TLS层开销。下面是一个可用的log_format配置,字段顺序可根据日志分析平台调整。

# 需要在 http 块中启用日志格式
log_format tls_debug '$remote_addr - $remote_user [$time_local] '
                     '"$request" $status $body_bytes_sent '
                     '"$http_referer" "$http_user_agent" '
                     'proto=$ssl_protocol cipher=$ssl_cipher '
                     'handshake=$ssl_handshake_time reused=$ssl_session_reused '
                     'req_time=$request_time';
access_log /var/log/nginx/tls_debug.log tls_debug;

如果使用的Nginx低于1.19.4,日志里没有$ssl_handshake_time,也不必完全放弃观察。可以通过抓包计算ClientHello到ServerHello之间的耗时,或者直接升级Nginx。考虑到旧版本还存在不少TLS相关性能缺陷,建议至少升级到稳定主线版本。新版Nginx不仅能记录握手时间,还支持TLS 1.3和更合理的密码套件命令。

降低TLS协商时间的核心配置

影响TLS握手耗时最直接的是会话复用。默认情况下,Nginx的SSL会话缓存是关闭的,每次新连接都可能执行完整握手。打开共享缓存后,同一个客户端在超时窗口内的后续连接可以复用之前的会话,跳过证书验证和密钥交换。配置里ssl_session_cache shared:SSL:20m表示分配20MB共享内存,约能缓存几万个会话;ssl_session_timeout 1d是复用有效期。会话票据ssl_session_tickets on可以让客户端保存票据,在服务端重启或跨节点时也能快速恢复。

TLS 1.3比1.2在握手次数上有天然优势:1.3只需要一次往返即可完成握手,而1.2通常需要两次往返。升级协议优先级可以立刻减少几十到上百毫秒。但要注意不要只保留TLS 1.3,否则老客户端无法连接,保留TLSv1.2作为回退。密码套件方面,ECDSA证书的P-256密钥比RSA 2048体积小、运算更快,适合对握手延迟敏感的业务;使用ECDHE_ECDSA_AES128_GCM_SHA256这类套件可以同时降低计算和传输开销。

OCSP stapling可以让服务器在握手时附带证书吊销状态,避免客户端单独发起OCSP查询。这个查询有时会阻塞几百毫秒,尤其是证书链里包含多个中间证书时。启用ssl_stapling后需要配置resolver,如果内网DNS无法解析外部域名,可以用公共DNS或本机dnsmasq。同时,证书链里多余的根证书或过期中间证书会增大ServerHello之后的传输量,建议使用工具检查证书链,只保留必要中间证书。下面是一个综合配置示例。

http {
    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 1d;
    ssl_session_tickets on;
    ssl_session_ticket_key /etc/nginx/ticket.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
    ssl_prefer_server_ciphers off;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 1.1.1.1 valid=300s;
    resolver_timeout 5s;
    ssl_certificate /etc/nginx/certs/fullchain_ecc.pem;
    ssl_certificate_key /etc/nginx/certs/privkey_ecc.pem;
}

用日志数据验证优化效果

优化不是配置完了就结束,必须用日志证明握手时间真的下降。因为$ssl_handshake_time字段格式为handshake=0.045,可以写一条awk命令统计非零握手的平均、最小和最大值。以下命令遍历每一行日志,找到handshake匹配项,跳过0值,输出样本数量与耗时分布。

awk '{
    for (i=1; i<=NF; i++) {
        if ($i ~ /^handshake=/) {
            split($i, a, "=");
            gsub(/;/, "", a[2]);
            val = a[2] + 0;
            if (val > 0) {
                count++; sum += val;
                if (val > max) max = val;
                if (min == 0 || val < min) min = val;
            }
        }
    }
}
END {
    if (count > 0) {
        printf "count=%d avg=%.3f min=%.3f max=%.3f\n", count, sum/count, min, max;
    }
}' /var/log/nginx/tls_debug.log

另一个关键指标是会话复用率。统计$ssl_session_reused为r的请求占比,若复用率长期低于30%,说明缓存被频繁清空或用户大部分是首次连接。此时可以检查共享内存是否被占满、客户端是否因安全设置禁用会话票据,以及是否有多节点共享缓存的问题。对于多节点环境,单机缓存不够,需要把会话票据密钥同步到每台节点,或使用分布式存储做会话共享。

如果日志显示完整握手仍然明显高于网络基线,建议用tcpdump或tshark抓包。重点看ClientHello与ServerHello之间的服务端处理时长、证书消息长度和客户端Key Exchange往返。也可以使用curl -w中的time_appconnect指标快速验证,但要清楚它包含TCP连接建连时间,与Nginx的$ssl_handshake_time统计口径略有不同。持续观察一周的日志,按协议版本和复用状态分组,才能判断问题来自TLS1.2兼容分支、证书链大小还是网络RTT本身。

广告位 · 文章底部(后台-模块管理替换此区域)