flask多線程下,連接泄露的bug【轉載】

flask多線程下,連接泄露的bug

架構圖

 

如圖所示,底層使用mysql,web服務使用flask-SqlAlchemy的連接池(複用連接,減少創建銷燬開銷),邏輯層代碼使用線程池(異步IO操作,如果要異步cpu操作,可以很方便改成進程池)。

基礎知識

  1. 使用db.engine.execute(sql): 從連接池獲取一個連接,執行完sql後自動commit;(commit操作的回調是: 歸還連接到池裏);
  2. 使用sessionorm(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的關係:

每個線程有自己的threadlocalsession對象,並且隨着線程銷燬,會自動釋放session,也就是會隱式調用session.remove,也就是會隱式釋放session的連接。

多線程兩種使用:

  1. t1=threading.Thread(...);
  2. 線程池: future= pool.submit(...).
    方法1的線程使用完以後自動銷燬=>session自動銷燬=>連接自動釋放;
    方法2的線程使用完以後歸還線程池=>session手動銷燬=>連接釋放。

小結:
不使用線程池=>連接自動釋放;
使用線程池=>連接手動釋放.
手動釋放的方法:

1
db.session.remove()

空閒連接超時與連接釋放bug

前面說到使用線程池時,連接沒有自動釋放,一直維護在線程的threadlocal存儲中(tls)。那麼這樣似乎也沒有什麼關係,只要線程池大小<連接池大小,這樣連接池有空閒連接,每個線程也有自己的連接可以用,一切似乎也相安無事。

然而,這裏有一個之前沒有提到的機制:空閒連接超時回收。

空閒連接超時回收

mysql服務端:

定期檢查現存連接的空閒時間,把超出wait_timeout的連接刪除,此時客戶端保存的長連接引用就失效了; 這個時間的設定:

1
2
show global variables like 'wait_timeout';
set global wait_timeout=10*60; -- seconds

web服務:

flask會定期檢查連接池裏的連接,把空閒連接刪除,重新向mysql服務端申請新的連接,這樣就不會訪問到失效的連接引用了。其中定期的時間是: app.config['SQLALCHEMY_POOL_RECYCLE'] =xxx(秒,應當設置爲小於wait_timeout)。這就是爲什麼最好連接用完及時歸還,否則可能就沒法被flask刷新成新連接。

空閒連接超時與連接釋放bug

bug發生的流程

  1. mysql服務端清除了空閒時間過長的連接;
  2. 線程池中線程一直不銷燬,因此持有了活了很久的session;
  3. 活了很久的session持有了空閒很久的連接, 這個連接其實已經被服務端銷燬了,因此已經不可用了,但是由於其一直沒有歸還到連接池中,因此一直沒有得到更新。
  4. 此時web服務收到數據請求,使用該線程中的該session中的連接,就會拋異常了,因爲連接已經不可用了。

一般來說,空閒時間很長以後,線程池裏所有線程的所有session的所有連接都會失效,因此就會完全無法通過orm訪問數據庫了。

相關異常信息:

1
2
MySQL server has gone away
Can't reconnect until invalid transaction is rolled back

這裏之所以說invalid transaction is rolled back,是因爲老session收到數據請求後,準備要用連接了。
而連接上的事務沒有自動提交,也沒有rollback,因此不能直接用。
因此嘗試把連接上,上一次請求的事務提交,但由於連接已經失效,所以失敗了。

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