DuckSoft's Miscellanies Ex nihilo ad astra.

记 gevent 的文件描述符泄漏问题

开端

最近用 Docker 向服务器上部署了一批使用 Flask + gevent 的 Web 应用程序,经过不同的时间、不同的访问量后,这些应用程序总会变得对网络请求无应答,显然这些程序因为某种原因已经 down 掉了。

使用 docker logs 命令查看容器日志,一排排的正常请求记录映入眼帘,不断滚动;随后则是满屏幕滚动的 Traceback。没有动态视力技能,加之日志实在太长,懒癌发作的笔者直接敲了 Ctrl+C 停掉了日志回显,改用 docker logs --tail=50 命令查看最后 50 行日志,自然是看到了之前滚动的 Traceback。这些 Traceback 的最后一行都是相同的内容:

OSError: [Errno 24] Too many open files

而出错的区域正是落在 gevent.pywsgi 模块。猜想应该是因为某种原因,pywsgi 模块打开了过多的文件而没有关闭,导致文件描述符数目超出了限制。

还是那句话,有问题,找谷歌。果不其然,在 gevent 的 GitHub 的 Issues #649 找到了我想要的答案。

溯源

Issue 主有一个日访问 250 万次的 API 地址(约 20 次/s),使用最原生的 gevent pywsgi (甚至没有用上 Flask 等框架)作出最简单的回应。启动服务器,开始接受流量,仅仅 1 小时之后就发现 Python 宿主进程使用了 200 多个文件描述符,并且文件描述符的数量随时间还在增长,当超过系统限制之后程序不断报错。

gevent 的主要贡献者 @jamadden 回应称,这是因为 gevent 在处理 Keep-Alive 的长连接时没有引入有效的超时机制,因此每当有一个连接超时就会积累一个文件描述符,最终积累导致服务器崩溃。同时他也给出了几种可行的解决方案:

然而我并没有选择这些方案中的任何一个。我选择更为直接的方案——换框架

补牢

其实本来我就对 gevent 这个 Python 轮子非常不爽,因为每次构建基于 Alpine 的 Docker 镜像的时候,gevent 总是那个只用 pip 永远装不上的包——你得用 apk add py3-gevent跑一遍才行。这不仅破坏了基于 requirements.txt 单一依赖清单的美好,也给 Docker 构建过程带来了很多冗余。嗯,终于有机会把他从我的项目中甩掉了。

然而问题又来了,甩掉 gevent 之后又该投向什么呢?再次询问万能的谷歌爸爸,最终在 Flask 的一份文档中找到了想要的答案。文中提到了使用 Tornado 作为容器部署 Flask 应用的方法。于是脑子一热,就用 Tornado 把工程改写了一下,竟然可以正常运行(我的 App 名叫 dog):

if __name__ == '__main__':
    from tornado.wsgi import WSGIContainer
    from tornado.httpserver import HTTPServer
    from tornado.ioloop import IOLoop
    from dog import app

    http_server = HTTPServer(WSGIContainer(app))
    http_server.listen(3333)
IOLoop.instance().start()

于是改写 Dockerfile,甩掉该死的 gevent 依赖,添加了 tornadorequirements.txt,重新构建并部署了我的 Web 应用程序。

小插曲

顺手查了一下 Tornado 的相关资料,这是一个异步的 Web 框架,然而非常滑稽的是 Flask 是一个同步的 Web 框架。用异步的 Web 框架做同步的 Web 框架的 WSGI 容器,看起来似乎并没有什么好处(因为还是阻塞的)。

再次翻阅了 Flask 的官方文档,在相同标题的页面内,赫然发现 Tornado 已经被删掉了(!!!),顿时有了一种看了假文档的感觉。

不过,令人心安的是,Tornado 称自己完全可以处理好长连接的问题,至少可能已经解决了我们的问题,所以……再懒一回吧,233。

comments powered by Disqus