問題
在Django中使用mysql偶爾會出現資料庫連線丟失的情況,錯誤通常有如下兩種
1. `OperationalError: (2006, `MySQL server has gone away`)`
1. `OperationalError: (2013, `Lost connection to MySQL server during query`)`
查詢mysql全域性變數SHOW GLOBAL VARIABLES;
可以看到wait_timeout
,此變數表示連線空閒時間。如果客戶端使用一個連線查詢多次資料庫,如果連續查詢則沒有問題,如果查詢幾次後停頓超過wait_timeout
後再次查詢就會出現資料庫連線丟失。
復現
下面用Django復現下次問題:
- 將mysql的
wait_timeout
設定為10秒,然後進入django shell模擬查詢(以下錯誤資訊只保留了部分)
In[1]:import time
In[2]:from django.contrib.auth.models import User
In[3]:list(User.objects.filter(id=1))
Out[3]:[<User: admin>]
In[4]:time.sleep(15) # 模擬比較慢的程式碼(其中沒有查詢資料庫的程式碼),或者空閒什麼都不操作一段時間,此時間要比`wait_timeout`大一些
list(User.objects.filter(id=1))
Traceback (most recent call last):
File "<ipython-input-4-3574ae8220ee>", line 1, in <module>
list(User.objects.filter(id=1))
File "/usr/lib/python3.6/site-packages/pymysql/connections.py", line 1037, in _read_bytes
CR.CR_SERVER_LOST, "Lost connection to MySQL server during query")
django.db.utils.OperationalError: (2013, `Lost connection to MySQL server during query`)
尋求
那麼以上問題就基本說明了是空閒時間過長導致的錯誤。
django為了減少不必要的資料庫連線、關閉,複用了資料庫連線,當開始一個請求後建立一個連線池存放連線,之後此次請求都複用一個連線。那猜測就是django儲存連線的比wait_timeout
長了,如果儲存時間短一些就可以重新建立連線避免此錯誤了。
沒錯,官方文件也已經說明了此問題,設定資料庫 CONN_MAX_AGE
引數,示例:
DATABASES = {
"default": {
`ENGINE`: `django.db.backends.mysql`,
`NAME`: ``,
`USER`: ``,
`PASSWORD`: ``,
`HOST`: ``,
`CONN_MAX_AGE`: 9 # 比wait_timeout小一些
}
}
當我們測試後卻發現,事情並非想想中那麼簡單。為何錯誤依舊出現?這一切的背後, 是人性的扭曲還是道德的淪喪?敬請收看下節《突破》。
突破
對django原始碼中CONN_MAX_AGE
進行了一番搜尋,順藤摸瓜發現了django關閉失效連線的方法django.db.close_old_connections()
:
# Register an event to reset transaction state and close connections past
# their lifetime.
def close_old_connections(**kwargs):
for conn in connections.all():
conn.close_if_unusable_or_obsolete()
signals.request_started.connect(close_old_connections)
signals.request_finished.connect(close_old_connections)
重點在最後兩行,通過signal實現特定事件時執行此方法,兩個特定事件顧名思義是請求開始和請求結束。而我們報錯的是在一次請求中,所以此法通常無效,僅僅是實現每個請求關閉並重新建立連線。
解決
復現問題的django shell不要關閉,繼續執行如下程式碼:
In[5]:from django.db import close_old_connections
In[6]:close_old_connections()
In[7]:list(User.objects.filter(id=1))
Out[7]: [<User: admin>]
呼叫django.db.close_old_connections
後再次查詢就沒有錯誤了。
那麼我們要避免此錯誤就要執行每個資料庫查詢前呼叫django.db.close_old_connections
方法。
- 一般情況不會出現此類問題,因為一個請求中不間斷進行資料庫查詢,無需每個請求呼叫此方法,杞人憂天。
- 有時候一個請求中資料量較大,會查詢資料庫後進行一段時間其他(不涉及資料庫)處理,比如先查詢一些資料,然後將資料處理、生成excel、儲存檔案並生成url。已知此過長需要非常長時間,那麼最終url儲存資料庫就最好先呼叫
django.db.close_old_connections
防止連線丟失
題外話
實際上②所述情況最好從根本上解決處理慢的問題,也可以換作非同步處理,從根本上解決問題。