flask多線程下,連接泄露的bug
架構圖
如圖所示,底層使用mysql,web服務使用flask-SqlAlchemy
的連接池(複用連接,減少創建銷燬開銷),邏輯層代碼使用線程池(異步IO操作,如果要異步cpu操作,可以很方便改成進程池)。
基礎知識
- 使用
db.engine.execute(sql)
: 從連接池獲取一個連接,執行完sql後自動commit
;(commit
操作的回調是: 歸還連接到池裏); - 使用
session
的orm
(xxxModel.query等
): 默認配置及推薦配置是autocommit=false
,執行完增刪查改後,處於事務未提交的狀態,也就是沒有歸還連接。如果要歸還連接,可以使用語句:1 2 3 4
db.session.commit() db.session.rollback() db.session.close() db.session.remove(): 底層會調用db.session.close()
小結:
db.engine.execute(sql) => 自動commit => 自動歸還
session(orm) => 手動commit => 手動歸還
因此db.engine.execute(sql)是絕對安全的;
orm是有條件的。接着往下看orm的安全條件。
線程與session
使用flask的SqlAlchemy插件flask-SqlAlchemy
時,每個線程可以直接用db.session
獲得session,即使不顯式獲得,使用orm的model時,其實也隱式得獲得了session
。
線程與session的關係:
每個線程有自己的threadlocal
的session
對象,並且隨着線程銷燬,會自動釋放session
,也就是會隱式調用session.remove
,也就是會隱式釋放session
的連接。
多線程兩種使用:
t1=threading.Thread(...)
;- 線程池:
future= pool.submit(...)
.
方法1的線程使用完以後自動銷燬=>session自動銷燬=>連接自動釋放;
方法2的線程使用完以後歸還線程池=>session手動銷燬=>連接釋放。
小結:
不使用線程池=>連接自動釋放;
使用線程池=>連接手動釋放.
手動釋放的方法:
|
|
空閒連接超時與連接釋放bug
前面說到使用線程池時,連接沒有自動釋放,一直維護在線程的threadlocal存儲中(tls)。那麼這樣似乎也沒有什麼關係,只要線程池大小<連接池大小,這樣連接池有空閒連接,每個線程也有自己的連接可以用,一切似乎也相安無事。
然而,這裏有一個之前沒有提到的機制:空閒連接超時回收。
空閒連接超時回收
mysql服務端:
定期檢查現存連接的空閒時間,把超出wait_timeout
的連接刪除,此時客戶端保存的長連接引用就失效了; 這個時間的設定:
|
|
web服務:
flask會定期檢查連接池裏的連接,把空閒連接刪除,重新向mysql服務端申請新的連接,這樣就不會訪問到失效的連接引用了。其中定期的時間是: app.config['SQLALCHEMY_POOL_RECYCLE'] =xxx
(秒,應當設置爲小於wait_timeout
)。這就是爲什麼最好連接用完及時歸還,否則可能就沒法被flask刷新成新連接。
空閒連接超時與連接釋放bug
bug發生的流程
- mysql服務端清除了空閒時間過長的連接;
- 線程池中線程一直不銷燬,因此持有了活了很久的session;
- 活了很久的session持有了空閒很久的連接, 這個連接其實已經被服務端銷燬了,因此已經不可用了,但是由於其一直沒有歸還到連接池中,因此一直沒有得到更新。
- 此時web服務收到數據請求,使用該線程中的該session中的連接,就會拋異常了,因爲連接已經不可用了。
一般來說,空閒時間很長以後,線程池裏所有線程的所有session的所有連接都會失效,因此就會完全無法通過orm訪問數據庫了。
相關異常信息:
|
|
這裏之所以說invalid transaction is rolled back
,是因爲老session收到數據請求後,準備要用連接了。
而連接上的事務沒有自動提交,也沒有rollback,因此不能直接用。
因此嘗試把連接上,上一次請求的事務提交,但由於連接已經失效,所以失敗了。