Node.js中關(guān)于Accept時Emfile的處理
本文轉(zhuǎn)載自微信公眾號「編程雜技」,作者theanarkh。轉(zhuǎn)載本文請聯(lián)系編程雜技公眾號。
EMFILE表示進(jìn)程打開的文件描述符達(dá)到了上限,比如建立了一個TCP連接后,調(diào)用accept函數(shù)的時候就可能觸發(fā)這個錯誤。那么這個會導(dǎo)致什么問題呢?首先我們看看Node.js是如何處理連接的。
void uv__server_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
uv_stream_t* stream;
int err;
stream = container_of(w, uv_stream_t, io_watcher);
while (uv__stream_fd(stream) != -1) {
// 摘取一個TCP連接
err = uv__accept(uv__stream_fd(stream));
// 記錄下來
stream->accepted_fd = err;
// 執(zhí)行上層回調(diào),回調(diào)里消費accepted_fd
stream->connection_cb(stream, 0);
// 下一個循環(huán)
}
}
當(dāng)監(jiān)聽socket上可讀事件觸發(fā)的時候,Node.js就會執(zhí)行uv__server_io進(jìn)行處理。在uv__server_io中Node.js就會不斷地調(diào)用accept摘取連接,然后執(zhí)行回調(diào)處理該連接。這是正常的流程,那么如果accept出錯了,那會怎么樣?比如返回了EMFILE錯誤。
因為Node.js中,epoll的工作模式是水平觸發(fā),所以每輪事件循環(huán)中,uv__server_io都會被觸發(fā),然后執(zhí)行accept,接著觸發(fā)錯誤(如果還沒有可用的文件描述符的話)。然而底層已完成三次握手的TCP連接無法得到處理,客戶端也只能默默地在等待。Node.js選擇的處理策略是關(guān)閉連接來通知客戶端,服務(wù)器已經(jīng)過載。我們看看Node.js具體是怎么做的。在初始化第一個Libuv stream的時候會首先預(yù)留一個文件描述符。
if (loop->emfile_fd == -1) {
err = uv__open_cloexec("/dev/null", O_RDONLY);
if (err < 0)
/* In the rare case that "/dev/null" isn't mounted open "/"
* instead.
*/
err = uv__open_cloexec("/", O_RDONLY);
if (err >= 0)
loop->emfile_fd = err;
}
我們看到Node.js打開了一個資源,然后拿到了一個文件描述符保存到emfile_fd。當(dāng)Node.js處理TCP連接的時候,這個emfile_fd可能就會被用上。
// 摘取TCP連接
err = uv__accept(uv__stream_fd(stream));
if (err < 0) {
// 文件描述符過載
if (err == UV_EMFILE || err == UV_ENFILE) {
err = uv__emfile_trick(loop, uv__stream_fd(stream));
if (err == UV_EAGAIN || err == UV__ERR(EWOULDBLOCK))
break;
}
stream->connection_cb(stream, err);
continue;
}
我們看到當(dāng)uv_accept返回UV_EMFILE錯誤的時候,會執(zhí)行uv__emfile_trick。
static int uv__emfile_trick(uv_loop_t* loop, int accept_fd) {
int err;
int emfile_fd;
if (loop->emfile_fd == -1)
return UV_EMFILE;
// 關(guān)閉預(yù)留的文件描述符,下面的uv_accept才能執(zhí)行成果
uv__close(loop->emfile_fd);
loop->emfile_fd = -1;
// 循環(huán)關(guān)閉無法處理的TCP連接
do {
// 摘取TCP連接
err = uv__accept(accept_fd);
if (err >= 0)
// 關(guān)閉TCP連接,通知客戶端服務(wù)器過載
uv__close(err);
} while (err >= 0 || err == UV_EINTR);
// 重新獲取一個預(yù)留的文件描述符
emfile_fd = uv__open_cloexec("/", O_RDONLY);
if (emfile_fd >= 0)
loop->emfile_fd = emfile_fd;
return err;
}
我們看到uv__emfile_trick中關(guān)閉了所有無法處理的TCP連接,然后重新補充預(yù)留的文件描述符。正常來說uv_accept最后會返回UV_EAGAIN表示沒有連接需要處理了,從而結(jié)束處理連接的整個邏輯。