您现在的位置是:人工智能 >>正文

聊聊 Too Many Open Files 错误导致服务器死循环

人工智能5人已围观

简介0x01 前言 在服务器编程中,经常会遇到 Too many open files 这个报错,而且这个报错如果处理不好,很有可能会导致服务器死循环。0x02 示例代码 以上 ...

0x01 前言

在服务器编程中 ,聊聊经常会遇到 Too many open files 这个报错,错误而且这个报错如果处理不好,服务很有可能会导致服务器死循环。器死

0x02 示例代码

以上是循环我用rust写的一个非常简单的tcp服务器,它的聊聊主要逻辑是,先创建一个listener ,错误然后再在循环里不断调用listener.accept接收tcp连接,服务如果接收成功 ,器死就调用handle_client处理这个连接 ,循环如果接收失败 ,聊聊就打印一行错误日志。服务器租用错误

handle_client里的服务逻辑也非常简单 ,就是器死等待客户端关闭连接 ,或等待其发送任意数据 ,循环当这两种情况发生时,handle_client就会直接关闭这个连接 。

当然 ,如果在等待期间报错了,handle_client也会打印一行错误日志 。

下面我们就会使用这段程序,来演示服务器死循环的情况,这段程序不必非要用rust编写  ,用其他语言也都可以。

测试代码我已经放到github了,免费模板如果想要自己动手测试的 ,可以clone下来自己试下 。

代码地址:https://github.com/ytcoode/too-many-open-files

0x03 动手演示

先启动该服务器 :

由上图可见,该服务器的进程id是312004,监听地址是0.0.0.0:9999。

再查看下该服务器已打开的文件数  :

一共是10个 ,主要包括标准输入输出、epoll、及一些socket 。

再查看下该服务器进程最多可打开的源码库文件数 :

看选中行,Soft Limit那一列,其表示该进程最多可用的文件描述符数量为1024个,即最多可同时打开的文件数为1024个 。

我们把它改小一点 ,方便后续测试 :

上图中,先使用prlimit命令将该服务器进程的Max open files数改成12,然后再用cat命令确认下该改动已生效 。

至此 ,我们已经设置好该服务器进程最多可用的文件描述符数量为12 ,其当前已用的文件描述符数量为10 ,香港云服务器所以该服务器最多还可以再接收2个tcp连接。

我们用 `ncat localhost 9999` 命令建立连接试一下,当然你也可以用telnet, nc等其他命令 ,只要能建立tcp连接就行:

由上图服务器日志可见 ,该tcp连接已建立成功 。

再看下当前服务器已使用的文件描述符数量:

由上图可见 ,新建socket使用的文件描述符为10 ,当前服务器进程已使用11个文件描述符,到目前为止一切正常 。

用同样的命令再建立一个tcp连接,这次应该也能连接成功 ,不过会有一些有意思的事情发生:

首先看上图中最后一行info日志,亿华云它表示第二次tcp连接也建立成功了,如果此时去看文件描述符数量,也正好是12 。

不过此次连接建立也导致不断的error日志输出,该服务器死循环了。

但此时,如果我们关闭第二次ncat命令建立的tcp连接,服务器又不会一直输出error日志了 ,它又会恢复到正常状态 :

看上图中的最后一条info日志,它表示第二个tcp连接正常关闭了 ,且当前已建立的连接数量是源码下载1 。

此时,如果我们去看文件描述符数量,其也变成了11 ,这里就不再截图了 ,有兴趣的可以自己动手试下。

0x04 为什么会出现死循环?

首先 ,在linux的世界里,一切皆文件 ,这里就包括socket 。

其次,linux为保证系统的整体性安全,为每个进程限制了其最大可使用的文件描述符数量 ,即最大可打开的文件数  ,这个数量就是上面我们用 `cat /proc/$(pidof too-many-open-files)/limits` 命令输出的Max open files行 ,Soft Limit列对应的值 ,该值是可以通过各种方式修改的,在我的系统上 ,该值默认为1024 。

接着 ,我们启动了服务器 ,然后通过 `l /proc/$(pidof too-many-open-files)/fd/` 命令查看该服务器已使用的文件描述符数量,其为10 。

之后  ,我们用prlimit命令将该服务器进程最大可使用的文件描述符数量改成了12 ,这样该服务器就还只剩两个文件描述符可用。

再之后,我们用ncat命令建立了两个tcp连接 ,在服务器端的循环里 ,accept接收到这两个连接并进行处理,此时该服务器进程消耗完了最后两个可用的文件描述符  。

