首先介绍下怎么发现的吧, 线上的项目日志是通过 logging 模块打到 syslog 里, 跑了一段时间后发现 syslog 的 udp 连接超过了 8w, 没错是 8 w. 主要是 logging 模块用的不对
我们之前有这么一个需求, 就是针对每一个连接日志输出当前连接的信息, 所以每一个 连接就创建了一个日志实例, 并分配一个 formatter, 创建日志实例为了区分其他连接 所以我就简单粗暴的用了当前对象的 id 来作为日志名称:
import logging
class connection(object):
def __init__(self):
self._logger_name = “connection.{}”.format(id(self))
self.logger = logging.getlogger(self._logger_name)
当然测试环境是开 debug, 开 debug 就不会往 syslog 里打, 所以不会出现 udp 连接数 过多, 也就不会知道有内存泄露的, 我们来看看这样为什么会导致内存泄露, 首先看看 getlogger 的代码:
def getlogger(name=none):
“””
return a logger with the specified name, creating it if necessary.
if no name is specified, return the root logger.
“””
if name:
return logger.manager.getlogger(name)
else:
return root
主要调用了 logger.manager.getlogger, 这个函数有下面一段代码片段
if name in self.loggerdict:
rv = self.loggerdict[name]
if isinstance(rv, placeholder):
ph = rv
rv = (self.loggerclass or _loggerclass)(name)
rv.manager = self
self.loggerdict[name] = rv
self._fixupchildren(ph, rv)
self._fixupparents(rv)
else:
rv = (self.loggerclass or _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 的引用. 所以需要被回收的日志对象名称里不应包含 .