flink同步MySQL資料的時候出現記憶體溢位

实习小生發表於2024-10-17

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也不會自動新增的,需要手動加,不然資料就是一條一條插入的,這個坑我也踩了

相關文章