← 中文资源

简体中文版本

Node.js ECONNRESET — 服务端关闭了 keep-alive 连接

通过 reusedSocket、agent 与服务端超时对比,定位 Node.js read ECONNRESET 与 socket hang up,并只对幂等请求做重试。

阅读英文原文

现象

出现 Error: read ECONNRESET at TCP.onStreamRead 或 socket hang up;空闲一段时间后的第一个请求失败;经代理或负载均衡时偶发;相同请求重试后成功。

诊断

先确认失败的是复用连接还是新连接:

node -e "const http=require('http');const req=http.get('http://127.0.0.1:3000/health',r=>{r.resume();r.on('end',()=>console.log('ok'))});req.on('error',e=>console.log(e.code,e.syscall,'reused=',req.reusedSocket))"

reusedSocket=true 说明连接池里的 keep-alive 连接在复用前已被对端关闭;false 则更像建连阶段被拒绝或重置。再对比客户端 agent、代理空闲超时与服务端 keepAliveTimeout:客户端复用窗口长于服务端时,必然复用到已关闭的 socket。用 sleep 65 后 curl -sv 复现,可以确认是空闲超时而不是处理逻辑问题。

根因

HTTP/1.1 keep-alive 连接被对端关闭(空闲超时、重启或中间设备重置),而 Node 的 agent 仍认为它可用。请求写入已收到 FIN/RST 的 socket,于是得到 ECONNRESET,而不是透明地新建连接。

修复

客户端开启 keep-alive,并把复用时间设得比服务端和代理都短;服务端显式设置超时;每个响应都必须消费完:

const agent = new http.Agent({ keepAlive: true, keepAliveMsecs: 15_000, maxSockets: 50 });

server.keepAliveTimeout = 61_000; // 大于代理空闲超时
server.headersTimeout = 65_000;   // 必须大于 keepAliveTimeout

只对 GET、HEAD 等幂等请求在未收到响应字节前重试一次;POST 需要业务幂等键,否则会重复下单。未消费的响应体会让 socket 无法回到池中。

超时顺序应当是:服务端 keepAliveTimeout 大于代理空闲超时,代理空闲超时又大于客户端复用窗口;三者倒置就会复用到已被关闭的连接。

验证

让连接空闲超过代理超时后再次请求,应新建连接并成功且无 ECONNRESET;压测时 agent.freeSockets 有界,上游滚动重启只产生重试成功,不产生用户可见错误。