SQLAlchemy上的內存問題

起因

自己做的軟件被人告知掛掉了,發現內存泄露。

分析問題

軟件中使用了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沒有問題,問題出在使用上...



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章