《Flink SQL任務自動生成與提交》後續:修改flink源碼實現kafka connector BatchMode

因爲在一篇博文上看到介紹“汽車之家介紹flink數據平臺”中提到“基於 SQL 的開發流程”。基於kafka connector,通過source,sink,transformation三條sql完成數據接入,邏輯轉換處理,結果落地三步工作。出於興趣,自己去簡(粗)單(糙)實現了這其中的一個小功能。相關的博文在這裏,相關的代碼上傳到github

簡單說,通過kafka connector用3條sql實現如圖所示功能:

但是實現的過程中也遇到了兩個問題。

問題

  • 截止到目前最新的flink版本在kafka connector也只支持 inStreamingMode,並不支持inBatchMode。不能實現汽車之家通過kafka connector來實現每日的定時統計。

https://nightlies.apache.org/flink/flink-docs-release-1.14/docs/connectors/table/kafka/

如圖,只支持unbounded無界數據流,不支持bounded有界數據即batchmode.

  • 在sink的時候是隻支持append模式,而在append模式下,不支持group by,因爲使用了group by 會改行結果行。

    那麼按照汽車之家每日統計PV,UV的需求必然需要使用到group by.按目前flink的最新版本也是辦不到的。

    如果強行使用group by 將會拋出異常:
    目前sink只支持append模式,如果使用了group by 等會改變結果行,會報錯:AppendStreamTableSink doesn't support consuming update changes which is produced by node GroupAggregate

對於spark和flink這種絕對主流的大數據框架,稍微上點規模的公司應該都有維護自己的內部分支,基於自家的業務做一些定製化開發。汽車之家應該也不例外。

所以以上功能flink社區版不能做到,汽車之家應該是基於內部的實現。

以上是背景介紹。

基於該功能並不算特別複雜,花了兩天業餘時間實現了。

思路

  • kafka參數問題

    要接入kafka,就要設置kafka連接信息,起始信息,以及結束信息.

    開始信息可選參數如下:

    參數名 參數值
    scan.startup.mode 可選值:'earliest-offset', 'latest-offset', 'group-offsets', 'timestamp' and 'specific-offsets'
    scan.startup.specific-offsets 指定每個分區的偏移量,比如:'partition:0,offset:42;partition:1,offset:300'
    scan.startup.timestamp-millis 直接指定開始時間戳,long類型

    依葫蘆畫瓢,去除earliest-offset,結束信息可選參數可設置成:

    參數名 參數值
    scan.endup.mode 可選值:'latest-offset', 'group-offsets', 'timestamp' and 'specific-offsets'
    scan.endup.specific-offsets 指定每個分區的偏移量,比如:'partition:0,offset:42;partition:1,offset:300'
    scan.sendup.timestamp-millis 直接指定結束時間戳,long類型
  • 支持batchmode的問題

    這裏涉及到一個版本的問題。flink kafka connector API在最近幾個版本變化挺大的。就內部實現而言,1.13和1.14也有不小的變化。

    比如,判斷當前任務是否有界時,1.13版本是直接寫爲false

    而在1.14版本變成了可動態判斷並設置
    public boolean isBounded() { return kafkaSource.getBoundedness() == Boundedness.BOUNDED; }

    可以看到這裏通過kafkaSource.getBoundedness()獲取當前任務是否有界,點進KafkaSource,對於boundedness屬性,既然有getter那必然有setter啊。

    果不其然,在KafkaSourceBuilder類中提供了setBounded方法。

    這裏還有意外驚喜,這個方法不但提供了設置bounded的功能,還能直接設置結束的參數。那麼上一個kafka參數問題解決了定義問題,而在這裏就解決了設置的問題。

  • 參數提交至kafkasource的問題

    參數問題分爲定義提交。定義在第1部份已經解決,提交就是第2部份的setBounded,但在哪裏觸發呢?

    KafkaDynamicSource.createKafkaSource的方法裏。這裏可仿照switch(startupMode)寫一個switch(endupuMode),在裏面的分支去實現各種參數情況LATEST,TIMESTAMP,再在各個分支裏設置kafka參數。

    在case分支我們可以仿照kafkaSourceBuilder.setStartingOffsets實現一個kafkaSourceBuilder.setEndOffsets。在flink 1.13就得這麼實現。但在flink 1.14,經過前面的分析得知setBounded可設置結束參數。一舉兩得。

  • group by支持問題

    經過分析,我發現這個問題屬於是庸人自擾。

    AppendStreamTableSink doesn't support group by僅在streamingmode模式下,batchmode不存在改變結果行的問題,所以,只要改成了batchmode,天然的就不存在group by 異常問題。

