因為在一篇博文上看到介紹“汽車之家介紹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-offset
和timestamp
兩種方式。
-
具體實現細節,就不一一貼程式碼了,有湊字數之嫌。
有興趣實現細節的,可以檢視這兩個commit記錄。大致就是這些改動。 -
完成程式碼:
編譯
程式碼實現完畢,本地編譯。
使用maven,常規操作。有兩個注意的點:
-
flink使用了
spotless
進行程式碼格式化檢測。修改了原始碼重新編譯如果程式碼格式不對,可能就是沒換行或者少了多了一個空格,就通過不了。編譯前,可以使用
'mvn spotless:apply
自動校正。 -
flink 使用了
Checkstyle
,一些程式碼使用了import static
,新增靜態引入後進行編譯時要注意。
測試
編譯成功後,可部署成單點或者偽叢集模式測試。
這裡採用本地測試。
- 將
flink-connector-kafka_2.11-1.14.0.jar
和flink-connector-kafka_2.11-1.14.0.xml
pom檔案手動放入或者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資料來源,並將處理後的資料寫入本地檔案。
執行結果,測試通過。
完