“MySQL server has gone away” in django ThreadPoolExecutor

原文鏈接:MySQL-server-has-gone-away-in-django-ThreadPoolExecutor

MySQL server has gone away報錯

最近碰到MySQL server has gone away的報錯,報錯出現的現象是:

  • 生產環境周末到周一上午會出現一些間斷的報錯,晚些恢復正常
  • 測試環境每天上午會出現間斷報錯,晚些恢復正常
  • 出錯的場景都是通過ThreadPoolExecutor執行的異步任務中執行mysql查詢操作,其他查詢操作正常
  • 只有一部分異步任務會出現報錯,一部分執行正常

項目基本情況:基于python3.5+django1.8,數據庫mysql,生產和測試環境都是通過nginx+uwsgi部署

分析原因

谷歌了一下MySQL server has gone away問題可能的原因:

  1. MySQL服務宕了
  2. 連接超時
  3. 進程在server端被主動kill
  4. SQL語句太長

再結合項目實際情況逐條分析:

  • 首先第一條,“MySQL服務宕了”,查看mysql的報錯日志,看有沒有重啟信息,同時查看mysql的運行時長。
mysql> show global status like 'uptime';
+---------------+----------+
| Variable_name | Value    |
+---------------+----------+
| Uptime        | 13881221 |
+---------------+----------+
1 row in set (0.00 sec)

報錯日志中并沒有服務重啟的信息,同時uptime值很大,表示已經運行很長時間。因此第一條原因可以排除

  • 第三條,“進程在server端被主動kill”,當有長時間的慢查詢時執行kill導致。查一下慢查詢數量。
mysql> show global status like 'com_kill';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_kill      | 12    |
+---------------+-------+
1 row in set (0.00 sec)

Com_kill居然有這么多-_-||,但也不確定出錯的查詢語句是否是慢查詢。找了一下報錯代碼的查詢語句,屬于索引查詢,而且查詢時間不超過100ms,因此,這一條也可以排除。

  • 第四條,SQL語句太長導致。直接找到報錯的查詢語句,屬于普通查詢長度,所以這一條應該也可以排除。

  • 那么第二條,連接超時,當某個連接很長時間沒有新的請求了,達到了mysql服務器的超時時間,被強行關閉,再次使用該連接時,其實已經失效,就會出現"MySQL server has gone away"的報錯了。首先看一下數據庫連接的最大超時時間設置多大。

mysql> show global variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout  | 86400 |
+---------------+-------+
1 row in set (0.00 sec)

目前設置的最大超時時間是24小時,也就是在這24小時內有數據庫連接超時,超時連接后面又被用到,導致報錯。

之前在django數據庫連接中分析了django的數據庫連接是基于線程(thread.local)創建的全局變量,即線程本地變量,下面簡稱為線程變量。

  • 對于常規請求,要獲取數據庫連接時,會首先查看當前線程變量中是否有可用連接,沒有就創建并保存到線程變量中。每個request在開始和結束之前,會檢查當前線程的數據庫連接是否可用,并關閉不可用連接。這樣就保證了每次請求獲取到的一定是可用的數據庫連接。
  • 對于基于ThreadPoolExecutor的異步任務,線程池是在項目啟動時創建的,當其中的線程被調起執行異步任務時,首次查詢數據庫時創建數據庫連接,后續會一直保存在該異步線程中,該線程也會一直保存在線程池中。
    由于沒有像常規請求一樣的在開始和結束之前檢查數據庫連接是否可用的機制,線程池中的線程保存的數據庫連接也許是不可用的,就導致下次被調起執行數據庫操作時出現“MySQL server has gone away”的報錯。

再結合出錯現象分析一下:

  • 每到周末,生產環境活躍的用戶數量減少,尤其是涉及到異步任務的業務場景觸發減少,所以到周末線程池中的線程保存的部分數據庫連接閑置超過24小時,周一上班后用戶活躍增加,失效連接被調起,自然就會報錯了。
  • 線程池中的每個線程被調起的時機和次數都不同,某些線程最近剛被調起過,某些線程長時間沒被調起,所以數據庫連接失效,因此會出現部分異步任務報錯的現象。
  • 出現報錯后一段時間自己恢復正常,而ThreadPoolExecutor本身也沒有斷掉異常連接或者殺掉線程的機制,但是查看業務日志,發現恢復正常后執行異步任務的線程和之前報錯的線程不同,也就是基于某種機制,之前的線程被kill了。

看一下ThreadPoolExecutor中創建線程的邏輯:

def _adjust_thread_count(self):
    # When the executor gets lost, the weakref callback will wake up
    # the worker threads.
    def weakref_cb(_, q=self._work_queue):
        q.put(None)
    # TODO(bquinlan): Should avoid creating new threads if there are more
    # idle threads than items in the work queue.
    if len(self._threads) < self._max_workers:
        t = threading.Thread(target=_worker,
                             args=(weakref.ref(self, weakref_cb),
                                   self._work_queue))
        # 線程被設為守護線程
        t.daemon = True
        t.start()
        self._threads.add(t)
        _threads_queues[t] = self._work_queue

線程池中創建的線程屬于守護線程,當主線程退出,子線程也會跟著退出。而子線程是在調用submit方法提交異步任務時,若線程池中實際線程數量小于指定數量,便會創建。因此主線程是請求線程。

在用uWSGI部署的django項目中,請求線程是由uWSGI分配的。uWSGI會根據配置文件中的process, threads參數決定開多少工作進程和子線程,同時還有max-requests參數,表示為每個工作進程設置的請求數上限。
當該工作進程請求數達到這個值,就會被回收重用(重啟),其子線程也會重啟。所以上面的報錯現象中,其實是工作進程重啟了,請求子線程也會重建,導致線程池中的守護線程也會被kill了,報錯就停止了。

總結一下原因

  • 由于django的數據庫連接是保存到線程本地變量中的,通過ThreadPoolExecutor創建的線程會保存各自的數據庫連接。當連接被保存的時間超過mysql連接的最大超時時間,連接失效,但不會被線程釋放。之后再調起線程執行涉及到數據庫操作的異步任務時,會用到失效的數據庫連接,導致報錯。
  • 又由于uWSGI的工作進程達到max-requests數量而重啟,導致請求線程重啟,線程池中的線程是根據請求線程創建的守護線程,因此會被kill,所以后面自己恢復正常。

解決方案

要解決這個問題,最直接的辦法是在線程池的所有異步任務中,在執行數據庫操作之前,檢查數據庫連接是否可用,然后關掉不可用連接。

from threading import local

def close_old_connections():
    # 獲取當前線程本地變量
    connections = local()
    # 根據數據庫別名獲取數據庫連接
    if hasattr(connections, 'default'):
        conn = getattr(connections, 'default')
        # 檢查連接可用性,并關閉不可用連接
        conn.close_if_unusable_or_obsolete()

或者改寫一下django獲取和保存數據庫連接的機制,可以創建一個全局的數據庫連接池,不管是常規請求還是異步任務,都從連接池獲取數據庫連接,由連接池保證數據庫連接的數量和可用性。



參考閱讀
MySQL server has gone away報錯原因分析
django數據庫連接
WSGI & uwsgi

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容