如何解决 Keep-Alive 导致 ECONNRESET 的问题
使用 Node.js 搭建的服务中,如果存在 HTTP 的 RPC 调用,并且使用了
keep-alive
来保持 TCP 长连接, 那么一定会有一个牛皮糖般的问题困扰着你,那就是
ECONNRESET
或者
socket hang up
这种错误。
一段简单的复现代码:
const http = require("http");
const agent = new http.Agent({ keepAlive: true });
// 从 Node.js 8 开始,服务器的 keep-alive 默认 5 秒超时
.createServer((req, res) => {
res.write("hello world");
res.end();
.listen(8080);
// 每 5 秒发起一次请求
setInterval(() => {
http.get("http://127.0.0.1:8080", { agent }, res => {
res.on("data", () => {})
res.on("end", () => {
console.log("success");
}, 5000);
等 3-4 次请求之后,会出现报错:
Error: read ECONNRESET
at TCP.onStreamRead (internal/stream_base_commons.js:111:27)
Emitted 'error' event at:
at Socket.socketErrorListener (_http_client.js:392:9)
at Socket.emit (events.js:189:13)
at emitErrorNT (internal/streams/destroy.js:82:8)
at emitErrorAndCloseNT (internal/streams/destroy.js:50:3)
at process._tickCallback (internal/process/next_tick.js:63:19)
这个问题是如何产生的
其实这就是状态机里一个简单的竞争情形:
- 客户端与服务端成功建立了长连接
- 连接静默一段时间(无 HTTP 请求)
- 服务端因为在一段时间内没有收到任何数据,主动关闭了 TCP 连接
- 客户端在收到 TCP 关闭的信息前,发送了一个新的 HTTP 请求
-
服务端收到请求后拒绝,客户端报错
ECONNRESET
总结一下就是:服务端先于客户端关闭了 TCP,而客户端此时还未同步状态,所以存在一个错误的暂态(客户端认为 TCP 连接依然在,但实际已经销毁了)
这个问题如何解决
有两种方法可选:
1、保证客户端永远先于服务端关闭 TCP 连接
这种方法就是把客户端的
keep-alive
超时时间设置得短一些(短于服务端即可)。这样就可以保证永远是客户端这边超时关闭的 TCP 连接,消除了错误的暂态。
但这样在实际生产环境中是没法 100% 解决问题的,因为无论把客户端超时时间如何设置到多少,因为网络延迟的存在,始终无法保证所有的服务端的
keep-alive
超时时间都长于客户端的值;如果把客户端超时时间设置得太小(比如 1 秒),又失去了意义。
可以参考: https:// zhuanlan.zhihu.com/p/34 147188
2、错误重试
最佳的解决方法还是,如果出现了这种暂态导致的错误,那么重试一次请求就好,但是只识别
ECONNRESET
这个错误码是不够的,因为服务端可能因为某些原因真的关闭了 TCP 端口。
所以最佳的做法是,
使用一个标记表示当前的请求是否复用了 TCP,如果错误码为
ECONNRESET
且存在标记(复用了 TCP),那么就重试一次
。但目前 Node.js 的 HTTP Agent 里还无法识别一个请求是否复用了 TCP 连接。
例如 request 的做法 ,就是使用 forever-agent 标记是否复用了 TCP,然后再识别错误码。然而, forever-agent 只会在 0.10 版本生效 ,现在早就不能用了。
所以近期在 Node.js 合入了
一个 PR
,加入了一个
req.reusedSocket
,将会在下一个 minor 版本中发布。我们可以通过
req.reusedSocket
是否为
true
来表示当前 HTTP 请求是否复用 TCP。
对于 Node.js 之前的版本,我们可以改造 HTTP Agent,使其在旧版本中也会有这个标记,例如 agentkeepalive 的改动: https:// github.com/node-modules /agentkeepalive/pull/82
于是我们可以像下面这样写代码:
const http = require("http");
const request = require("request");
const Agent = require("agentkeepalive");
const agent = new Agent();
.createServer((req, res) => {
res.write("hello world");
res.end();
.listen(8080);
setInterval(() => {
const reqInfo = request.get("http://127.0.0.1:8080", { agent }, (err) => {
if (!err) {
console.log("success");
} else if (err.code === 'ECONNRESET' && reqInfo.req.reusedSocket) {
// 如果错误码为ECONNRESET,且复用了TCP连接,那么重试一次
return request.get("http://127.0.0.1:8080", (err) => {
if (err) {
throw err;
} else {
console.log("success with retry");
} else {
throw err;
}, 5000);
输出如下,可以看到之前存在的偶现错误,都会自动重试:
success