接下来 ,服务器代码进入下一次循环 ,继续调用accept尝试接收新的连接,问题的关键点也就出现在了这里 。

accept是个系统调用  ,我们看下其对应的内核实现 :

这个是accept系统调用的入口函数 ,沿着函数调用,可找到以下代码:

由上图可见,在真正的do_accept之前,会先调用get_unused_fd_flags找一个还未被使用的文件描述符 ,如果寻找时报错了,即newfd < 0,则直接返回该错误码给用户层 ,如果找到了一个可用的文件描述符,则开始执行真正的accept操作 。

继续看get_unused_fd_flags函数 :

它在调用其他函数之前 ,会通过 rlimit(RLIMIT_NOFILE) 获取当前进程最大可使用的文件描述符数量  ,即我们上面通过prlitmit命令设置的12。

继续往下看  ,我们会找到以下代码:

该函数的目的是分配一个文件描述符,即fd ,图中选中行之前是找到一个还未被使用的fd,然后判断该 fd 是否 >= end ,如果是 ,则goto到out,进而return error ,而这个error就是EMFILE。

那end值是什么呢?它就是上面用 rlimit(RLIMIT_NOFILE) 获取的当前进程最大可用的文件描述符数。

结合上面的例子我们知道,当服务器接收完两个tcp连接后,其最大可使用的12个文件描述符已全部被用完,当其循环到下一次accept系统调用后,会最终进入到上图这个函数 ,这次新分配的fd值一定是12(因为fd值从0开始的,所以fd值为12表示第13个文件描述符),而我们又限制了该进程最大可用12个文件描述符,即我们限制了end值为12,所以在上图选中行进行判断时,fd 一定是 >= end 的 ,所以 ,该函数一定会返回EMFILE这个错误码 。

而EMFILE是什么呢?

它就是我们在运行测试程序时看到的 Too many open files 这个错误。

示例程序调用accept收到这个错误码后,会打印一行error日志 ,然后继续循环调用accept ,然后继续报错 ,就这样  ,服务器就在accept这里发生了死循环。

0x05 这个问题如何处理?

因为 too many open files 是个临时性错误 ,当进程中的其他地方关闭了一些文件 ,或者管理人员调高了该进程的 max open files值,accept就不会再报 EMFILE 错误 ,也就不会再死循环了 。

所以其处理方法也很简单 ,就是在accept发生错误时,sleep一段时间,这样既防止了cpu 100%的发生 ,也给进程时间来调整已用及最大的文件描述符数。

0x06 用epoll也会有这个问题吗?

会有,epoll只是个通知机制,当epoll检测到有连接可被接收时,还是会通过accept来接收这个连接。

不过这里分成两种情况 。

当使用epoll的edge-triggered模式时,正确写法是要一直循环调用accept接收连接,直到其返回 EAGAIN 或 EWOULDBLOCK 错误码,表示已经没有连接可接收了,这时才能退出accept循环 ,但如果在这之前accept返回了 too many open files 这个错误 ,就会发生死循环了。

当使用epoll的level-triggered模式时 ,可以不必一直循环调用accept直到其返回EAGAIN 或 EWOULDBLOCK,可以提前退出,但如果操作系统里还有建立好的连接等待被接收 ,epoll还是会一直通知应用层,告知其要调用accept接收这些连接,如果此时文件描述符没有了 ,accept还是会一直报 too many open files 错误 ,最终还是进入到了死循环 。

0x07 Go是如何处理的?

下面我们看下go内置的http服务器 ,是如何处理这个问题的 :

当accept返回err后 ,其会通过ne.Temporary()来检查该err是否是临时性错误,如果是 ,则会根据一定的规则,sleep一段时间。

这里,临时性错误就包括 EMFILE ,即too many open files错误:

我们也可以写个简单的例子测试下:

按照之前的方式 ,让其触发 too many open files 这个错误:

由图可见,和我们上面分析的一样 ,其也陷入了死循环,但是它用sleep的方式,防止cpu使用率100% 。

0x08 Redis是如何处理的  ?

下面我们看下redis是如何处理这个问题的:

当anetTcpAccept返回 too many open files 错误时,它只打印了一行错误日志  ,就直接return了。

不过因为redis使用的是level-triggered模式的epoll,所以虽然这里直接return了,但因为底层的连接没接收出来,epoll一直会调用这个函数,然后一直报错,进而死循环 。

实验下:

可以看到,其一直在输出这个错误 。

0x09 结语

希望通过这篇文章,能给大家的技术水平带来一点提高  。

Tags:

相关文章


滇ICP备2023006006号-40