解决Python中由于logging模块误用导致的内存泄露
首先介绍下怎么发现的吧,线上的项目日志是通过logging模块打到syslog里,跑了一段时间后发现syslog的UDP连接超过了8W,没错是8W.主要是logging模块用的不对
我们之前有这么一个需求,就是针对每一个连接日志输出当前连接的信息,所以每一个连接就创建了一个日志实例,并分配一个Formatter,创建日志实例为了区分其他连接所以我就简单粗暴的用了当前对象的id来作为日志名称:
importlogging classConnection(object): def__init__(self): self._logger_name="Connection.{}".format(id(self)) self.logger=logging.getLogger(self._logger_name)
当然测试环境是开DEBUG,开DEBUG就不会往syslog里打,所以不会出现UDP连接数过多,也就不会知道有内存泄露的,我们来看看这样为什么会导致内存泄露,首先看看getLogger的代码:
defgetLogger(name=None): """ Returnaloggerwiththespecifiedname,creatingitifnecessary. Ifnonameisspecified,returntherootlogger. """ ifname: returnLogger.manager.getLogger(name) else: returnroot
主要调用了Logger.manager.getLogger,这个函数有下面一段代码片段
ifnameinself.loggerDict: rv=self.loggerDict[name] ifisinstance(rv,PlaceHolder): ph=rv rv=(self.loggerClassor_loggerClass)(name) rv.manager=self self.loggerDict[name]=rv self._fixupChildren(ph,rv) self._fixupParents(rv) else: rv=(self.loggerClassor_loggerClass)(name) rv.manager=self self.loggerDict[name]=rv self._fixupParents(rv)
logging模块为了保证同一个名称引用同一个日志实例,所以就把所有的日志实例全部存在了一个loggerDict的字典里,所以除非程序退出,创建的日志实例引用是不会释放的,所以日志实例里的handlers也不会释放.之前我又用的对象的id来作为日志名称的一部分,所以SyslogHandler创建的UDP连接就一直被占用导致了过多的UDP连接.
为了解决这个问题我在连接关闭的时候加入了如下代码:
logging.Logger.manager.loggerDict.pop(self._logger_name) self.logger.manager=None self.logger.handlers=[]
按说只加上上面第一行的代码就应该释放了,但是没有,所以又有了第三行代码,SyslogHandler才最终释放,这个问题暂时还不知道为什么,还需要再查查.
2015-03-30更新如果日志名称是以.分隔,logging模块则会将最后一部分作为日志名,并往上去寻找父Logger,如果找不到则创建PlaceHolder对象作为父,并引用Logger.
比如创建的Logger名称为a.b.c,那么实际的名称则为c,并将b作为c的父,a作为b的父,如果没有该名称的Logger则创建PlaceHolder对象作为代替,PlaceHolder会创建对当前Logger的引用.所以需要被回收的日志对象名称里不应包含.