起因
自己做的软件被人告知挂掉了,发现内存泄露。
分析问题
软件中使用了python+web.py+sqlalchemy的架构。这三个东西的搭配都有很多人用,所以,我很不担心是这三个中的架构以及他们的代码出现的问题。一直怀疑我的代码。(我是如此的谦虚阿)。
经过排除,发现1个可疑点:
1. 代码的一个线程中的线程类,有一个request的对象,这个对象是我用来不停的和服务器进行HTTP交互的。
这个对象会每1秒发一个http请求,请求过程中是封装了cookie的,我担心由于多次的请求导致此类中的http cookie预留的buff增加导致问题。因此将此request不作为线程类的私有对象,而是作为函数临时对象申请。
本来觉得解决问题了,就跑了几个小时,发现内存还是不停的增加。
于是,开始尝试怀疑很多事情,python, web.py, sqlalchemy。 这一番查找还是很有效果的。
了解到
Python内存管理
Python因为是自己回收垃圾,所以一直不太去关注他到底是怎么回收的,gc的机制是啥。这次遇到一查看,才彻底了解了python的内存管理的特点
- python有个内存链表,每次施放资源的时候,先推到此链表中,不立刻将内存施放。这样在频繁的内存对象申请的时候,就可以直接从池里取出来,加快了对象分配时间。这个对python这种弱对象的语言的效率提升非常重要。
- python本身的垃圾机制是通过引用计数来实现的,引用计数实现的垃圾回收,最大的问题就是循环引用。即,A对象引用B对象的同时,B对象中的属性也引用了A对象。这样,在内存施放的时候,必然打来一部分的无法施放的内存。当然python设计的同时也确实很好的处理了这个问题,在gc设计上还引入了其他的回收机制来规避掉,循环引用带来的问题。
不看不知道,python还确实有一定的风险在这上面。但是,怎么查看我的简单代码都不像用到了循环引用。所以,我关注点放到了其他的上面。
Web.py
这个东西用的人也不少,而且我仔细的看了他的实现代码,不应该在请求的时候会造成内存泄露。同时,我也写了一个模拟场景测试是否会有请求的内存泄露问题。发现也确实没有。So,只有一种可能。SQLAlchemy...
SQLAlchemy
SQLAlchemy也是号称Python的Hibernate,我想不需要太关注他的框架设计上是否会有问题。所以,我最主要的是怀疑我的用法。在现有的程序上,我用到了如下内容:
1. BASE
2. DB
3. Session
BASE本身,是我的全局变量,只有一份,不太会产生问题。
DB,这个是作为我的全局单件的一个属性存在,因此,他也算是全局内容之一,只创建了一次。
因此,Session成了我的重点怀疑对象。
我查找了很多资料,有些人也提到了使用SQLAlchemy的内存增长。首先,我发现了一个问题:
- session不是线程安全
的确在我的session申请上,我使用了如下方式
class DB_Instance:
def __init__(self):
self.engine = create_engine(u'sqlite:///%s'%(config.db_file))
Base.metadata.create_all(self.engine)
pass
@property
def session(self):
return sessionmaker(bind=self.engine)()
sessionmaker创建出来的session,确认不是线程安全的,但是,我每次代码在使用
db = Instance.db_ins
session = db.session
...
的时候,自然开启了一个新的session,所以这个session可能会带来一定的内存问题。另外,我也发现,使用session的时候最后最好使用session.close()去关闭掉session,除非这个session是全局唯一的。因为session.close()会删除掉事务带来的开销。
如果想跨线程的使用session,必须要使用
Session = scoped_session(sessionmaker(autoflush=True))
但是这样改动之后,还是发现内存有增长
- session文档上要求的全局唯一
查看文档发现,session文档上要求最好放在全局定义上,但是查遍文档的其他说明,也没有说为啥?代码也看不出有什么特别。但是我的session,不能放在全局位置,不然势必带来线程的问题,创建session是一个线程,使用又是一个线程。
class DB_Instance:
def __init__(self):
self.engine = create_engine(u'sqlite:///%s'%(config.db_file))
Base.metadata.create_all(self.engine)
self.Session = sessionmaker()
self.Session.configure(bind=self.engine)
pass
@property
def session(self):
return self.Session()
这样修改之后,发现确认使用session的时候,不是直接使用sessionmaker()创建的了,而是首先使用sessionmaker()创建了Session类,再通过这个Session类来创建session对象。
经过一段时间的测试,确实没有内存增加。问题解决。
总结
SQLAlchemy文档还是指明了,我想他的意思是sessionmaker()这个函数必须在全局使用,而不是每次创建session的时候使用。
sessionmaker()在初始化的时候,肯定申请了一些全局的属性,而这些属性在每次调研sessionmaker()的同时都会创建,所以带来了内存不停增加的问题。
Session本身也不是cache,他会做一些数据库操作事务的缓存,但是他本身不是cache,这个是要明确的。
Python+web.py+SQLAlchemy没有问题,问题出在使用上...