實現

選定flink 1.14版本,fork,拉取到本地,新建分支。

目前scan.endup.mode 只支持latest-offsettimestamp兩種方式。

編譯

代碼實現完畢,本地編譯。

使用maven,常規操作。有兩個注意的點:

  • flink使用了spotless進行代碼格式化檢測。修改了源碼重新編譯如果代碼格式不對,可能就是沒換行或者少了多了一個空格,就通過不了。

    編譯前,可以使用'mvn spotless:apply自動校正。

  • flink 使用了Checkstyle,一些代碼使用了import static,添加靜態引入後進行編譯時要注意。

測試

編譯成功後,可部署成單點或者僞集羣模式測試。
這裏採用本地測試。

  • flink-connector-kafka_2.11-1.14.0.jarflink-connector-kafka_2.11-1.14.0.xmlpom文件手動放入或者mvn install本地倉庫。
  • 我測試的時候,需手動引用kafka-clients依賴。這點我不保證。

確保將重新編譯後的jar包引入項目

測試代碼:

{
        EnvironmentSettings fsSettings = EnvironmentSettings.newInstance()
                .inBatchMode()
//                .inStreamingMode()
                .build();

        TableEnvironment te = TableEnvironment.create(fsSettings);

        String kafkaSql = "CREATE TABLE kafkatable (\n" +
                "  key STRING," +
                "  ts TIMESTAMP" +
                ") WITH (\n" +
                " 'connector' = 'kafka',\n" +
                " 'topic' = 'xxx',\n" +
                " 'properties.bootstrap.servers' = 'xxx.xx.xxx.xxx:9092',\n" +
                " 'properties.group.id' = 'xxx',\n" +
//                -- optional: valid modes are "earliest-offset",
//                -- "latest-offset", "group-offsets",
//                -- or "specific-offsets"
                "  'scan.startup.mode' = 'earliest-offset',\n" +
                "  'scan.endup.mode' = 'latest-offset',\n" +
//                "  'scan.endup.mode' = 'timestamp',\n" +
//                "  'scan.endup.timestamp-millis' = '1641974234163',\n" +
//                "  'scan.endup.mode' = 'latest-offset',\n" +
//                "'scan.startup.specific-offsets' = 'partition:0,offset:20'," +
//                " 'connector.specific-offsets.0.partition' = '0',"+
//                "'connector.specific-offsets.0.offset' = '1',"+
                " 'format' = 'json',\n" +
                " 'json.fail-on-missing-field' = 'false',\n" +
                " 'json.ignore-parse-errors' = 'true'\n" +
                ")";
        te.executeSql(kafkaSql);

        String sqlFile = "CREATE TABLE fs_table (\n" +
                " dt VARCHAR,\n" +
                "    pv BIGINT,\n" +
                "    uv BIGINT" +
                ") WITH (\n" +
                "  'connector'='filesystem',\n" +
                "  'path'='d://path',\n" +
                "  'format'='json',\n" +
                "  'sink.partition-commit.delay'='1 s',\n" +
                "  'sink.partition-commit.policy.kind'='success-file'\n" +
                ")";
        te.executeSql(sqlFile);


        te.executeSql("INSERT INTO fs_table\n" +
                "SELECT\n" +
                "  'as' as dt,\n" +
                "  COUNT(*) AS pv,\n" +
                "  COUNT(DISTINCT key) AS uv\n" +
                "FROM kafkatable group by key\n").print();
    }

測試代碼的sql邏輯僅爲測試。不要追究爲什麼count(*)是pv,隨手寫的。

測試代碼scan.endup.mode設置爲latest-offset。如果要實現最開始的按天統計,如下設置。scan.startup.mode同理。

"  'scan.endup.mode' = 'timestamp',\n" +
"  'scan.endup.timestamp-millis' = '1641974234163',\n" +

這段代碼測試通過inBatchMode引入kafka數據源,並將處理後的數據寫入本地文件。
運行結果,測試通過。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章