深度解讀:Flink 1.11 SQL流批一體的增強與完善

7月6日,Apache Flink 1.11 正式發佈。從3月初進行功能規劃到7月初正式發版,1.11 用將近4個月的時間重點優化了 Flink 的易用性問題,提升用戶的生產使用體驗。

SQL 作爲 Flink 中公認的核心模塊之一,對推動 Flink 流批一體功能的完善至關重要。在 1.11 中,Flink SQL 也進行了大量的增強與完善,開發大功能 10 餘項,不僅擴大了應用場景,還簡化了流程,上手操作更簡單。

其中,值得注意的改動包括:

  • 默認 Planner 已經切到 Blink planner 上。

  • 引入了對 CDC(Change Data Capture,變動數據捕獲)的支持,用戶僅用幾句簡單的 SQL 即可對接 Debezium 和 Canal 的數據源。

  • 離線數倉實時化,用戶可方便地使用 SQL 將流式數據從 Kafka 寫入 Hive 等。

Flink SQL 演變

隨着流計算的發展,挑戰不再僅限於數據量和計算量,業務變得越來越複雜,開發者可能是資深的大數據從業者、初學 Java 的愛好者,或是不懂代碼的數據分析者。如何提高開發者的效率,降低流計算的門檻,對推廣實時計算非常重要。

SQL 是數據處理中使用最廣泛的語言,它允許用戶簡明扼要地展示其業務邏輯。Flink 作爲流批一體的計算引擎,致力於提供一套 SQL 支持全部應用場景,Flink SQL 的實現也完全遵循 ANSI SQL 標準。之前,用戶可能需要編寫上百行業務代碼,使用 SQL 後,可能只需要幾行 SQL 就可以輕鬆搞定。

Flink SQL 的發展大概經歷了以下階段:

  • Flink 1.1.0:第一次引入 SQL 模塊,並且提供 TableAPI,當然,這時候的功能還非常有限。

  • Flink 1.3.0:在 Streaming SQL 上支持了 Retractions,顯著提高了 Streaming SQL 的易用性,使得 Flink SQL 支持了複雜的 Unbounded 聚合連接。

  • Flink 1.5.0:SQL Client 的引入,標誌着 Flink SQL 開始提供純 SQL 文本。

  • Flink 1.9.0:抽象了 Table 的 Planner 接口,引入了單獨的 Blink Table 模塊。Blink Table 模塊是阿里巴巴內部的 SQL 層版本,不僅在結構上有重大變更,在功能特性上也更加強大和完善。

  • Flink 1.10.0:作爲第一個 Blink 基本完成 merge 的版本,修復了大量遺留的問題,並給 DDL 帶來了 Watermark 的語法,也給 Batch SQL 帶來了完整的 TPC-DS 支持和高效的性能。

經過了多個版本的迭代支持,SQL 模塊在 Flink 中變得越來越重要,Flink 的 SQL 用戶也逐漸擴大。基於 SQL 模塊的 Python 接口和機器學習接口也在快速發展。毫無疑問, SQL 模塊作爲最常用的 API 之一和生態的集成變得越來越重要。

SQL 1.11 重要變更

Flink SQL 在原有的基礎上擴展了新場景的支持:

  • Flink SQL 引入了對 CDC(Change Data Capture,變動數據捕獲)的支持,它使 Flink 可以方便地通過像 Debezium 這類工具來翻譯和消費數據庫的變動日誌。

  • Flink SQL 擴展了類 Filesystem connector 對實時化用戶場景和格式的支持,從而可以支持將流式數據從 Kafka 寫入 Hive 等場景。

除此之外,Flink SQL 也從多個方面提高 SQL 的易用性,系統性的解決了之前的bug、完善了用戶 API。

CDC 支持

CDC 格式是數據庫中一種常用的模式,業務上典型的應用是通過工具(比如 Debezium 或 Canal)將 CDC 數據通過特定的格式從數據庫中導出到 Kafka 中。在以前,業務上需要定義特殊的邏輯來解析 CDC 數據,並把它轉換成一般的 Insert-only 數據,後續的處理邏輯需要考慮到這種特殊性,這種 work-around 的方式無疑給業務上帶來了不必要的複雜性。

