web.py session 的坑
用 web.py 开发的一个后台 API,但被请求了几十万次后服务器出现了磁盘空间不够:
# df Filesystem 512-blocks Used Avail Capacity Mounted on /dev/sd0a 2057756 477776 1477096 24% / /dev/sd0k 624161968 335328 592618544 0% /home /dev/sd0d 8250780 28 7838216 0% /tmp /dev/sd0f 4122108 753660 3162344 19% /usr /dev/sd0g 2057756 409804 1545068 21% /usr/X11R6 /dev/sd0h 20636924 2010480 17594600 10% /usr/local /dev/sd0j 4122108 4 3916000 0% /usr/obj /dev/sd0i 4122108 4 3916000 0% /usr/src /dev/sd0e 48964444 17933392 28582832 39% /var
如上可见,磁盘空间是足够的,这种情况肯定是 inode 用完了,用 df -i 就可以看到。
导致 inode 耗尽的原因是有大量的文件创建,用下面列出每个文件夹文件数:
for i in /*; do echo $i; find $i |wc -l; done
最后发现是 Web 程序下 sessions 目录有几十万个小文件,因为代码中我将 Session 指定保存在文件中的:
if web.config.get('_session') is None: session = web.session.Session(app, web.session.DiskStore('sessions'), initializer={'loginin': False}) web.config._session = session
先将 sessions 目录里的文件删除后,系统恢复了正常。
最先怀疑是我代码逻辑有问题,review 一次确定代码本身逻辑正确。为了 bug 复现,我在测试环境中循环请求网站:
for i in {1..100};do curl localhost:8080;done
和预期一致,sessions 目录出现大量小文件,怀疑对象马上转移到 web.py 上。看了下 web.py 源码,web.py 的 Session 功能实现在 session.py 的 Session 类中。在 Session 类的构造函数有这样一句代码:
if app: app.add_processor(self._processor)
我在网站后台初始化 Session 时提供了 app 参数,所以这条 if 语句成立:
web.session.Session(app, web.session.DiskStore('sessions'), initializer={'loginin': False})
app 参数是 application 类实例,定义在 web.py 的 application.py 中。
application 类中定义了一个 processors 列表,每接受到一个 HTTP 请求时就递归调用列表里的函数对象:
class application: def __init__(self, mapping=(), fvars={}, autoreload=None): ... self.processors = [] ...
add_processor 方法就是负责添加处理函数,所以 Session 类中一开始就把内部的 _processor 函数放在其中,对每个 HTTP 请求都调用它。
_processor 实现如下:
def _processor(self, handler): """Application processor to setup session for every request""" self._cleanup() self._load() try: return handler() finally: self._save()
重点就在 _load 的实现的这段代码:
def _load(self): ... self.session_id = web.cookies().get(cookie_name) if self.session_id and not self._valid_session_id(self.session_id): self.session_id = None self._check_expiry() if self.session_id: d = self.store[self.session_id] self.update(d) self._validate_ip() if not self.session_id: self.session_id = self._generate_session_id() ...
如果一次 HTTP 请求的 Cookie 字段中没有 Session 信息,就产生新的 session id。
导致 session 文件爆增的原因就是:如果开启了 Session 功能,对每次 HTTP 请求,web.py 都会验证 Cookie 里是否有 session id,如果没有,web.py 就生成一个 session id 并把信息保存在 sessions 目录中,然后返回 Set-Cookie 让客户端设置一个 session 信息。但是调用 API 的程序是不会理会 Set-Cookie,这就导致在大量请求的情况下服务器消耗 inode 非常快。
用官方文档里提供的 Session 样例代码也可以复现这个问题:http://webpy.org/cookbook/sessions
根本原因就是 web.py 对 Session 的实现不合理,这种情况很容易产生拒绝服务攻击,所以目前来看,我不建议在生产环境中用 web.py 做开发框架:)