web.py Session的坑

用web.py开发的一个后台程序,负责增/减/改数据。随着业务的变化,这些数同时需要提供API供其他程序使用,所以我实现了一组API,但对API请求了几十万次后服务器出现了磁盘空间不够。在服务器上执行df命令

# 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做开发框架:)