Blog Email GitHub

28 Apr 2014
epoll由EMFILE引发CPU飙升

问题表现

项目中有台服务器发现突然 cpu 和 load average 飙升。最后查到原因,是因为服务器系统升级重启后,单个进程打开的最大描述符数未设置好,被修改成1024了,改回65536,重启服务,问题修复。

追本溯源

这个问题很有意思,为何我们常用的 nginx 也遇到了 Too many open files 的问题,未见cpu飙升,为何加速节点上的服务会导致cpu飙升?咨询了下曹局,曹局解释说:

由于tcp监听队列满了,而异步io持续触发去读时又没有句柄能分配给这个网络请求,所以,导致队列一直是满的,但是因为没有fd而无法将请求从监听队列中移除去。

写个提供 echo 服务的 tcp server 验证下,很容易。

  • server 。使用 epoll 来进行多路IO复用。启动服务前,需要 ulimit -n 20 限制服务器能够打开的最大文件描述符数是 20,这样除了标准输入、标准输出、标准错误、监听socket对象、epoll对象占用了5个文件描述符,最多并发能够接受15个链接。
  • clinet 。模拟客户端发起16个并发链接。

大量的抛出 Too many open files 错误。

while True:
    events = epoll.poll()
    for fileno, event in events:
        if fileno == sock.fileno():
            try:
                connection, address = sock.accept()
                connection.setblocking(0)
                epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLOUT)
                connections[connection.fileno()] = connection
                packets[connection.fileno()] = b''
            except socket.error, ex:
                if ex.errno != errno.EMFILE:
                    raise
                logging.error(ex.strerror)
        elif ...

由于文件描述符的打开数量达到上限后, accept 的时候,抛出 EMFILE 错误,对于这个错误简单的处理方式是记下log,这样监听sock对象的 EPOLLIN 时间依然会被触发,陷入了死循环。因此cpu飙升。

根本解决

该如何根本解决这个问题呢?我还曾经想过,能不能使用边缘触发呢?仅仅触发一次?

epoll.register(sock.fileno(), select.EPOLLIN | select.EPOLLET)

由于边缘触发,对于事件通知仅仅通知一次,为了防止丢事件,就必须一直重试。

In edge-triggered mode the program would need to accept() new socket connections until a socket.error exception occurs.

给下代码或许清晰:

while True:
    events = epoll.poll()
    for fileno, event in events:
        if fileno == sock.fileno():
            while True:
                try:
                    connection, address = sock.accept()
                    connection.setblocking(0)
                    epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLOUT)
                    connections[connection.fileno()] = connection
                    packets[connection.fileno()] = b''
                except socket.error, ex:
                    if ex.errno not in (errno.EMFILE, errno.EAGAIN):
                        raise
                    logging.error(ex.strerror)
        elif ...

依然会陷入死循环。最好的解决方法是, accept后立马关闭该连接 。但是文件描述符都达到了上限,又accept不了。所以 需要在服务器启动之初,就申请一个限制的文件描述符 ,当出现 Too many open files ,释放掉这个文件描述符,accept连接,接着立马关闭该连接。看代码

idle_fd = open('/dev/null')
...
while True:
    events = epoll.poll()
    for fileno, event in events:
        if fileno == sock.fileno():
            try:
                connection, address = sock.accept()
                connection.setblocking(0)
                epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLOUT)
                connections[connection.fileno()] = connection
                packets[connection.fileno()] = b''
            except socket.error, ex:
                if ex.errno != errno.EMFILE:
                    raise
                idle_fd.close()
                connection, address = sock.accept()
                connection.close()
                logging.error(ex.strerror)
                idle_fd = open('/dev/null')

问题得到完美解决。

References & Resoures

  1. How To Use Linux epoll with Python