如果 Flink SQL 引擎能原生支持 CDC 數據的輸入,將 CDC 對接到 Flink SQL 的 Changelog Stream 概念上,將會大大降低用戶業務的複雜度。

流計算的本質是就是不斷更新、不斷改變結果的計算。考慮一個簡單的聚合 SQL,流計算中,每次計算產生的聚合值其實都是一個局部值,所以會產生 Changelog Stream。在以前想要把聚合的數據輸出到 Kafka 中,如上圖所示,幾乎是不可能的,因爲 Kafka 只能接收 Insert-only 的數據。

Flink 之前主要是因爲 Source&Sink 接口的限制,導致不能支持 CDC 數據的輸入。Flink SQL 1.11 經過了大量的接口重構,在新的 Source&Sink 接口上,支持了 CDC 數據的輸入和輸出,並且支持了 Debezium 與 Canal 格式(FLIP-105)。這一改動使動態 Table Source 不再只支持 append-only 的操作,而且可以導入外部的修改日誌(插入事件)將它們翻譯爲對應的修改操作(插入、修改和刪除)並將這些操作與操作的類型發送到後續的流中。

如上圖所示,理論上,CDC 同步到 Kafka 的數據就是 Append 的一個流,只是在格式中含有 Changelog 的標識:

  • 一種方式是把 Changlog 標識看做一個普通字段,這也是目前普遍的使用方式。
  • 在 Flink 1.11 後,可以將它聲明成 Changelog 的格式,Flink 內部機制支持 Interpret Changelog,可以原生識別出這個特殊的流,將其轉換爲 Flink 的 Changlog Stream,並按照 SQL 的語義處理;同理,Flink SQL 也具有輸出 Change Stream 的能力 (Flink 1.11 暫無內置實現),這就意味着,你可以將任意類型的 SQL 寫入到 Kafka 中,只要有 Changelog 支持的 Format。

爲了消費 CDC 數據,用戶需要在使用 SQL DDL 創建表時指指定“format=debezium-json”或者“format=canal-json”:

CREATE TABLE my_table (
    ... 
) WITH ( 
    'connector'='...', -- e.g. 'kafka'
    'format'='debezium-json'
);

Flink 1.11 的接口都已 Ready,但是在實現上:

  • 只支持 Kafka 的 Debezium-json 和 Canal-json 讀。
  • 歡迎大家擴展實現自己的 Format 和 Connector。

Source & Sink 重構

Source & Sink 重構的一個重要目的是支持上節所說的 Changelog,但是除了 Changelog 以外,它也解決了諸多之前的遺留問題。

新 Source & Sink 使用標準姿勢 (詳見官方文檔):

CREATE TABLE kafka_table (
    ... 
) WITH (
    'connector' = 'kafka-0.10', 
    'topic' = 'test-topic', 
    'scan.startup.mode' = 'earliest-offset', 
    'properties.bootstrap.servers' = 'localhost:9092', 
    'format' = 'json', 
    'json.fail-on-missing-field' = 'false'
);

Flink 1.11 爲了向前兼容性,依然保留了老 Source & Sink,使用 “connector.type” 的 Key,即可 Fallback 到老 Source & Sink 上。

Factory 發現機制

Flink 1.11 前,用戶可能經常遇到一個異常,叫做 NoMatchingFactory 異常:

指的是,定義了一個 DDL,在用的時候,DDL 屬性找不到對應的 TableFactory 實現,可能的原因是:

  • Classpath 下沒有實現類,Flink SQL 是通過 Java SPI 的機制來發現 Factory;
  • 參數寫錯了。

但是報的異常讓人非常疑惑,根據異常的提示消息,很難找到到底哪裏的代碼錯了,更難明確知道哪個 Key 寫錯了。

public interface Factory {
    String factoryIdentifier();
    ……
}

所以在 Flink 1.11 中,社區重構了 TableFactory 接口,提出了一個新的 Factory 接口,它有一個方法,叫做 FactoryIdentifier。以後所有的 Factory 的 look up 都通過 identifier。這樣的話就非常清晰明瞭,找不到是因爲 Classpath 下沒 Factory 的類,找得到那就可以定位到 Factory 的實現中,進行確定性的校驗。

類型與數據結構

之前的 Source&Sink 接口支持用戶自定義數據結構,即框架知道如何把自定義的數據結構轉換爲 Flink SQL 認識的內部數據結構,如:

public interface TableSource<T> {
    TypeInformation<T> getReturnType();
    ...
}

用戶可以自定義泛型 T,通過 getReturnType 來告訴框架怎麼轉換。

不過問題來了,當 getReturnType 和 DDL 中聲明的類型不一致時怎麼辦?特別是兩套類型系統的情況下,如:Runtime 的 TypeInformation,SQL 層的 DataType。由於精度等問題,可能導致經常出現類型不匹配的異常。

Flink 1.11 系統性地解決了這個問題。現在 Connector 開發者不能自定義數據結構,只能使用 Flink SQL 內部的數據結構:RowData。所以保證了默認 type 與 DDL 的對應,不用再返回類型讓框架去確認了。

RowData 數據結構在 SQL 內部設計出來爲了:

  • 抽象類接口,在不同場景有適合的高性能實現。
  • 包含 RowKind,契合流計算中的 CDC 數據格式。
  • 遵循 SQL 規範,比如包含精度信息。
  • 對應 SQL 類型的可枚舉的數據結構。

Upsert 與 Primary Key

流計算的一個典型場景是把聚合的數據寫入到 Upsert Sink中,比如 JDBC、HBase,當遇到複雜的 SQL 時,時常會出現:

UpsertStreamTableSink 需要上游的 Query 有完整的 Primary Key 信息,不然就直接拋異常。這個現象涉及到 Flink 的 UpsertStreamTableSink 機制。顧名思義,它是一個更新的 Sink,需要按 key 來更新,所以必須要有 key 信息。

如何發現 Primary Key?一個方法是讓優化器從 Query 中推斷,如下圖發現 Primary Key 的例子。

這種情況下在簡單 Query 當中很好,也滿足語義,也非常自然。但是如果是一個複雜的 Query,比如聚合又 Join 再聚合,那就只有報錯了。不能期待優化器有多智能,很多情況它都不能推斷出 PK,而且,可能業務的 SQL 本身就不能推斷出 PK,所以導致了這樣的異常。

怎麼解決問題?Flink 1.11 徹底的拋棄了這個機制,不再從 Query 來推斷 PK 了,而是完全依賴Create table 語法。比如 create 一個 jdbc_table,需要在定義中顯式地寫好 Primary Key(後面 not enforced 的意思是不強校驗,因爲 connector 也許沒有具備 PK 的強校驗的能力)。當指定了 PK,就相當於就告訴框架這個 jdbc Sink 會按照對應的 key 來進行更新。如此,就跟 query 完全沒有關係了,這樣的設計可以定義得非常清晰,如何更新完全按照設置的定義來。

CREATE TABLE jdbc_table (
    id BIGINT,
    ... 
    PRIMARY KEY (id) NOT ENFORCED
)

Hive 流批一體

首先看傳統的 Hive 數倉。一個典型的 Hive 數倉如下圖所示。一般來說,ETL 使用調度工具來調度作業,比如作業每天調度一次或者每小時調度一次。這裏的調度,其實也是一個疊加的延遲。調度產生 table1,再產生 table2,再調度產生 table3,計算延時需要疊加起來。

問題是慢,延遲大,並且 Ad-hoc 分析延遲也比較大,因爲前面的數據入庫,或者前面的調度的 ETL 會有很大的延遲。Ad-hoc 分析再快返回,看到的也是歷史數據。

所以現在流行構建實時數倉,從 Kafka 讀計算寫入 Kafka,最後再輸出到 BI DB,BI DB 提供實時的數據服務,可以實時查詢。Kafka 的 ETL 爲實時作業,它的延時甚至可能達到毫秒級。實時數倉依賴 Queue,它的所有數據存儲都是基於 Queue 或者實時數據庫,這樣實時性很好,延時低。但是:

  • 第一,基於 Queue,一般來說就是行存加 Queue,存儲效率其實不高。
  • 第二,基於預計算,最終會落到 BI DB,已經是聚合好的數據了,沒有歷史數據。而且 Kafka 存的一般來說都是15天以內的數據,沒有歷史數據,意味着無法進行 Ad-hoc 分析。所有的分析全是預定義好的,必須要起對應的實時作業,且寫到 DB 中,這樣纔可用。對比來說,Hive 數倉的好處在於它可以進行 Ad-hoc 分析,想要什麼結果,就可以隨時得到什麼結果。

能否結合離線數倉和實時數倉兩者的優勢,然後構建一個 Lambda 的架構?

核心問題在於成本過高。無論是維護成本、計算成本還是存儲成本等都很高。並且兩邊的數據還要保持一致性,離線數倉寫完 Hive 數倉、SQL,然後實時數倉也要寫完相應 SQL,將造成大量的重複開發。還可能存在團隊上分爲離線團隊和實時團隊,兩個團隊之間的溝通、遷移、對數據等將帶來大量人力成本。如今,實時分析會越來越多,不斷的發生遷移,導致重複開發的成本也越來越高。少部分重要的作業尚可接受,如果是大量的作業,維護成本其實是非常大的。

如何既享受 Ad-hoc 的好處,又能實現實時化的優勢?一種思路是將 Hive 的離線數倉進行實時化,就算不能毫秒級的實時,準實時也好。所以,Flink 1.11 在 Hive 流批一體上做了一些探索和嘗試,如下圖所示。它能實時地按 streaming 的方式來導出數據,寫到 BI DB 中,並且這套系統也可以用分析計算框架來進行 Ad-hoc 的分析。這個圖當中,最重要的就是 Flink Streaming 的導入。

Streaming Sink

早期 Flink 版本在 DataStreaming 層,已經有一個強大的 StreamingFileSink 將流數據寫到文件系統。它是一個準實時的、Exactly-once 的系統,能實現一條數據不多,一條數據不少的 Sink。

具體原理是基於兩階段提交:

  • 第一階段:SnapshotPerTask,關閉需要 Commit 的文件,或者記錄正在寫的文件的 Offset。
  • 第二階段:NotifyCheckpointComplete,Rename 需要 Commit 的文件。注意,Rename 是一個原子且冪等的操作,所以只要保證 Rename 的 At-least-once,即可保證數據的 Exactly-once。

這樣一個 File system 的 Writer 看似比較完美了。但是在 Hive 數倉中,數據的可見性是依賴 Hive Metastore 的,那在這個流程中,誰來通知 Hive Metastore 呢?

SQL 層在 StreamingFileSink,擴展了 Partition 的 Committer。

相當於不僅要進行 File 的 Commit,還要進行 Partition 的 Commit。如圖所示,FileWriter 對應之前的 StreamingFileSink,它提供的是 Exactly-once 的 FileWriter。而後面再接了一個節點 PartitionCommitter。支持的 Commit Policy 有:

  • 內置支持 Add partition 到 Hive metastore;
  • 支持寫 SuccessFile 到文件系統當中;
  • 並且也可以自定義Committer,比如可以 analysis partition、合併 partition 裏面的小文件。

Committer 掛在 Writer 後, 由 Commit Trigger 決定什麼時機來 commit :

  • 默認的 commit 時機是,有文件就立即 commit。因爲所有 commit 都是可重入的,所以這一點是可允許的。
  • 另外,也支持通過 partition 時間和 Watermark 來共同決定的。比如小時分區,如果現在時間到11點,10點的分區就可以 commit 了。Watermark 保證了作業當前的準確性。

Streaming Source

Hive 數倉中存在大量的 ETL 任務,這些任務往往是通過調度工具來週期性的運行,這樣做主要有兩個問題:

  • 實時性不強,往往調度最小也是小時級。
  • 流程複雜,組件多,容易出現問題。

針對這些離線的 ETL 作業,Flink 1.11 爲此開發了實時化的 Hive 流讀,支持:

  • Partition 表,監控 Partition 的生成,增量讀取新的 Partition。
  • 非 Partition 表,監控文件夾內新文件的生成,增量讀取新的文件。

甚至可以使用10分鐘級別的分區策略,使用 Flink 的 Hive streaming source 和 Hive streaming sink ,可以大大提高 Hive 數倉的實時性到準實時分鐘級,在實時化的同時,也支持針對 Table 全量的 Ad-hoc 查詢,提高靈活性。

SELECT * FROM hive_table /*+ OPTIONS('streaming-source.enable'=’true’, 'streaming-source.consume-start-offset'='2020-05-20') */;

另外除了 Scan 的讀取方式,Flink 1.11 也支持了 Temporal Join 的方式,也就是以前常說的 Streaming Dim Join。

SELECT
  o.amout, o.currency, r.rate, o.amount * r.rate
FROM
  Orders AS o
  JOIN LatestRates FOR SYSTEM_TIME AS OF o.proctime AS r
  ON r.currency = o.currency

目前支持的方式是 Cache All,並且是不感知分區的,比較適合小表的情況。

Hive Dialect

Flink SQL 遵循的是 ANSI-SQL 的標準,而 Hive SQL 有它自己的 HQL 語法,它們之間的語法、語義都有些許不同。

如何讓 Hive 用戶遷移到 Flink 生態中,同時避免用戶太大的學習成本?爲此, Flink SQL 1.11 提供了 Hive Dialect,可以使得用戶在 Flink 生態中使用 HQL 語言來計算。目前只支持 DDL,後續版本會逐步攻堅 Qeuries。

Filesystem Connector

Hive Integration 提供了一個重量級的集成,功能豐富,但是環境比較複雜。如果只是想要一個輕量級的 Filesystem 讀寫呢?

Flink table 在長久以來只支持一個 CSV 的 filesystem table,並且還不支持 Partition,行爲上在某些方面也有些不符合大數據計算的直覺。

Flink 1.11重構了整個 Filesystem connector 的實現:

  • 結合 Partition,現在,Filesystem connector 支持 SQL 中 Partition 的所有語義,支持 Partition 的 DDL,支持 Partition Pruning,支持靜態/動態 Partition 的插入,支持 overwrite 的插入。
  • 支持各種 Formats:
    • CSV
    • JSON
    • Aparch AVRO
    • Apache Parquet
    • Apache ORC
  • 支持 Batch 的讀寫。
  • 支持 Streaming sink,也支持 Partition commit,支持寫 Success 文件。

用幾句簡單的 SQL,不用搭建 Hive 集成環境即可:

  • 啓動一個流作業寫入 Filesystem 中,然後在 Hive 端即可查詢到 Filesystem 上的數據,相比之前 Datastream 的作業,簡單 SQL 即可搞定離線數據的入庫。
  • 通過 Filesystem Connector 來查詢 Hive 數倉中的數據,功能沒有 Hive 集成那麼全,但是定義簡單。

Table 易用性

DDL Hints 和 Like

在 Flink 1.10 以後,Hive MetaStore 逐漸成爲 Flink streaming SQL 中 table 相關的 Meta 信息的存儲。比如,可以通過 Hive Catalog 保存 Kafka tables。這樣可以在啓動的時候直接使用 tables。

通過 DDL 這種方式,把 SQL 提交到 cluster,就可以寫入 Kafka,或者寫入 MySQL、 DFS。使用 Hive Catalog 後,是不是說只用寫一次DDL,之後的流計算作業都是直接使用 kafka 的 table 呢?

不完全是,因爲還是有一些缺陷。比如,一個典型的 Kafka table 有一些 execution 相關的參數。因爲 kafka 一般來說都是存15天以內的數據,需要指定每次消費的時間偏移,時間偏移是在不斷變化的。每次提交作業,使用 kafka table 的參數是不一樣的。而這些參數又存儲在 Catalog 裏面,這種情況下只能創建另外一張表,所以字段和參數要重寫一遍,非常繁瑣。

  • Flink 1.11,社區就開發了 Table Hints,它在1.11中目前只專注一個功能,即 Dynamic Table Options。用起來很簡單,在 SQL 中 select from 時,在 table 後面寫 table hints 的方式來指定其動態 options ,在不同的使用場景,指定不同的動態參數。
  • Flink 1.11,引入了 Like 語法。LIKE 是標準的 SQL 定義。相當於 Clone一張表出來複用它的 schema。LIKE 支持多種 constraints。可以選擇繼承,也可以選擇完全覆蓋。

Table Hints:

SELECT id, name FROM kafka_table1 /*+ OPTIONS('scan.startup.mode'='earliest-offset') */;

LIKE:

CREATE TABLE kafka_table2 WITH ( 'scan.startup.mode'='earliest-offset') LIKE kafka_table1;

這兩個手段在對接 Hive Catalog 的基礎上,是非常好的補充,能夠儘可能的避免在每次作業
的時候都寫一大堆 Schema。

內置 Connectors

Flink SQL 1.11 引入了新的三個內置 Connectors,主要是爲了大家更方便的進行調試、測試,以及進行壓測和線上的觀察:

  • Datagen Source:一個無中生有產生數據的 Source,可以定義生成的策略,比如 Sequence,比如 Random 的生成。方便線下進行功能性的測試,也可以拿來性能測試。
  • Print Sink:直接在 Task 節點 Runtime 的打印出數據,比如線上作業某個 Sink 少數據了,不知道是上游發來數據有問題,還是 Sink 邏輯有問題,這時可以額外接一個 Print Sink,排查上游數據到底有沒有問題。
  • Blackhole Sink:默默把數據給喫掉,方便功能性的調試。

這三個 Connectors 的目的是爲了在調試、測試中排除 Connectors 的影響,一般來說,Connectors 在流計算中是不可控的存在,很多問題把 Connectors 糅雜在一起,變得比較複雜難以排查。

SQL-API

TableEnvironment

TableEnvironment 作爲 SQL 層的編程入口,無疑是非常重要的,之前的 API 主要是:

  • Table sqlQuery:從一段 Select 的 query 中返回 Table 接口,把用戶的 SQL 翻譯成 Flink 的 Table。
  • void sqlUpdate:本質上是執行一段 DDL/DML。但是行爲上,當是 DDL 時,直接執行;當是 DML 時,默默 Cache 到 TableEnvironment,等到後續的 execute 調用,纔會真正的執行。
  • execute:真正的執行,提交作業到集羣。

TableEnvironment 默默的 Cache 執行計劃,而且多個 API 感覺上會很混亂,所以,1.11 社區重構了編程接口,目的是想要提供一個乾淨、並且不易出 bug 的清晰接口。

  • 單 SQL 執行:TableResult executeSql(String sql)
  • 多 SQL 執行:StatementSet createStatementSet()
  • TableResult:支持 collect、print、getJobClient

現在 executeSql 就是一個大一統的接口,不管是什麼 SQL,是 Query 還是 DDL 還是 DML,直接丟給它都可以很方便地使用起來。

並且,和 Datastream 也有了很清晰的界限:

  • 調用過 toDataStream:一定要使用 StreamExecutionEnvironment.execute
  • 沒調用過 toDataStream:一定要使用 TableEnvironment.executeSql

SQL-Client

SQL-Client 在 1.11 對齊了很多 Flink 內部本來就支持的 DDL,除此之外值得注意的是,社區還開發了 Tableau 的結果展示模式,展示更自然一些,直接在命令行展示結果,而不是切換頁面:

總結和展望

上述解讀主要側重在用戶接口方面,社區已經有比較豐富的文檔,大家可以去官網查看這些功能的詳細文檔,以便更深入的瞭解和使用。

Flink SQL 1.11 在 CDC 方面開了個頭,內部機制和 API 上打下了夯實的基礎,未來會內置更多的 CDC 支持,直接對接數據庫 Binlog,支持更多的 Flink SQL 語法。後續版本也會從底層提供更多的流批一體支持,給 SQL 層帶來更多的流批一體的可能性。

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