心靈純潔的人,生活充滿甜蜜和喜悅。——列夫·托爾斯泰node
關於 Socket hang up 最先是在一次服務壓測中出現的,後來獲得瞭解決,近期在 Node.js 服務遷移 K8S 容器中時又報出了此問題,覈查緣由以後發現是對容器的 CPU、內存大小作了限制引發的,這裏總結下什麼是 Socket hang up 及在什麼狀況下發生,該如何解決。git
做者簡介:五月君,Nodejs Developer,慕課網認證做者,熱愛技術、喜歡分享的 90 後青年,歡迎關注 Nodejs技術棧 和 Github 開源項目 www.nodejs.redgithub
這裏也能夠作爲面試題目來提問,什麼是 Socket hang up?面試
hang up 翻譯爲英文有掛斷的意思, socket hang up 也能夠理解爲 socket(連接)被掛斷。不管使用哪一種語言,也許多多少少應該都會碰見過,只是不知道你有沒有去思考這是爲何?例如在 Node.js 中系統提供的 http server 默認超時爲 2 兩分鐘(server.timeout 能夠查看),若是一個請求超出這個時間,http server 會關閉這個請求連接,當客戶端想要返回一個請求的時候發現這個 socket 已經被 「掛斷」,就會報 socket hang up 錯誤。bash
弄懂一個問題,仍是要多去實踐,下面從一個小的 demo 復現這個問題而後結合 Node.js http 相關源碼進一步瞭解 Socket hang up 是什麼?另外也推薦你看下萬能的 stack overflow 上面也有對這個問題的討論 stackoverflow.com/questions/1…。服務器
服務端socket
開啓一個 http 服務,定義 /timeout 接口設置 3 秒以後延遲響應ui
const http = require('http');
const port = 3020;
const server = http.createServer((request, response) => {
console.log('request url: ', request.url);
if (request.url === '/timeout') {
setTimeout(function() {
response.end('OK!');
}, 1000 * 60 * 3)
}
}).listen(port);
console.log('server listening on port ', port);
複製代碼
客戶端this
const http = require('http');
const opts = {
hostname: '127.0.0.1',
port: 3020,
path: '/timeout',
method: 'GET',
};
http.get(opts, (res) => {
let rawData = '';
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => {
try {
console.log(rawData);
} catch (e) {
console.error(e.message);
}
});
}).on('error', err => {
console.error(err);
});
複製代碼
啓動服務端以後再啓動客戶端大約 2 兩分鐘以後或者直接 kill 掉服務端會報以下錯誤,能夠看到相應的錯誤堆棧url
Error: socket hang up
at connResetException (internal/errors.js:570:14)
at Socket.socketOnEnd (_http_client.js:440:23)
at Socket.emit (events.js:215:7)
at endReadableNT (_stream_readable.js:1183:12)
at processTicksAndRejections (internal/process/task_queues.js:80:21) {
code: 'ECONNRESET'
}
複製代碼
爲何在 http client 這一端會報 socket hang up 這個錯誤,看下 Node.js http client 端源碼會發現因爲沒有獲得響應,那麼就認爲這個 socket 已經結束,所以會在 L440 處觸發一個 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. 設置 http server socket 超時時間
看如下 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;
};
複製代碼
修改後的代碼以下所示:
const server = http.createServer((request, response) => {
console.log('request url: ', request.url);
if (request.url === '/timeout') {
setTimeout(function() {
response.end('OK!');
}, 1000 * 60 * 3)
}
}).listen(port);
server.setTimeout(0); // 設置超時時間
複製代碼
若是不設置 setTimeout 也能夠針對這種錯誤在 http client 端進行捕獲放入隊列發起重試,當這種錯誤機率很大的時候要去排查相應的服務是否存在處理很慢等異常問題。
這裏注意區分下 ECONNRESET 與 ETIMEDOUT 的區別
ECONNRESET 爲讀取超時,當服務器太慢沒法正常響應時就會發生 {"code":"ECONNRESET"} 錯誤,例如上面介紹的 socket hang up 例子。
ETIMEDOUT 爲連接超時,是指的在客戶端與遠程服務器創建連接發生的超時,下面給一個 request 模塊的請求例子。
const request = require('request');
request({
url: 'http://127.0.0.1:3020/timeout',
timeout: 5000,
}, (err, response, body) => {
console.log(err, body);
});
複製代碼
以上示例,大約持續 5 秒中以後會報 { code: 'ETIMEDOUT' } 錯誤,堆棧以下:
Error: ETIMEDOUT
at Timeout._onTimeout (/Users/test/node_modules/request/request.js:677:15)
at listOnTimeout (internal/timers.js:531:17)
at processTimers (internal/timers.js:475:7) {
code: 'ETIMEDOUT'
}
複製代碼