问题描述
网站上线并配置HTTPS证书后,通过浏览器访问一切正常。然而,当使用curl命令发起HTTPS请求时,连接被立即重置(reset),如下图所示。

排查过程
初步分析与排除
首先,尝试curl同域名下的HTTP(80端口)URL,请求正常返回,说明客户端与服务器之间的80端口网络通信无异常。
接着,curl服务器上其他已配置HTTPS的域名,请求也正常返回,这表明443端口的基础网络连通性也是好的。
那么,问题是否出在证书本身?检查证书有效期,确认未过期。通过专业站点(如myssl.com)查询证书链详情,也未发现任何问题。
怀疑是Nginx配置的加密套件兼容性不足,于是更换了一套更通用、兼容性更强的加密套件列表:
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
配置后重启Nginx,问题依旧。
深入抓包分析
在排查陷入僵局时,决定在客户端和服务器端同时使用tcpdump抓包,并通过Wireshark进行深入分析。

从抓包数据看,从发起请求到连接被重置,共经历了16个数据包。关键在于,TCP三次握手和TLS握手均已完成,在客户端开始发送应用层数据后,服务器返回了第一个确认(ACK)包,紧接着就发送了重置(RST)包,中断了连接。
调整Nginx缓冲区配置
既然握手成功,却在数据传输伊始被重置,一个可能的推测是客户端发送的请求头或请求体过大,超过了Nginx的默认缓冲区限制。于是调整了Nginx配置文件中关于客户端请求的相关参数:
client_header_buffer_size 64k;
large_client_header_buffers 4 64k;
client_body_buffer_size 20m;
keepalive_timeout 120;
(由于从抓包看,问题发生在Nginx处理请求的早期,尚未到达FastCGI等上游模块,故未调整相关配置。)
修改并重启后,令人沮丧的是,错误现象没有丝毫改变。
尝试更换证书类型
虽然直觉上认为与证书关系不大,但在“穷途末路”之际,决定尝试更换证书类型进行验证。原证书为RSA类型,现更换为ECC(椭圆曲线)证书。
更换后,curl请求出现了新的错误提示:
curl: (35) Cannot communicate securely with peer: no common encryption algorithm(s).
(无法与对等体安全通信:无通用加密算法)
这引发了新的思考。经查询发现,在RedHat/CentOS系统上,curl默认使用NSS库,并且禁用了ECC加密算法。虽然服务器端支持ECC套件,但客户端不支持,导致握手失败。需要通过--ciphers参数为curl指定一个双方都支持的加密套件,例如:
curl --ciphers ecdhe_rsa_aes_128_gcm_sha_256 ...
指定套件后,curl请求又回到了最初的起点——连接被重置。这证明问题根因并非证书类型。
问题解决与根因分析
关键配置:ssl_session_cache
在几乎尝试了所有能想到的排查方向后,开始逐行比对问题站点与其他正常站点的Nginx配置差异。发现唯一明显的区别是,正常站点的配置中包含了 ssl_session_cache 参数,而问题站点没有。
ssl_session_cache 通常被视为一项TLS性能优化配置,用于缓存SSL/TLS会话参数,以减少重复握手带来的开销。虽然不认为这是问题的直接原因,但在无计可施的情况下,决定在问题站点的配置中添加上此配置,例如使用共享缓存:
ssl_session_cache shared:SSL:10m; # 分配10MB内存用于共享会话缓存
ssl_session_timeout 10m;
添加并重启Nginx后,奇迹发生了——curl请求成功返回了预期的内容!
抓包对比与原理探究
为什么一个看似用于性能优化的参数会决定连接的成功与否?为了弄清原因,再次进行了抓包对比。
对比添加 ssl_session_cache 前后的抓包数据发现,在导致reset的连接过程中,TLS握手阶段多了一个“Server Key Exchange”消息。根据SSL/TLS握手协议规范,“Server Key Exchange”消息是一个可选步骤,主要在以下情况出现:
- 协商使用RSA加密,但服务器证书中没有包含RSA公钥(仅包含签名)。
- 协商使用DH(Diffie-Hellman)或ECDH(椭圆曲线Diffie-Hellman)密钥交换算法,但服务器证书中没有提供相应的参数。
- 协商使用Fortezza密钥交换算法。
从抓包解析看,问题连接中协商使用的正是(EC)Diffie-Hellman算法。初步推断,当Nginx未配置 ssl_session_cache 时,在某些条件下(可能与客户端行为、OpenSSL版本或特定算法套件有关),它可能会选择一种需要“Server Key Exchange”消息的密钥交换方式。而这一过程可能与某些客户端的实现存在兼容性问题,最终导致了连接在握手后的瞬间被重置。
配置 ssl_session_cache 后,Nginx可能更倾向于复用或采用另一种不需要额外交换密钥参数的握手流程,从而避免了问题。需要注意的是,这更多是基于现象和协议分析的合理推测,其深层机制涉及Nginx与OpenSSL库的交互细节。
Nginx常见错误日志速查
以下列举一些Nginx错误日志中常见的错误信息及其可能原因,供快速定位问题时参考:
-
“upstream prematurely closed connection”
- 原因:上游服务器在未完全返回响应时,用户端断开了连接。通常对服务无实质影响。
-
“recv() failed (104: Connection reset by peer)”
- 原因:对端(客户端或上游)发送了RST包重置连接。可能由于服务端过载主动断开、客户端提前关闭浏览器、或用户点击停止等。
-
“(111: Connection refused) while connecting to upstream”
- 原因:Nginx无法连接到上游服务(upstream),可能因为上游服务未启动或网络不通。
-
“(110: Connection timed out) while connecting to upstream”
-
“(104: Connection reset by peer) while connecting to upstream”
- 原因:在连接上游服务过程中,上游服务发送了RST包重置连接。
-
“client intended to send too large body”
- 原因:客户端发送的请求体大小超过了
client_max_body_size 指令的设定值。
-
“SSL_do_handshake() failed”
-
“ngx_slab_alloc() failed: no memory in SSL session shared cache” 或 “could not add new SSL session to the session cache while SSL handshaking”
- 原因:配置的
ssl_session_cache 共享缓存空间不足,无法存储新的会话。
总结与建议
- 排查工具:tcpdump适合抓取原始数据包,而Wireshark强大的协议分析功能对于理解复杂的网络交互(如HTTPS握手)至关重要。
- 证书选择:ECC证书在相同安全强度下,比RSA证书体积更小,加解密速度更快,是现代TLS部署的推荐选择。
- 关键配置:
ssl_session_cache 不仅是一项性能优化配置,在某些场景下可能成为影响HTTPS服务兼容性与稳定性的关键因素。生产环境中建议务必配置,并使用 shared 共享缓存模式以提升性能。