Python中由於logging模組誤用導致的記憶體泄露的解決方案
這篇文章主要介紹瞭解決Python中由於logging模組誤用導致的記憶體泄露,針對由於過多的UDP串連所產生的問題,需要的朋友可以參考下
首先介紹下怎麼發現的吧, 線上的項目日誌是通過 logging 模組打到 syslog 裡, 跑了一段時間後發現 syslog 的 UDP 串連超過了 8W, 沒錯是 8 W. 主要是 logging 模組用的不對
我們之前有這麼一個需求, 就是針對每一個串連日誌輸出當前串連的資訊, 所以每一個 串連就建立了一個日誌執行個體, 並分配一個 Formatter, 建立日誌執行個體為了區分其他串連 所以我就簡單粗暴的用了當前對象的 id 來作為日誌名稱:
?
1 2 3 4 5 6 7 |
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 的代碼:
?
1 2 3 4 5 6 7 8 9 10 |
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, 這個函數有下面一段程式碼片段
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 串連.
為瞭解決這個問題我在串連關閉的時候加入了如下代碼:
?
1 2 3 |
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 的引用. 所以需要被回收的日誌對象名稱裡不應包含 .