從何而來
關係型API有很多好處:是宣告式的,使用者只需要告訴需要什麼,系統決定如何計算;使用者不必特地實現;更方便優化,可以執行得更高效。本身Flink就是一個統一批和流的分散式計算平臺,所以社群設計關係型API的目的之一是可以讓關係型API作為統一的一層,兩種查詢擁有同樣的語義和語法。大多數流處理框架的API都是比較low-level的API,學習成本高而且很多邏輯需要寫到UDF中,所以Apache Flink 新增了SQL-like的API處理關係型資料--Table API。這套API中最重要的概念是Table
(可以在上面進行關係型操作的結構化的DataSet或DataStream)。Table
API 與 DataSet
和DataStream
API 結合緊密,DataSet 和 DataStream都可以很容易地轉換成 Table,同樣轉換回來也很方便:
val execEnv = ExecutionEnvironment.getExecutionEnvironment
val tableEnv = TableEnvironment.getTableEnvironment(execEnv)
// obtain a DataSet from somewhere
val tempData: DataSet[(String, Long, Double)] =
// convert the DataSet to a Table
val tempTable: Table = tempData.toTable(tableEnv, 'location, 'time, 'tempF)
// compute your result
val avgTempCTable: Table = tempTable
.where('location.like("room%"))
.select(
('time / (3600 * 24)) as 'day,
'Location as 'room,
(('tempF - 32) * 0.556) as 'tempC
)
.groupBy('day, 'room)
.select('day, 'room, 'tempC.avg as 'avgTempC)
// convert result Table back into a DataSet and print it
avgTempCTable.toDataSet[Row].print()複製程式碼
example使用的是Scala的API,Java版API也有同樣的功能。
下圖展示了 Table API 的架構:
從 DataSet 或 DataStream 建立一個 Table,然後在上面進行關係型操作比如 fliter
、join
、select
。對Table的操作將會轉換成邏輯運算子樹。Table 轉換回 DataSet 和 DataStream 的時候將會轉換成DataSet 和 DataStream的運算元。有些類似 'location.like("room%")
的表示式將會通過 code generation
編譯成Flink的函式。
然而,最初傳統的Table API 有一定的限制。首先,它不能獨立使用。Table API 的 query 必須嵌入到 DataSet 或 DataStream的程式中。對批處理表的查詢不支援outer join
,sorting
和很多SQL中常見的標量函式。對於流處理的查詢只支援filtetr
union
和 projection
,不支援aggregation
和join
。而且,轉換過程中沒有利用太多查詢優化技術,除了適用於所有DataSet程式的優化。
Table API 和 SQL 緊密結合
隨著流處理的日益普及和Flink在該領域的增長,Flink社群認為需要一個更簡單的API使更多的使用者能夠分析流資料。一年前Flink社群決定將Table API提升到一個新的層級,擴充套件Table API中流處理的能力以及支援SQL。社群不想重複造輪子,於是決定在 Apache Calcite (一個比較流行的SQL解析和優化框架)的基礎上構建新的 Table API。Apache Calcite 被用在很多專案中,包括 Apache Hive,Apache Drill,Cascading等等。除此之外,Calcite社群將 SQL on Stream 寫入它的roadmap,所以Flink的SQL很適合和它結合。
以Calcite為核心的新架構圖:
新架構提供兩種API進行關係型查詢,Table API 和 SQL。這兩種API的查詢都會用包含註冊過的Table的catalog進行驗證,然後轉換成統一Calcite的logical plan。在這種表示中,stream和batch的查詢看起來完全一樣。下一步,利用 Calcite的 cost-based 優化器優化轉換規則和logical plan。根據資料來源的性質(流式和靜態)使用不同的規則進行優化。最終優化後的plan轉傳成常規的Flink DataSet 或 DataStream 程式。這步還涉及code generation(將關係表示式轉換成Flink函式)。
下面我們舉一個例子來理解新的架構。表示式轉換成Logical Plan如下圖所示:
呼叫Table API 實際上是建立了很多 Table API 的 LogicalNode
,建立的過程中對會對整個query進行validate。比如table是CalalogNode
,window groupBy之後在select時會建立WindowAggregate
和Project
,where對應Filter
。然後用RelBuilder
翻譯成Calcite LogicalPlan。如果是SQL API 將直接用Calcite的Parser進行解釋然後validate生成Calcite LogicalPlan。
利用Calcite內建的一些rule來優化LogicalPlan,也可以自己新增或者覆蓋這些rule。轉換成Optimized Calcite Plan後,仍然是Calcite的內部表示方式,現在需要transform成DataStream Plan,對應上圖第三列的類,裡面封裝瞭如何translate成普通的DataStream或DataSet程式。隨後呼叫相應的tanslateToPlan
方法轉換和利用CodeGen超程式設計成Flink的各種運算元。現在就相當於我們直接利用Flink的DataSet或DataStream API開發的程式。
Table API的新架構除了維持最初的原理還改進了很多。為流式資料和靜態資料的關係查詢保留統一的介面,而且利用了Calcite的查詢優化框架和SQL parser。該設計是基於Flink已構建好的API構建的,DataStream API 提供低延時高吞吐的流處理能力而且就有exactly-once語義而且可以基於event-time進行處理。而且DataSet擁有穩定高效的記憶體運算元和流水線式的資料交換。Flink的core API和引擎的所有改進都會自動應用到Table API和SQL上。
新的SQL介面整合到了Table API中。DataSteam, DataSet和外部資料來源可以在TableEnvironment中註冊成表,為了是他們可以通過SQL進行查詢。TableEnvironment.sql()
方法用來宣告SQL和將結果作為Table返回。下面的是一個完整的樣例,從一個JSON編碼的Kafka topic中讀取流表,然後用SQL處理並寫到另一個Kafka topic。
// get environments
val execEnv = StreamExecutionEnvironment.getExecutionEnvironment
val tableEnv = TableEnvironment.getTableEnvironment(execEnv)
// configure Kafka connection
val kafkaProps = ...
// define a JSON encoded Kafka topic as external table
val sensorSource = new KafkaJsonSource[(String, Long, Double)](
"sensorTopic",
kafkaProps,
("location", "time", "tempF"))
// register external table
tableEnv.registerTableSource("sensorData", sensorSource)
// define query in external table
val roomSensors: Table = tableEnv.sql(
"SELECT STREAM time, location AS room, (tempF - 32) * 0.556 AS tempC " +
"FROM sensorData " +
"WHERE location LIKE 'room%'"
)
// define a JSON encoded Kafka topic as external sink
val roomSensorSink = new KafkaJsonSink(...)
// define sink for room sensor data and execute query
roomSensors.toSink(roomSensorSink)
execEnv.execute()複製程式碼
這個樣例中忽略了流處理中最有趣的部分:window aggregate 和 join。這些操作如何用SQL表達呢?Apache Calcite社群提出了一個proposal來討論SQL on streams的語法和語義。社群將Calcite的stream SQL描述為標準SQL的擴充套件而不是另外的 SQL-like語言。這有很多好處,首先,熟悉SQL標準的人能夠在不學習新語法的情況下分析流資料。靜態表和流表的查詢幾乎相同,可以輕鬆地移植。此外,可以同時在靜態表和流表上進行查詢,這和flink的願景是一樣的,將批處理看做特殊的流處理(批看作是有限的流)。最後,使用標準SQL進行流處理意味著有很多成熟的工具支援。
下面的example展示瞭如何用SQL和Table API進行滑動視窗查詢:
SQL
SELECT STREAM
TUMBLE_END(time, INTERVAL '1' DAY) AS day,
location AS room,
AVG((tempF - 32) * 0.556) AS avgTempC
FROM sensorData
WHERE location LIKE 'room%'
GROUP BY TUMBLE(time, INTERVAL '1' DAY), location複製程式碼
Table API
val avgRoomTemp: Table = tableEnv.ingest("sensorData")
.where('location.like("room%"))
.partitionBy('location)
.window(Tumbling every Days(1) on 'time as 'w)
.select('w.end, 'location, , (('tempF - 32) * 0.556).avg as 'avgTempCs)複製程式碼
Table API的現狀
Batch SQL & Table API 支援:
- Selection, Projection, Sort, Inner & Outer Joins, Set operations
- Windows for Slide, Tumble, Session
Streaming Table API 支援:
- Selection, Projection, Union
- Windows for Slide, Tumble, Session
Streaming SQL:
- Selection, Projection, Union, Tumble
Streaming SQL案例
持續的ETL和資料匯入
獲取流式資料,然後轉換這些資料(歸一化,聚合...),將其寫入其他系統(File,Kafka,DBMS)。這些query的結果通常會儲存到log-style的系統。
實時的Dashboards 和 報表
獲取流式資料,然後對資料進行聚合來支援線上系統(dashboard,推薦)或者資料分析系統(Tableau)。通常結果被寫到k-v儲存中(Cassandra,Hbase,可查詢的Flink狀態),建立索引(Elasticsearch)或者DBMS(MySQL,PostgreSQL...)。這些查詢通常可以被更新,改進。
即席分析
針對流資料的即席查詢,以實時的方式進行分析和瀏覽資料。查詢結果直接顯示在notebook(Apache Zeppelin)中。
Flink社群還提出來和資料庫中Materialized View
很相似的Dynamic table 動態表
概念,將在以後的版本中支援,具體細節將另開文章解釋。