flink同步MySQL資料的時候出現記憶體溢位
背景:需要將1000w的某型別資料同步到別的資料來源裡面,使用公司的大資料平臺可以很快處理完畢,而且使用的記憶體只有很少很少量(公司的大資料平臺的底層是flink,但是聯結器使用的是chunjun開源產品),由於我個人想使用flink原生的聯結器來嘗試一下,所以就模擬了1000w的資料,然後啟動了flink單節點,透過flinksql的方式提交了同步任務,最終結果記憶體溢位!!!
下面的問題是在使用MySQL資料來源的時候出現的,別的資料來源可能不會有這個問題
下面是在main方法裡面寫的flink程式碼
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.slf4j.LoggerFactory;
import java.util.List;
public class Main2 {
static {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
List<Logger> loggerList = loggerContext.getLoggerList();
loggerList.forEach(logger -> {
logger.setLevel(Level.INFO);
});
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
StreamExecutionEnvironment streamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment(configuration);
streamExecutionEnvironment.setParallelism(1);
StreamTableEnvironment streamTableEnvironment = StreamTableEnvironment.create(streamExecutionEnvironment);
// 定義目標表
streamTableEnvironment.executeSql("CREATE TABLE `gsq_hsjcxx_pre_copy1` (\n" +
" `reportid` BIGINT COMMENT 'reportid',\n" +
" `sfzh` VARCHAR COMMENT 'sfzh',\n" +
" `cjddh` VARCHAR COMMENT 'cjddh',\n" +
" `cjsj` VARCHAR COMMENT 'cjsj',\n" +
" PRIMARY KEY (`reportid`) NOT ENFORCED\n" +
") WITH (\n" +
" 'connector' = 'jdbc',\n" +
" 'url' = 'jdbc:mysql://127.0.0.1:3306/xxx?useSSL=false&useInformationSchema=true&nullCatalogMeansCurrent=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&',\n" +
" 'table-name' = 'xxx',\n" +
" 'username' = 'xxx',\n" +
" 'password' = 'xxx',\n" +
" 'sink.buffer-flush.max-rows' = '1024'\n" +
")");
// 定義源表
streamTableEnvironment.executeSql("CREATE TABLE `gsq_hsjcxx_pre` (\n" +
" `reportid` BIGINT COMMENT 'reportid',\n" +
" `sfzh` VARCHAR COMMENT 'sfzh',\n" +
" `cjddh` VARCHAR COMMENT 'cjddh',\n" +
" `cjsj` VARCHAR COMMENT 'cjsj'\n" +
") WITH (\n" +
" 'connector' = 'jdbc',\n" +
" 'url' = 'jdbc:mysql://127.0.0.1:3306/xxx?useSSL=false&useInformationSchema=true&nullCatalogMeansCurrent=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai',\n" +
" 'table-name' = 'xxx',\n" +
" 'username' = 'xxx',\n" +
" 'password' = 'xxx',\n" +
" 'scan.fetch-size' = '1024'\n" +
")");
// 將源表資料插入到目標表裡面
streamTableEnvironment.executeSql("INSERT INTO `gsq_hsjcxx_pre_copy1` (`reportid`,\n" +
" `sfzh`,\n" +
" `cjddh`,\n" +
" `cjsj`)\n" +
"(SELECT `reportid`,\n" +
" `sfzh`,\n" +
" `cjddh`,\n" +
" `cjsj`\n" +
" FROM `gsq_hsjcxx_pre`)");
streamExecutionEnvironment.execute();
}
}
以上是一個簡單的示例,定義了三個sql語句,首先是定義兩個資料來源,然後再進行查詢插入操作,執行之後就會開始執行flinksql。
如果在啟動的時候指定jvm的記憶體大小為 -Xms512m -Xmx1g,會發現壓根啟動不起來,直接就oom了。
如果不指定jvm記憶體的話,則程式能啟動,記憶體的使用量會慢慢的升高,甚至要使用將近4G記憶體,如果在flink叢集上執行的話,直接會oom的。
先說flink讀取資料的流程,flink讀取資料的時候是分批讀取的,不可能一次性把資料全部讀出來的,但是透過現象來看是flink讀取資料的時候,所有資料都在記憶體裡面的,這個現象是不合理的。
分析原始碼
透過除錯模式分析程式碼是怎麼走的,經過一番除錯之後發現了一下程式碼
public void openInputFormat() {
try {
Connection dbConn = this.connectionProvider.getOrEstablishConnection();
if (this.autoCommit != null) {
dbConn.setAutoCommit(this.autoCommit);
}
this.statement = dbConn.prepareStatement(this.queryTemplate, this.resultSetType, this.resultSetConcurrency);
if (this.fetchSize == -2147483648 || this.fetchSize > 0) {
this.statement.setFetchSize(this.fetchSize);
}
} catch (SQLException var2) {
throw new IllegalArgumentException("open() failed." + var2.getMessage(), var2);
} catch (ClassNotFoundException var3) {
throw new IllegalArgumentException("JDBC-Class not found. - " + var3.getMessage(), var3);
}
}
先說下flink是怎麼是如果分批拉取資料的,flink是使用的遊標來分批拉取資料,那麼這個時候就要確定是否真正使用了遊標。
於是乎,我寫了一個原生的JDBC程式讀取資料的程式(沒有限制jvm記憶體)
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class Main3 {
public static void main(String[] args) {
Connection connection = null;
Runtime runtime = Runtime.getRuntime();
System.out.printf("啟動前總記憶體>%s 使用前的空閒記憶體>%s 使用前最大記憶體%s%n", runtime.totalMemory() / 1024 / 1024, runtime.freeMemory() / 1024 / 1024, runtime.maxMemory() / 1024 / 1024);
try {
int i = 0;
connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/xxx?useSSL=false&useInformationSchema=true&nullCatalogMeansCurrent=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useCursorFetch=true", "xxx", "xxx");
connection.setAutoCommit(false);
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `reportid`,\n" +
" `sfzh`,\n" +
" `cjddh`,\n" +
" `cjsj`\n" +
" FROM `gsq_hsjcxx_pre`", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
// 每批拉取的資料量
preparedStatement.setFetchSize(1024);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
i++;
}
System.out.printf("啟動前總記憶體>%s 使用前的空閒記憶體>%s 使用前最大記憶體%s%n", runtime.totalMemory() / 1024 / 1024, runtime.freeMemory() / 1024 / 1024, runtime.maxMemory() / 1024 / 1024);
System.out.println("資料量> " + i);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
最終列印的結果是
很顯然,資料是全部讀取出來的,這個時候需要確認的程式是不是真正使用了遊標,經過一番檢視後發現,需要在jdbc的引數裡面加上&useCursorFetch=true,才能使遊標生效
修改完jdbc引數之後,問題就得到了完全的結局
除此之外我用過apahce的seatunnel,這個同步資料的時候是真的快,快的離譜。不過使用的時候可能會漏掉一些jdbc相關的引數(MySQL為例)
"rewriteBatchedStatements" : "true" 這個批次的引數 apache seatunnel也不會自動新增的,需要手動加,不然資料就是一條一條插入的,這個坑我也踩了