深度分析 | JDBC與MySQL臨時表空間的分析

原創做者:秦沛、胡呈清java


背景

應用 JDBC 鏈接參數採用 useCursorFetch=true,查詢結果集存放在 mysqld 臨時表空間中,致使ibtmp1 文件大小暴增到90多G,耗盡服務器磁盤空間。爲了限制臨時表空間的大小,設置了:mysql

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

問題描述

在限制了臨時表空間後,當應用仍按之前的方式訪問時,ibtmp1文件達到2G後,程序一直等待直到超時斷開鏈接。 SHOW PROCESSLIST顯示程序的鏈接線程爲sleep狀態,state和info信息爲空。 這個對應用開發來講不太友好,程序等待超時以後要分析緣由也缺乏提示信息。服務器

問題分析過程

爲了分析問題,咱們進行了如下測試jvm

測試環境:測試

  • mysql:5.7.16this

  • java:1.8u162atom

  • jdbc 驅動:5.1.36url

  • OS:Red Hat 6.4spa

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();
}
}
}
}
相關文章
相關標籤/搜索