1. 程式人生 > >JDBC與MySQL臨時表空間的分析

JDBC與MySQL臨時表空間的分析

文章來源:愛可生雲資料庫

作者:秦沛、胡呈清

背景

應用 JDBC 連線引數採用 useCursorFetch=true,查詢結果集存放在 mysqld 臨時表空間中,導致ibtmp1 檔案大小暴增到90多G,耗盡伺服器磁碟空間。為了限制臨時表空間的大小,設定了:

innodb_temp_data_file_path = ibtmp1:12M:autoextend:max:2G

問題描述

在限制了臨時表空間後,當應用仍按以前的方式訪問時,ibtmp1檔案達到2G後,程式一直等待直到超時斷開連線。 SHOW PROCESSLIST顯示程式的連線執行緒為sleep狀態,state和info資訊為空。 這個對應用開發來說不太友好,程式等待超時之後要分析原因也缺少提示資訊。

問題分析過程

為了分析問題,我們進行了以下測試

測試環境:

  • mysql:5.7.16

  • java:1.8u162

  • jdbc 驅動:5.1.36

  • OS:Red Hat 6.4

1. 手工模擬臨時表超過最大限制的場景

模擬以下環境:

  • ibtmp1:12M:autoextend:max:30M

  • 將一張 500萬行的 sbtest 表的 k 欄位索引刪除

執行一條 group by 的查詢,產生的臨時表大小超過限制後,會直接報錯:

select sum(k) from sbtest1 group by k;  ERROR 1114 (HY000): The table '/tmp/#sql_60f1_0' is full 

2. 檢查驅動對 mysql 的設定

我們上一步看到,sql 手工執行會返回錯誤,但是 jdbc 不返回錯誤,導致連線一直 sleep,懷疑是 mysql 驅動做了特殊設定,驅動連線 mysql,通過 general_log 檢視做了哪些設定。未發現做特殊設定。

3. 測試 JDBC 連線

問題的背景中有對JDBC做特殊配置:useCursorFetch=true,不知道是否與隱藏報錯有關,接下來進行測試:

發現以下現象:

  • 加引數 useCursorFetch=true時,做同樣的查詢確實不會報錯

    這個引數是為了防止返回結果集過大而採用分段讀取的方式。即程式下發一個 sql 給 mysql 後,會等 mysql 可以讀結果的反饋,由於 mysql 在執行sql時,返回結果達到 ibtmp 上限後報錯,但沒有關閉該執行緒,該執行緒處理 sleep 狀態,程式得不到反饋,會一直等,沒有報錯。如果 kill 這個執行緒,程式則會報錯。

  • 不加引數 useCursorFetch=true時,做同樣的查詢則會報錯

結論

  1. 正常情況下,sql 執行過程中臨時表大小達到 ibtmp 上限後會報錯;

  2. 當JDBC設定 useCursorFetch=true,sql 執行過程中臨時表大小達到 ibtmp 上限後不會報錯。

解決方案

進一步瞭解到使用 useCursorFetch=true 是為了防止查詢結果集過大撐爆 jvm;

但是使用 useCursorFetch=true 又會導致普通查詢也生成臨時表,造成臨時表空間過大的問題;

臨時表空間過大的解決方案是限制 ibtmp1 的大小,然而 useCursorFetch=true 又導致JDBC不返回錯誤。

所以需要使用其它方法來達到相同的效果,且 sql 報錯後程序也要相應的報錯。除了 useCursorFetch=true 這種段讀取的方式外,還可以使用流讀取的方式。流讀取程式詳見附件部分。

  • 報錯對比

    • 段讀取方式,sql 報錯後,程式不報錯

    • 流讀取方式,sql 報錯後,程式會報錯

  • 記憶體佔用對比

這裡對比了普通讀取、段讀取、流讀取三種方式,初始記憶體佔用 28M 左右:

  • 普通讀取後,記憶體佔用 100M 多

  • 段讀取後,記憶體佔用 60M 左右

  • 流讀取後,記憶體佔用 60M 左右

補充知識點

MySQL共享臨時表空間知識點

MySQL 5.7在 temporary tablespace上做了改進,已經實現將 temporary tablespace 從 ibdata(共享表空間檔案)中分離。並且可以重啟重置大小,避免出現像以前 ibdata 過大難以釋放的問題。

其引數為:innodb_temp_data_file_path

1. 表現

MySQL啟動時 datadir 下會建立一個 ibtmp1 檔案,初始大小為 12M,預設值下會無限擴充套件:

