• 作者:老汪软件技巧
  • 发表时间:2024-09-06 04:01
  • 浏览量:

看以下 Node.js http server 源码,默认情况下服务器的超时值为 2 分钟,如果超时,socket 会自动销毁,可以通过调用 server.setTimeout(msecs) 方法将超时时间调节大一些,如果传入 0 将关闭超时机制。

// https://github.com/nodejs/node/blob/v12.x/lib/_http_server.js#L348
function Server(options, requestListener) {
  // ...
  this.timeout = kDefaultHttpServerTimeout; // 默认为 2 * 60 * 1000
  this.keepAliveTimeout = 5000;
  this.maxHeadersCount = null;
  this.headersTimeout = 40 * 1000; // 40 seconds
}
Object.setPrototypeOf(Server.prototype, net.Server.prototype);
Object.setPrototypeOf(Server, net.Server);
Server.prototype.setTimeout = function setTimeout(msecs, callback) {
  this.timeout = msecs;
  if (callback)
    this.on('timeout', callback);
  return this;
};

但是这种不受限的连接,明显不安全呀,而且也会很占用资源,这种做法我是不敢用的,哈哈哈。如果不设置 setTimeout 也可以针对这种错误在 http client 端进行捕获放入队列发起重试,当这种错误概率很大的时候要去排查相应的服务是否存在处理很慢等异常问题。

2、关闭keep-alive

这点我们针对axios的,在请求的拦截中我们看到它默认开启了keep-alive,持久连接。双方都可以发起持久连接的请求,但是双方也都可以拒绝持久连接。通俗说就是我可以请求你在我闲着的时候保持通话,但是你也可以在我闲着没声儿的时候直接挂我电话的意思,反之亦然。有大佬特别耐心地抓了网络包,并分析了tcp包的数据传输过程,有兴趣可以了解一下。,

当然服务器是支持的,而且postman发请求一直都是没问题的。只是为了避免在长连接中出现socket变化,导致ECONNRESET问题,我们可以直接关闭,让每次请求都重新进行三次握手:

import axios from 'axios'
import https from 'https'
request.interceptors.request.use(
    (config) => {
        //在请求拦截中设置
        config.httpsAgent = new https.Agent({ keepAlive: false });
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
)

不过这样也有问题,毕竟长连接的作用本来就是减少请求时间和资源消耗,对于一些c端场景可能并不是最优解。

3、启用axios-retry(最优解)

axios-retry是一个用于在Node.js和浏览器中使用的Axios插件,它提供了在HTTP请求失败时自动重试的功能。通过使用axios-retry,您可以配置Axios实例以在遇到连接问题或其他临时错误时自动重试请求,以增加请求的可靠性。

同时我们在请求拦截中增加对重试请求的监听或者响应里的错误监听,通过日志来判断网络请求的稳定性和触达率。

import axios from 'axios'
import axiosRetry from 'axios-retry'
let baseUrl = 'https://XXX.com的接口地址'
const request = axios.create({
   baseURL: baseUrl, //基准地址
   timeout: 500000
})
// 设置axios-retry的配置
axiosRetry(request, {
    retries: 3, // 设置重试次数
    retryDelay: axiosRetry.exponentialDelay, // 设置重试延迟策略
});
// 请求拦截
request.interceptors.request.use((config) => {
    if (config.__isRetryRequest) {
        // 这里是针对重试请求的监听
        logger.info('重试请求', config.url)
    }
   return config
})
// 响应拦截
request.interceptors.response.use(
   (res) => {
      // 。 。 。
      return res
   },
   (error) => {
      if (error.code === 'ECONNABORTED') {
         // 监听Socket hang up的错误日志
         logger.error('sockt错误', error)
      } else{
         // 。 。 。
      }
      return Promise.reject(error)
   }
)

三、思考一下

除了上面提到的可行或不可行的方法,处理过程中还尝试去看node版本和axios版本对它的影响,但是都没有绝对的处理掉,并且在github上关于axios的这个问题也有讨论,并且现在的bug状态还是open,应该是还没解决。

axios源码中看到有这么一段话,翻译是说:

有时响应会很慢,如果不响应,连接事件就会被事件循环系统阻塞;

计时器回调将被触发,并在连接之前调用abort(),然后获得“socket hang up”和code码ECONNRESET;

此时,如果我们有大量的请求,nodejs会在后台挂起一些socket。这个数字会越来越大。然后这些被挂起的套接字就会一点点地吞噬CPU;

ClientRequest.setTimeout将在指定的毫秒内触发,并且可以确保在连接后触发abort()。

它注释说明的代码是用来处理超时的,不过也从侧面反映出它抛出“socket hang up”的原因:计时器启动了,或者socket太大占内存了。

// Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.
// And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.
// At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.
// And then these socket which be hang up will devouring CPU little by little.
// ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.

在文章开始的部分我提到两次请求间隔较长,可以看下打印的log,可以看出第二次请求的开始,很有可能超出20s的时间限制,导致axois提前结束。

// 超时设置20s
firts: 19.747s
// 中间可能有多少毫秒的操作
secend: 29.197ms

总结

既然是官方还未解决的问题,一方面我们需要根据场景适当地调整超时设置,另一方面也要采取重试进行兜底。

原文链接:node发起axios请求报错:Socket hang up