看這一段人畜無害的代碼,axios的引入和配置,再加上超長的超時限制
import axios from 'axios'
let baseUrl = 'https://XXX.com的接口地址'
const request = axios.create({
baseURL: baseUrl, //基準地址
timeout: 500000
})
// 請求攔截
request.interceptors.request.use((config) => {
return config
})
// 響應(yīng)攔截
request.interceptors.response.use(
(res) => {
switch (res.status) {
case '500':
// Toast.fail("服務(wù)器錯誤")
return
case '404':
// Toast.fail("頁面丟失了~~")
}
return res
},
(error) => {
// 錯誤處理
if (error && error.response) {
console.log(error.response)
}
return Promise.reject(error)
}
)
export default request
那么問題來了,第一個請求之后,node處理了很多邏輯操作,以至于要等好久才會發(fā)起第二次請求,第二次請求的攔截已經(jīng)可以打印config,但是后面完全沒有響應(yīng)了,并且報錯:socket hang up
result: 'error',
data: AxiosError: socket hang up
at AxiosError.from (file:///xxx本地路徑/node_modules/axios/lib/core/AxiosError.js:89:14)
at RedirectableRequest.handleRequestError (file:///xxx本地路徑/node_modules/axios/lib/adapters/http.js:610:25)
at RedirectableRequest.emit (node:events:531:35)
at RedirectableRequest.emit (node:domain:488:12)
at eventHandlers.<computed> (xxx本地路徑/node_modules/follow-redirects/index.js:38:24)
at ClientRequest.emit (node:events:519:28)
at ClientRequest.emit (node:domain:488:12)
at TLSSocket.socketOnEnd (node:_http_client:524:9)
at TLSSocket.emit (node:events:531:35)
at TLSSocket.emit (node:domain:488:12)
at Axios.request (file:///xxx本地路徑/node_modules/axios/lib/core/Axios.js:45:41)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async GetServiceInfo (file:///xxx本地路徑/src/service/index.js:26:9)
at async HandleEventList (file:///xxx本地路徑/src/creatproject/HandleEventList.js:92:47)
at async HandleTreeData (file:///xxx本地路徑/src/creatproject/HandlePageData.js:81:25)
at async handlePageData (file:///xxx本地路徑/src/creatproject/HandlePageData.js:37:5)
at async CreatProject.creatRN (file:///xxx本地路徑/src/creatproject/CreatProject.js:128:4) {
code: 'ECONNRESET',
config: {
transitional: [Object],
adapter: [Array],
transformRequest: [Array],
transformResponse: [Array],
timeout: 50000,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
env: [Object],
validateStatus: [Function: validateStatus],
headers: [Object [AxiosHeaders]],
baseURL: 'https://www不能告訴你的接口地址',
url: '/zzz不能告訴你的路徑',
method: 'post',
data: '{...參數(shù)}'
},
request: Writable {
_events: [Object],
_writableState: [WritableState],
_maxListeners: undefined,
_options: [Object],
_ended: true,
_ending: true,
_redirectCount: 0,
_redirects: [],
_requestBodyLength: 111,
_requestBodyBuffers: [Array],
_eventsCount: 3,
_onNativeResponse: [Function (anonymous)],
_currentRequest: [ClientRequest],
_currentUrl: 'https://www不能告訴你的接口地址/zzz不能告訴你的路徑',
_timeout: null,
[Symbol(shapeMode)]: true,
[Symbol(kCapture)]: false
},
cause: Error: socket hang up
at TLSSocket.socketOnEnd (node:_http_client:524:23)
at TLSSocket.emit (node:events:531:35)
at TLSSocket.emit (node:domain:488:12)
at endReadableNT (node:internal/streams/readable:1696:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
code: 'ECONNRESET'
}
}
最喜歡疑難雜癥,因為比較有意思,但也最怕這種坑,因為老板總是催業(yè)務(wù),不敢耽擱太久。
一、我們先盤一下問題
在瀏覽器中,Axios使用 XMLHttpRequest 對象作為其底層的 HTTP 客戶端。XMLHttpRequest 是瀏覽器提供的原生對象,用于在客戶端發(fā)起 HTTP 請求。在 Node.js 環(huán)境中,Axios使用 http 模塊或 https 模塊作為其底層的 HTTP 客戶端,具體取決于請求的協(xié)議是 HTTP 還是 HTTPS。這些模塊提供了在 Node.js 環(huán)境中發(fā)起 HTTP 請求所需的功能。因此,無論是在瀏覽器端還是在 Node.js 環(huán)境中,Axios都會使用適當(dāng)?shù)?Http client實現(xiàn)網(wǎng)絡(luò)請求。所以我們暫且把他倆當(dāng)作同一個情況。
什么是 Socket hang up
說不定以后也可以做為面試題目來提問,什么是 Socket hang up?
hang up 翻譯為英文有掛斷的意思, socket hang up 也可以理解為 socket(鏈接)被掛斷。無論使用哪種語言,也許多多少少應(yīng)該都會遇見過,只是不知道你有沒有去思考這是為什么?例如在 Node.js 中系統(tǒng)提供的 http server 默認超時為 2 分鐘(server.timeout 可以查看),如果一個請求超出這個時間,http server 會關(guān)閉這個請求鏈接,當(dāng)客戶端想要返回一個請求的時候發(fā)現(xiàn)這個 socket 已經(jīng)被 “掛斷”,就會報 socket hang up 錯誤。
問題復(fù)現(xiàn)
比較偶現(xiàn),但是本地實際應(yīng)用中,兩次請求間隔時間較長,再通過調(diào)整timeout大小還是比較穩(wěn)定復(fù)現(xiàn)的。
網(wǎng)上有一種復(fù)現(xiàn)方式有興趣可以試試:
1、服務(wù)端:開啟一個 http 服務(wù),定義 /timeout 接口設(shè)置 3 分鐘之后延遲響應(yīng),也就是讓它3米分鐘后再response;
2、客戶端:創(chuàng)建一個http請求并監(jiān)聽請求和error;
3、操作:啟動服務(wù)端之后再啟動客戶端大約 2 分鐘之后或者直接 kill 掉服務(wù)端會報Soket hang up的錯
錯誤原因
為什么在 http client 這一端會報 socket hang up 這個錯誤,看下 Node.js http client 端源碼會發(fā)現(xiàn)由于沒有得到響應(yīng),那么就認為這個 socket 已經(jīng)結(jié)束,因此會觸發(fā)一個 connResetException('socket hang up') 錯誤。
// https://github.com/nodejs/node/blob/v12.x/lib/_http_client.js#L440
function socketOnEnd() {
const socket = this;
const req = this._httpMessage;
const parser = this.parser;
if (!req.res && !req.socket._hadError) {
// If we don't have a response then we know that the socket
// ended prematurely and we need to emit an error on the request.
req.socket._hadError = true;
req.emit('error', connResetException('socket hang up'));
}
if (parser) {
parser.finish();
freeParser(parser, req, socket);
}
socket.destroy();
}
二、解決問題的過程
1、設(shè)置 http server socket 超時時間(不建議)
看以下 Node.js http server 源碼,默認情況下服務(wù)器的超時值為 2 分鐘,如果超時,socket 會自動銷毀,可以通過調(diào)用 server.setTimeout(msecs) 方法將超時時間調(diào)節(jié)大一些,如果傳入 0 將關(guān)閉超時機制。
// 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;
};
但是這種不受限的連接,明顯不安全呀,而且也會很占用資源,這種做法我是不敢用的,哈哈哈。如果不設(shè)置 setTimeout 也可以針對這種錯誤在 http client 端進行捕獲放入隊列發(fā)起重試,當(dāng)這種錯誤概率很大的時候要去排查相應(yīng)的服務(wù)是否存在處理很慢等異常問題。
2、關(guān)閉keep-alive
這點我們針對axios的,在請求的攔截中我們看到它默認開啟了keep-alive,持久連接。雙方都可以發(fā)起持久連接的請求,但是雙方也都可以拒絕持久連接。通俗說就是我可以請求你在我閑著的時候保持通話,但是你也可以在我閑著沒聲兒的時候直接掛我電話的意思,反之亦然。有大佬特別耐心地抓了網(wǎng)絡(luò)包,并分析了tcp包的數(shù)據(jù)傳輸過程,有興趣可以了解一下。,
當(dāng)然服務(wù)器是支持的,而且postman發(fā)請求一直都是沒問題的。只是為了避免在長連接中出現(xiàn)socket變化,導(dǎo)致ECONNRESET問題,我們可以直接關(guān)閉,讓每次請求都重新進行三次握手:
import axios from 'axios'
import https from 'https'
request.interceptors.request.use(
(config) => {
//在請求攔截中設(shè)置
config.httpsAgent = new https.Agent({ keepAlive: false });
return config;
},
(error) => {
return Promise.reject(error);
}
)
不過這樣也有問題,畢竟長連接的作用本來就是減少請求時間和資源消耗,對于一些c端場景可能并不是最優(yōu)解。
3、啟用axios-retry(最優(yōu)解)
axios-retry是一個用于在Node.js和瀏覽器中使用的Axios插件,它提供了在HTTP請求失敗時自動重試的功能。通過使用axios-retry,您可以配置Axios實例以在遇到連接問題或其他臨時錯誤時自動重試請求,以增加請求的可靠性。
同時我們在請求攔截中增加對重試請求的監(jiān)聽或者響應(yīng)里的錯誤監(jiān)聽,通過日志來判斷網(wǎng)絡(luò)請求的穩(wěn)定性和觸達率。
import axios from 'axios'
import axiosRetry from 'axios-retry'
let baseUrl = 'https://XXX.com的接口地址'
const request = axios.create({
baseURL: baseUrl, //基準地址
timeout: 500000
})
// 設(shè)置axios-retry的配置
axiosRetry(request, {
retries: 3, // 設(shè)置重試次數(shù)
retryDelay: axiosRetry.exponentialDelay, // 設(shè)置重試延遲策略
});
// 請求攔截
request.interceptors.request.use((config) => {
if (config.__isRetryRequest) {
// 這里是針對重試請求的監(jiān)聽
logger.info('重試請求', config.url)
}
return config
})
// 響應(yīng)攔截
request.interceptors.response.use(
(res) => {
// 。 。 。
return res
},
(error) => {
if (error.code === 'ECONNABORTED') {
// 監(jiān)聽Socket hang up的錯誤日志
logger.error('sockt錯誤', error)
} else{
// 。 。 。
}
return Promise.reject(error)
}
)
三、思考一下
除了上面提到的可行或不可行的方法,處理過程中還嘗試去看node版本和axios版本對它的影響,但是都沒有絕對的處理掉,并且在github上關(guān)于axios的這個問題也有討論,并且現(xiàn)在的bug狀態(tài)還是open,應(yīng)該是還沒解決。
axios源碼中看到有這么一段話,翻譯是說:
有時響應(yīng)會很慢,如果不響應(yīng),連接事件就會被事件循環(huán)系統(tǒng)阻塞;
計時器回調(diào)將被觸發(fā),并在連接之前調(diào)用abort(),然后獲得“socket hang up”和code碼ECONNRESET;
此時,如果我們有大量的請求,nodejs會在后臺掛起一些socket。這個數(shù)字會越來越大。
然后這些被掛起的套接字就會一點點地吞噬CPU;
ClientRequest.setTimeout將在指定的毫秒內(nèi)觸發(fā),并且可以確保在連接后觸發(fā)abort()。
它注釋說明的代碼是用來處理超時的,不過也從側(cè)面反映出它拋出“socket hang up”的原因:計時器啟動了,或者socket太大占內(nèi)存了。
// 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的時間限制,導(dǎo)致axois提前結(jié)束。
// 超時設(shè)置20s
firts: 19.747s
// 中間可能有多少毫秒的操作
secend: 29.197ms
總結(jié)
既然是官方還未解決的問題,一方面我們需要根據(jù)場景適當(dāng)?shù)卣{(diào)整超時設(shè)置,另一方面也要采取重試進行兜底。