添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
首发于 Starkwang.log
如何解决 Keep-Alive 导致 ECONNRESET 的问题

如何解决 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)

这个问题是如何产生的

其实这就是状态机里一个简单的竞争情形:

  1. 客户端与服务端成功建立了长连接
  2. 连接静默一段时间(无 HTTP 请求)
  3. 服务端因为在一段时间内没有收到任何数据,主动关闭了 TCP 连接
  4. 客户端在收到 TCP 关闭的信息前,发送了一个新的 HTTP 请求
  5. 服务端收到请求后拒绝,客户端报错 ECONNRESET

总结一下就是:服务端先于客户端关闭了 TCP,而客户端此时还未同步状态,所以存在一个错误的暂态(客户端认为 TCP 连接依然在,但实际已经销毁了)

这个问题如何解决

有两种方法可选:

1、保证客户端永远先于服务端关闭 TCP 连接

这种方法就是把客户端的 keep-alive 超时时间设置得短一些(短于服务端即可)。这样就可以保证永远是客户端这边超时关闭的 TCP 连接,消除了错误的暂态。

但这样在实际生产环境中是没法 100% 解决问题的,因为无论把客户端超时时间如何设置到多少,因为网络延迟的存在,始终无法保证所有的服务端的 keep-alive 超时时间都长于客户端的值;如果把客户端超时时间设置得太小(比如 1 秒),又失去了意义。

可以参考: zhuanlan.zhihu.com/p/34

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 的改动: github.com/node-modules

于是我们可以像下面这样写代码:

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