Mysql臨時表突增問題定位與分析

一、問題現象

數據展示系統出現異常,首頁刷不出數據,查看日誌後發現模塊無法連接數據庫(從庫,以下數據庫都表示從庫),緊接着數據分析模塊出現報警,服務器出現磁盤空間不足報警。

image2020-9-4_11-42-20.png


查看AWS RDS監控發現,在時間段 ‘08.31 19:15:00 - 09.01 01:19:00’ 內,數據庫實例存儲空間急劇下降

image.png


監控顯示,數據庫連接數量沒有明顯增加,但在該時間段內磁盤IO明顯增大

磁盤IO

image.png

數據庫連接數量

image.png

通過查看實例上磁盤的佔用情況,發現磁盤臨時表佔用了大量空間(420G/500G),於是我們開始定位爲何會有大量的臨時表被持久化到磁盤上

image.png



二、問題分析與復現

2.1 臨時表

MySQL中有兩種臨時表:外部臨時表和內部臨時表。其中外部臨時表可有用戶查詢時手動創建保存一些中間數據,提升查詢效率等;

內部臨時表會在查詢過程中由Mysql自動創建並存儲某些中間操作的結果,這種操作可能出現在優化階段或者執行階段,所以這種臨時表對用戶不可見,一般通過EXPLAIN可以查看查詢語句是否用到了內部臨時表。

而內部臨時表又可以被分爲磁盤臨時表和內存臨時表,Mysql在使用內部臨時表時會優先使用內存臨時表,即將臨時表存放在內存裏,當臨時表過大或內存不夠用時,就會轉化成磁盤臨時表,存放在ibtmp1文件中。

在Mysql5.7中,內存臨時表在查詢結束後會被釋放,而文件ibtmp1佔用的空間不會被自動釋放,但會被標記刪除,等數據庫重啓時釋放這部分空間。

(如何釋放?數據庫服務關閉時,會直接刪除ibtmp1文件,等數據庫重啓時,會新建一個分配了初始空間新ibtmp1文件)

2.2 相關配置項

  • 內存臨時表可使用的內存空間(min('max_heap_table_size', 'tmp_table_size')):16M
    image.png


  • 初始ibtmp1文件大小(innodb_temp_data_file_path):12M且增長無上限
    image.png


  • ibtmp1文件信息(被擴展的總空間) = TOTAL_EXTENTS(拓展次數) * EXTENT_SIZE(每次擴展的大小): 204次 * 1M/次 = 204M
    image.png

2.3 問題分析

查看mysql的monitor device_base和device_bt_base的rate暴漲,插入等正常,由此猜測是這兩個表查詢引起的臨時表使用空間暴漲,查看是事故時間的query,pharos請求相較平成更加頻繁,由此猜測是併發量太大導致,接着預本地模擬,嘗試重現事故。

2.5 問題復現

  • 搭建數據庫實例Mysql5.7.31
  • 創建測試表

    CREATE TABLE `account` (
      `user_id` bigint(20) NOT NULL,
      `address` varchar(255) DEFAULT NULL,
      `create_time` datetime DEFAULT NULL,
      `email` varchar(255) NOT NULL,
      `nickname` varchar(255) DEFAULT NULL,
      `password` varchar(255) NOT NULL,
      `update_time` datetime DEFAULT NULL,
      PRIMARY KEY (`user_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;


  • 插入測試數據約30W條,表大小爲28.56M
  • 開始測試
    1.執行會使用臨時表的查詢語句,且不顯示釋放客戶端,單線程查詢3000次

    def groupData(k):
        i = 0
        a = []
        while i < 3000:
            i = i + 1
            conn = pymysql.connect(host='127.0.0.1', port=3307, user='root', password='123456', db='mysql')
            cur = conn.cursor()
            group_sql = 'SELECT address, COUNT(*) as count from account where user_id > %s GROUP BY  address' % (3000 + i / 2 * 10000)
            print(group_sql)
            cur.execute(group_sql)
        for i in a:
            print(len(i))
        time.sleep(100)
        print("stop")


          執行結果爲:ibtmp1文件未進行任何擴展,查詢結束後依然爲12M。表明每次查詢的臨時表都使用了內存空間,執行完成之後空間被釋放

         image.png

  •  
    2.依然使用上面的函數groupData進行查詢,但改用多線程併發執行,查詢總次數一致,用20個線程每個查詢150次

    def groupData(k):
        i = 0
        a = []
        while i < 150:
            i = i + 1
            conn = pymysql.connect(host='127.0.0.1', port=3307, user='root', password='123456', db='mysql')
            cur = conn.cursor()
            group_sql = 'SELECT address, COUNT(*) as count from account where user_id > %s GROUP BY  address' % (3000 + k / 2 * 10000)
            print(group_sql)
            cur.execute(group_sql)
        for i in a:
            print(len(i))
        time.sleep(100)
        print("stop")
    
    
    if __name__ == '__main__':
        with ThreadPoolExecutor(max_workers=40) as e:
            for k in range(40):
                e.submit(groupData, k)

    執行結果爲:ibtmp1文件進行了460次擴展,查詢結束後ibtmp1文件大小爲460M。表明查詢時大量內部臨時表使用了磁盤臨時表


    image.png
  • 原因分析
    不同客戶端併發訪問Mysql進行查詢時,產生的內存臨時表會暫時佔據已分配的內存空間,後面查詢產生的臨時表因爲沒有足夠的內存空間而改用磁盤臨時表空間
    而磁盤空間不會因爲客戶端斷開而釋放空間,最終導致磁盤空間被消耗殆盡。


2.6 問題解決

  • 導致產生本次問題的SQL語句如下。device_type和config_model字段沒有索引,查詢會產生較大的臨時表。需要對該字段添加索引避免產生臨時表。

    SELECT device_type, config_model, count(*) AS rowCount FROM device_base  GROUP BY device_type, config_model


          爲何爲忽然出現該問題?
          系統中利用緩存限制訪問數據庫的次數,併發訪問時會通過緩存獲取數據。但由於更新緩存的操作沒有加鎖,當緩存大面積失效且有大量請求時,出現該問題


  • 數據庫實例參數設置
    目前我們的數據庫臨時表空間的參數爲默認參數,即初始大小爲12M,可擴展大小無限制。可考慮設置擴展大小上限,避免磁盤被直接耗盡影響到所有的數據庫操作。

    innodb_temp_data_file_path = ibtmp1:12M:autoextend:max:20000M








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