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

1、問題現象

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

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


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

image.png


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

磁盤IO緩存

image.png

數據庫鏈接數量服務器

image.png

經過查看實例上磁盤的佔用狀況,發現磁盤臨時表佔用了大量空間(420G/500G),因而咱們開始定位爲什麼會有大量的臨時表被持久化到磁盤上多線程

image.png



2、問題分析與復現

2.1 臨時表

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

內部臨時表會在查詢過程當中由Mysql自動建立並存儲某些中間操做的結果,這種操做可能出如今優化階段或者執行階段,因此這種臨時表對用戶不可見,通常經過EXPLAIN能夠查看查詢語句是否用到了內部臨時表。ide

而內部臨時表又能夠被分爲磁盤臨時表和內存臨時表,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
相關文章
相關標籤/搜索