通常來說,查詢導致的臨時表(如group by)如果超出 tmp_table_size、max_heap_table_size 大小限制則建立 innodb 磁碟臨時表(MySQL5.7預設臨時表引擎為 innodb),存放在共享臨時表空間;

如果某個操作建立了一個大小為100 M的臨時表,則臨時表空間資料檔案會擴充套件到 100M大小以滿足臨時表的需要。當刪除臨時表時,釋放的空間可以重新用於新的臨時表,但 ibtmp1 檔案保持擴充套件大小。

2. 查詢檢視

可查詢共享臨時表空間的使用情況:

SELECT FILE_NAME, TABLESPACE_NAME, ENGINE, INITIAL_SIZE, TOTAL_EXTENTS*EXTENT_SIZE AS TotalSizeBytes, DATA_FREE,MAXIMUM_SIZE FROM INFORMATION_SCHEMA.FILES WHERE TABLESPACE_NAME = 'innodb_temporary'\G

*************************** 1. row ***************************                 FILE_NAME:    /data/mysql5722/data/ibtmp1 TABLESPACE_NAME:    innodb_temporary                       ENGINE:    InnoDB              INITIAL_SIZE:    12582912             TotalSizeBytes:    31457280                DATA_FREE:    27262976         MAXIMUM_SIZE:    31457280 1 row in set (0.00 sec)

3. 回收方式

重啟 MySQL 才能回收

4. 限制大小

為防止臨時資料檔案變得過大,可以配置該 innodb_temp_data_file_path (需重啟生效)選項以指定最大檔案大小,當資料檔案達到最大大小時,查詢將返回錯誤:

innodb_temp_data_file_path=ibtmp1:12M:autoextend:max:2G

5. 臨時表空間與 tmpdir 對比

共享臨時表空間用於儲存非壓縮InnoDB臨時表(non-compressed InnoDB temporary tables)、關係物件(related objects)、回滾段(rollback segment)等資料;

tmpdir 用於存放指定臨時檔案(temporary files)和臨時表(temporary tables),與共享臨時表空間不同的是,tmpdir儲存的是compressed InnoDB temporary tables。

可通過如下語句測試:

CREATE TEMPORARY TABLE compress_table (id int, name char(255)) ROW_FORMAT=COMPRESSED;

CREATE TEMPORARY TABLE uncompress_table (id int, name char(255)) ;

附件

SimpleExample.java

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.sql.Statement;

import java.util.Properties;

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.atomic.AtomicLong;

public class SimpleExample {

public static void main(String[] args) throws Exception {

Class.forName("com.mysql.jdbc.Driver");

Properties props = new Properties();

props.setProperty("user""root");

props.setProperty("password""root");

SimpleExample engine = new SimpleExample();

//      engine.execute(props,"jdbc:mysql://10.186.24.31:3336/hucq?useSSL=false");

engine.execute(props,"jdbc:mysql://10.186.24.31:3336/hucq?useSSL=false&useCursorFetch=true");

}

final AtomicLong tmAl = new AtomicLong();

final String tableName="test";

public void execute(Properties props,String url) {

CountDownLatch cdl = new CountDownLatch(1);

long start = System.currentTimeMillis();

for (int i = 0; i < 1; i++) {

TestThread insertThread = new TestThread(props,cdl, url);

Thread t = new Thread(insertThread);

t.start();

System.out.println("Test start");

}

try {

cdl.await();

long end = System.currentTimeMillis();

System.out.println("Test end,total cost:" + (end-start) + "ms");

catch (Exception e) {

}

}

class TestThread implements Runnable {

Properties props;

private CountDownLatch countDownLatch;

String url;

public TestThread(Properties props,CountDownLatch cdl,String url) {

this.props = props;

this.countDownLatch = cdl;

this.url = url;

}

public void run() {

Connection connection = null;

PreparedStatement ps = null;

Statement st = null;

long start = System.currentTimeMillis();

try {

connection = DriverManager.getConnection(url,props);

connection.setAutoCommit(false);

st = connection.createStatement();

//st.setFetchSize(500);

st.setFetchSize(Integer.MIN_VALUE);  //僅修改此處即可

ResultSet rstmp;

st.executeQuery("select sum(k) from sbtest1 group by k"); 

rstmp = st.getResultSet();

while(rstmp.next()){

}

catch (Exception e) {

System.out.println(System.currentTimeMillis() - start);

System.out.println(new java.util.Date().toString());

e.printStackTrace();

finally {

if (ps != null)

try {

ps.close();

catch (SQLException e1) {

e1.printStackTrace();

}

if (connection != null)

try {

connection.close();

catch (SQLException e1) {

e1.printStackTrace();

}

this.countDownLatch.countDown();

}

}

}

}