Django (2006, 'MySQL server has gone away') 本地重現與解決


最近我們的Django項目供Java Sofa應用進行tr調用時, 經常會出現一個異常: django.db.utils.OperationalError: (2006, 'MySQL server has gone away'). 本文記錄了分析, 本地重現與解決此問題的全過程.

原因分析:

Django在1.6引入長鏈接(Persistent connections)的概念, 可以在一個HTTP請求中一直用同一個連接對數據庫進行讀寫操作.
但我們的應用對數據庫的操作太不頻繁了, 兩次操作數據庫的間隔大於MySQL配置的超時時間(默認為8個小時), 導致下一次操作數據庫時的connection過期失效.

Our databases have a 300-second (5-minute) timeout on inactive connections. That means, if you open a connection to the database, and then you don’t do anything with it for 5 minutes, then the server will disconnect, and the next time you try to execute a query, it will fail.

重現問題:

設置mysql wait_timeout為10s

在macOS上的mysql配置文件路徑: /usr/local/etc/my.cnf

1
2
3
4
5
6
# Default Homebrew MySQL server config
[mysqld]
# Only allow connections from localhost
bind-address = 127.0.0.1
wait_timeout = 10
interactive_timeout = 10

 

重啟mysql:

1
2
3
4
➜  ~ brew services restart mysql
Stopping `mysql`... (might take a while)
==> Successfully stopped `mysql` (label: homebrew.mxcl.mysql)
==> Successfully started `mysql` (label: homebrew.mxcl.mysql)

 

檢查wait_timeout的值是否已被更新.

1
2
3
4
5
6
7
8
9
mysql> show variables like '%wait_timeout%';
+--------------------------+----------+
| Variable_name | Value |
+--------------------------+----------+
| innodb_lock_wait_timeout | 50 |
| lock_wait_timeout | 31536000 |
| wait_timeout | 10 |
+--------------------------+----------+
3 rows in set (0.00 sec)

 

重現exception:

1
2
3
4
5
6
7
8
9
10
11
12
>>> XXX.objects.exists()
True
>>> import time
>>> time.sleep(15)
>>> XXX.objects.exists()
True
>>> XXX.objects.exists()
...
django.db.utils.OperationalError: (2013, 'Lost connection to MySQL server during query')
>>> XXX.objects.exists()
...
django.db.utils.OperationalError: (2006, 'MySQL server has gone away')

有意思的一個點是, sleep 10s 之后, 第一次操作數據庫, 會出現(2013, 'Lost connection to MySQL server during query’)異常. 之后再操作數據庫, 才會拋出(2006, 'MySQL server has gone away’)異常.

解決問題:

第一個最暴力的方法就是增加mysql的wait_timeout讓mysql不要太快放棄連接. 感覺不太靠譜, 因為不能杜絕這種Exception的發生.

第二個辦法就是手動把connection直接關閉:

1
2
3
4
5
6
7
8
>>> Alarm.objects.exists()
True
>>> from django.db import connection
>>> connection.close()
>>> time.sleep(10)
>>> Alarm.objects.exists()
True
>>>

 

發現不會出現(2006, 'MySQL server has gone away’)異常了, 但總感覺還是不夠優雅.
最終決定在客戶端(Django), 設置超時時間(CONN_MAX_AGE: 5)比mysql服務端(wait_timeout = 10)小:

1
2
3
4
5
6
7
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'CONN_MAX_AGE': 5,
<other params here>
}
}

 

但很奇怪沒有生效??? 看了源代碼, 發現只有在request_started(HTTP request)和request_finished的時候, 在close_if_unusable_or_obsolete才用到CONN_MAX_AGE並去驗證時間關閉connection.

具體代碼見: python3.6/site-packages/django/db/__init__.py#64

1
2
3
4
5
6
7
8
9
# 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)

 

而我的代碼是處理一個任務而不是HTTP請求, 所以不會觸發這個signal. 於是我寫了一個裝飾器, 在任務的開始和結束的時候, 關閉所有數據庫連接.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from django.db import connections


# ref: django.db.close_old_connections
def close_old_connections():
for conn in connections.all():
conn.close_if_unusable_or_obsolete()


def handle_db_connections(func):
def func_wrapper(request):
close_old_connections()
result = func(request)
close_old_connections()

return result

return func_wrapper

# ------割-------
@handle_db_connections
def process_trsbrain_request(request):
...

 

ps. CONN_MAX_AGE默認其實為0, 意味着默認在http請求和結束時會關閉所有數據庫連接.

其他:

django.db中connection和connections的區別???

  1. connection對應的是默認數據庫的連接, 用代碼表示就是connections[DEFAULT_DB_ALIAS]
  2. connections對應的是setting.DATABASES中所有數據庫的connection


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM