Upsert Kafka Connector - 讓實時統計更簡單

點擊上方 藍色字體 ,選擇“ 設爲星標
回覆”資源“獲取更多資源


在某些場景中,例如讀取 compacted topic 或者輸出(更新)聚合結果的時候,需要將 Kafka 消息記錄的 key 當成主鍵處理,用來確定一條數據是應該作爲插入、刪除還是更新記錄來處理。爲了實現該功能,社區爲 Kafka 專門新增了一個 upsert connector(upsert-kafka),該 connector 擴展自現有的 Kafka connector,工作在 upsert 模式(FLIP-149)下。新的 upsert-kafka connector 既可以作爲 source 使用,也可以作爲 sink 使用,並且提供了與現有的 kafka connector 相同的基本功能和持久性保證,因爲兩者之間複用了大部分代碼。

要使用 upsert-kafka connector,必須在創建表時定義主鍵,併爲鍵(key.format)和值(value.format)指定序列化反序列化格式。

一、Upsert Kafka Connector是什麼?

Upsert Kafka 連接器支持以 upsert 方式從 Kafka topic 中讀取數據並將數據寫入 Kafka topic。

作爲 source,upsert-kafka 連接器生產 changelog 流,其中每條數據記錄代表一個更新或刪除事件。更準確地說,數據記錄中的 value 被解釋爲同一 key 的最後一個 value 的 UPDATE,如果有這個 key(如果不存在相應的 key,則該更新被視爲 INSERT)。用表來類比,changelog 流中的數據記錄被解釋爲 UPSERT,也稱爲 INSERT/UPDATE,因爲任何具有相同 key 的現有行都被覆蓋。另外,value 爲空的消息將會被視作爲 DELETE 消息。

作爲 sink,upsert-kafka 連接器可以消費 changelog 流。它會將 INSERT/UPDATE_AFTER 數據作爲正常的 Kafka 消息寫入,並將 DELETE 數據以 value 爲空的 Kafka 消息寫入(表示對應 key 的消息被刪除)。Flink 將根據主鍵列的值對數據進行分區,從而保證主鍵上的消息有序,因此同一主鍵上的更新/刪除消息將落在同一分區中。

如果是更新,則同一個key會存儲多條數據,但在讀取該表數據時,只保留最後一次更新的值),並將 DELETE 數據以 value 爲空的 Kafka 消息寫入(key被打上墓碑標記,表示對應 key 的消息被刪除)。

Flink 將根據主鍵列的值對數據進行分區,從而保證主鍵上的消息有序,因此同一主鍵上的更新/刪除消息將落在同一分區中。

upsert-kafka connector相關參數

connector

必選。指定要使用的連接器,Upsert Kafka 連接器使用:'upsert-kafka'。

topic 必選。用於讀取和寫入的 Kafka topic 名稱。

properties.bootstrap.servers 必選。以逗號分隔的 Kafka brokers 列表。

key.format 必選。用於對 Kafka 消息中 key 部分序列化和反序列化的格式。key 字段由 PRIMARY KEY 語法指定。支持的格式包括 'csv'、'json'、'avro'。

value.format 必選。用於對 Kafka 消息中 value 部分序列化和反序列化的格式。支持的格式包括 'csv'、'json'、'avro'。

properties 可選。該選項可以傳遞任意的 Kafka 參數。選項的後綴名必須匹配定義在 Kafka 參數文檔中的參數名。Flink 會自動移除 選項名中的 "properties." 前綴,並將轉換後的鍵名以及值傳入 KafkaClient。例如,你可以通過 'properties.allow.auto.create.topics' = 'false' 來禁止自動創建 topic。但是,某些選項,例如'key.deserializer' 和 'value.deserializer' 是不允許通過該方式傳遞參數,因爲 Flink 會重寫這些參數的值。

value.fields-include 可選,默認爲ALL。控制key字段是否出現在 value 中。當取ALL時,表示消息的 value 部分將包含 schema 中所有的字段,包括定義爲主鍵的字段。當取EXCEPT_KEY時,表示記錄的 value 部分包含 schema 的所有字段,定義爲主鍵的字段除外。

key.fields-prefix 可選。爲了避免與value字段命名衝突,爲key字段添加一個自定義前綴。默認前綴爲空。一旦指定了key字段的前綴,必須在DDL中指明前綴的名稱,但是在構建key的序列化數據類型時,將移除該前綴。見下面的示例。在需要注意的是:使用該配置屬性,value.fields-include的值必須爲EXCEPT_KEY。

二、使用步驟

1.引入庫

        <!-- Flink kafka connector: kafka版本大於1.0.0可以直接使用通用的連接器 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_2.11</artifactId>
<version>1.12.0</version>
<scope>provided</scope>
</dependency>

2.SQL計算

示例:實時地統計網頁PV和UV的總量

-- 創建kafka數據源表(json格式)
-- 'format.type' = 'json', -- required: specify the format type
-- 'format.fail-on-missing-field' = 'true', -- optional: flag whether to fail if a field is missing or not,'false' by default
-- 'format.ignore-parse-errors' = 'true', -- optional: skip fields and rows with parse errors instead of failing;

CREATE TABLE source_ods_fact_user_ippv (
user_id STRING,
client_ip STRING,
client_info STRING,
pagecode STRING,
access_time TIMESTAMP,
dt STRING,
WATERMARK FOR access_time AS access_time - INTERVAL '5' SECOND -- 定義watermark
) WITH (
'connector' = 'kafka',
'topic' = 'user_ippv',
'scan.startup.mode' = 'earliest-offset',
'properties.group.id' = 'group1',
'properties.bootstrap.servers' = 'xxx:9092',
'format' = 'json',
'json.fail-on-missing-field' = 'false',
'json.ignore-parse-errors' = 'true'
);

-- 創建kafka upsert結果表且指定組合主鍵爲:do_date,do_min
CREATE TABLE result_total_pvuv_min (
do_date STRING, -- 統計日期
do_min STRING, -- 統計分鐘
pv BIGINT, -- 點擊量
uv BIGINT, -- 一天內同個訪客多次訪問僅計算一個UV
currenttime TIMESTAMP, -- 當前時間
PRIMARY KEY (do_date, do_min) NOT ENFORCED
) WITH (
'connector' = 'upsert-kafka',
'topic' = 'result_total_pvuv_min',
'properties.bootstrap.servers' = 'xxx:9092',
'key.json.ignore-parse-errors' = 'true',
'value.json.fail-on-missing-field' = 'false',
'key.format' = 'json',
'value.format' = 'json',
'value.fields-include' = 'ALL'
);
-- 創建視圖
CREATE VIEW view_total_pvuv_min AS
SELECT
dt AS do_date, -- 時間分區
count (client_ip) AS pv, -- 客戶端的IP
count (DISTINCT client_ip) AS uv, -- 客戶端去重
max(access_time) AS access_time -- 請求的時間
FROM
source_ods_fact_user_ippv
GROUP BY dt;


-- 將每分鐘的pv/uv統計結果寫入kafka upsert表
INSERT INTO result_total_pvuv_min
SELECT
do_date,
cast(DATE_FORMAT (access_time,'HH:mm') AS STRING) AS do_min,-- 分鐘級別的時間
pv,
uv,
CURRENT_TIMESTAMP AS currenttime
from
view_total_pvuv_min;

該處使用示例數據和驗證結果如下:

kafak 數據源:
{"user_id":"1","client_ip":"192.168.12.1","client_info":"phone","pagecode":"1001","access_time":"2021-01-23 11:32:24","dt":"2021-01-08"}
{"user_id":"1","client_ip":"192.168.12.1","client_info":"phone","pagecode":"1201","access_time":"2021-01-23 11:32:55","dt":"2021-01-08"}
{"user_id":"2","client_ip":"192.165.12.1","client_info":"pc","pagecode":"1031", "access_time":"2021-01-23 11:32:59","dt":"2021-01-08"}
{"user_id":"1","client_ip":"192.168.12.1","client_info":"phone","pagecode":"1101","access_time":"2021-01-23 11:33:24","dt":"2021-01-08"}
{"user_id":"3","client_ip":"192.168.10.3","client_info":"pc","pagecode":"1001", "access_time":"2021-01-23 11:33:30","dt":"2021-01-08"}
{"user_id":"1","client_ip":"192.168.12.1","client_info":"phone","pagecode":"1001","access_time":"2021-01-23 11:34:24","dt":"2021-01-08"}


實時統計的結果表(TOPIC:result_total_pvuv_min):
{"do_date":"2021-01-08","do_min":"11:32","pv":1,"uv":1,"currenttime":"2021-01-23 08:22:06.431"}
{"do_date":"2021-01-08","do_min":"11:32","pv":2,"uv":1,"currenttime":"2021-01-23 08:22:06.526"}
{"do_date":"2021-01-08","do_min":"11:32","pv":3,"uv":2,"currenttime":"2021-01-23 08:22:06.527"}
{"do_date":"2021-01-08","do_min":"11:33","pv":4,"uv":2,"currenttime":"2021-01-23 08:22:06.527"}
{"do_date":"2021-01-08","do_min":"11:33","pv":5,"uv":3,"currenttime":"2021-01-23 08:22:06.528"}
{"do_date":"2021-01-08","do_min":"11:34","pv":6,"uv":3,"currenttime":"2021-01-23 08:22:06.529"}


----------------分割線--------------------

重測試輸入如下示例數據:
{"user_id":"10","client_ip":"192.168.12.1","client_info":"phone","pagecode":"1001","access_time":"2021-01-22 10:10:24","dt":"2021-01-22"}
{"user_id":"11","client_ip":"192.168.12.2","client_info":"phone","pagecode":"1002","access_time":"2021-01-22 11:10:24","dt":"2021-01-22"}
{"user_id":"10","client_ip":"192.168.12.1","client_info":"phone","pagecode":"1001","access_time":"2021-01-22 10:11:24","dt":"2021-01-22"}
{"user_id":"11","client_ip":"192.168.12.3","client_info":"phone","pagecode":"1002","access_time":"2021-01-22 11:12:14","dt":"2021-01-22"}


打印待更新結果:
+----+--------------------------------+--------------------------------+----------------------+----------------------+-----------------------+
| op | do_date | do_min | pv | uv | currenttime |
+----+--------------------------------+--------------------------------+----------------------+----------------------+-----------------------+
| +I | 2021-01-22 | 10:10 | 1 | 1 | 2021-01-23T08:33:2... |
| -U | 2021-01-22 | 10:10 | 1 | 1 | 2021-01-23T08:33:2... |
| +U | 2021-01-22 | 11:10 | 2 | 2 | 2021-01-23T08:33:2... |
| -U | 2021-01-22 | 11:10 | 2 | 2 | 2021-01-23T08:33:2... |
| +U | 2021-01-22 | 11:10 | 3 | 2 | 2021-01-23T08:33:2... |
| -U | 2021-01-22 | 11:10 | 3 | 2 | 2021-01-23T08:33:3... |
| +U | 2021-01-22 | 11:12 | 4 | 3 | 2021-01-23T08:33:3... |

3. Kafka -> FLINK -> TIDB

Flink on TIDB 在當前已經有小紅書、貝殼金服等在使用,作爲一個支持upsert的實時數據同步方案具備一定的可行性。

select version(); -- 5.7.25-TiDB-v4.0.8
drop table if exists result_user_behavior;
CREATE TABLE `result_user_behavior` (
`user_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`client_ip` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL,
`client_info` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL,
`page_code` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL,
`access_time` TIMESTAMP COLLATE utf8mb4_general_ci DEFAULT NULL,
`dt`varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
// 支持upsert的一種可行數據同步方案
tenv.executeSql("CREATE TABLE source_kafka_user_behavior (\n" +
" user_id INT,\n" +
" client_ip STRING, \n" +
" client_info STRING, \n" +
" page_code STRING, \n" +
" access_time TIMESTAMP, \n" +
" dt STRING, \n" +
" WATERMARK FOR access_time AS access_time - INTERVAL '5' SECOND \n" +
") WITH (\n" +
" 'connector' = 'kafka',\n" +
" 'topic' = 'user_ippv',\n" +
" 'scan.startup.mode' = 'latest-offset',\n" +
" 'properties.group.id' = 'test-group1',\n" +
" 'properties.bootstrap.servers' = 'xx:9092', \n" +
" 'format' = 'json', \n" +
" 'json.fail-on-missing-field' = 'false',\n" +
" 'json.ignore-parse-errors' = 'true'\n" +
")").print();

tenv.executeSql("CREATE TABLE sink_upsert_tidb (\n" +
" user_id INT,\n" +
" client_ip STRING, \n" +
" client_info STRING, \n" +
" page_code STRING, \n" +
" access_time TIMESTAMP, \n" +
" dt STRING, \n" +
" PRIMARY KEY (user_id) NOT ENFORCED" +
") WITH (\n" +
" 'connector' = 'jdbc',\n" +
" 'url' = 'jdbc:mysql://xxx:4000/bi',\n" +
" 'username' = 'bi_rw',\n" +
" 'password' = 'xxx',\n" +
" 'table-name' = 'result_user_behavior'\n" +
")");


tenv.executeSql("insert into sink_upsert_tidb" +
" select " +
" user_id ,\n" +
" client_ip , \n" +
" client_info , \n" +
" page_code , \n" +
" access_time , \n" +
" dt \n" +
"from source_kafka_user_behavior").print();

測試輸入:

測試數據:
{"user_id":"11","client_ip":"192.168.12.3","client_info":"phone","page_code":"1002","access_time":"2021-01-25 11:12:14","dt":"2021-01-25"}
{"user_id":"11","client_ip":"192.168.12.3","client_info":"phone","page_code":"1003","access_time":"2021-01-25 11:12:14","dt":"2021-01-25"}
{"user_id":"11"} -- 值全部置空
{"user_id":"11","client_ip":"192.168.12.4","client_info":"phone","page_code":"10","access_time":"2021-01-25 11:35:14","dt":"2021-01-25"}
{"user_id":"12","client_ip":"192.168.12.5","client_info":"phone","page_code":"10","access_time":"2021-01-25 11:35:14","dt":"2021-01-25"}

Tidb查詢結果示例:

總結

這裏演示了使用kaka作爲source和sink的使用示例,其中我們把從kafka source中消費的數據進行視圖查詢的時候則顯示以上更新結果,每一條以統計日期和統計分鐘作爲聯合主鍵的數據插入都會被解析爲+I(插入)-U(標記待刪除值) +U (更新新值),這樣在最新的result_total_pvuv_min 的kafka upsert 結果表中就是最新的數據。

當前kafka-upsert connector 適用於Flink-1.12的版本,作爲一個數據聚合的中轉對於很多業務場景有一定的普適性,比如kafka upsert結果表還可以作爲維表join, 或者通過flink sink 到HDFS, iceberg table等進行離線分析。

如果想真正實時,Flink+Tidb就是一個很好的解決方案。雖然Tidb存儲和計算不分離,但是能使用加機器解決的問題,性能都不是事,況且Tidb完全兼容MySQL語法,非常適合MySQL平遷,而且支持事務,和使用MySQL沒有什麼特別大的區別,

官方已出TiSpark查詢引擎,雖還未實測性能,但想必會比MySQL 引擎查詢的效率要高。我司也開始着手Tidb的使用,目前的實時的任務是基於微批的形式處理,還不能算是完全的實時,後面隨着對其的瞭解原來越完善,完全實時化則指日可待。



FileSystem/JDBC/Kafka - Flink三大Connector實現原理及案例

企業數據治理及在美團的最佳實踐


歡迎點贊+收藏+轉發朋友圈素質三連

文章不錯?點個【在看】吧! 

本文分享自微信公衆號 - 大數據技術與架構(import_bigdata)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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