淺析 Flink Table/SQL API

mtunique發表於2019-03-04

從何而來

關係型API有很多好處:是宣告式的,使用者只需要告訴需要什麼,系統決定如何計算;使用者不必特地實現;更方便優化,可以執行得更高效。本身Flink就是一個統一批和流的分散式計算平臺,所以社群設計關係型API的目的之一是可以讓關係型API作為統一的一層,兩種查詢擁有同樣的語義和語法。大多數流處理框架的API都是比較low-level的API,學習成本高而且很多邏輯需要寫到UDF中,所以Apache Flink 新增了SQL-like的API處理關係型資料--Table API。這套API中最重要的概念是Table(可以在上面進行關係型操作的結構化的DataSet或DataStream)。Table API 與 DataSetDataStream 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 的架構:

淺析 Flink Table/SQL API

從 DataSet 或 DataStream 建立一個 Table,然後在上面進行關係型操作比如 fliterjoinselect。對Table的操作將會轉換成邏輯運算子樹。Table 轉換回 DataSet 和 DataStream 的時候將會轉換成DataSet 和 DataStream的運算元。有些類似 'location.like("room%") 的表示式將會通過 code generation 編譯成Flink的函式。

然而,最初傳統的Table API 有一定的限制。首先,它不能獨立使用。Table API 的 query 必須嵌入到 DataSet 或 DataStream的程式中。對批處理表的查詢不支援outer joinsorting和很多SQL中常見的標量函式。對於流處理的查詢只支援filtetr unionprojection,不支援aggregationjoin。而且,轉換過程中沒有利用太多查詢優化技術,除了適用於所有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為核心的新架構圖:

淺析 Flink Table/SQL API

新架構提供兩種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如下圖所示:

淺析 Flink Table/SQL API

呼叫Table API 實際上是建立了很多 Table API 的 LogicalNode,建立的過程中對會對整個query進行validate。比如table是CalalogNode,window groupBy之後在select時會建立WindowAggregateProject,where對應Filter。然後用RelBuilder翻譯成Calcite LogicalPlan。如果是SQL API 將直接用Calcite的Parser進行解釋然後validate生成Calcite LogicalPlan。

淺析 Flink Table/SQL API

利用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和資料匯入

淺析 Flink Table/SQL API

獲取流式資料,然後轉換這些資料(歸一化,聚合...),將其寫入其他系統(File,Kafka,DBMS)。這些query的結果通常會儲存到log-style的系統。

實時的Dashboards 和 報表

淺析 Flink Table/SQL API

獲取流式資料,然後對資料進行聚合來支援線上系統(dashboard,推薦)或者資料分析系統(Tableau)。通常結果被寫到k-v儲存中(Cassandra,Hbase,可查詢的Flink狀態),建立索引(Elasticsearch)或者DBMS(MySQL,PostgreSQL...)。這些查詢通常可以被更新,改進。

即席分析

淺析 Flink Table/SQL API

針對流資料的即席查詢,以實時的方式進行分析和瀏覽資料。查詢結果直接顯示在notebook(Apache Zeppelin)中。

Flink社群還提出來和資料庫中Materialized View很相似的Dynamic table 動態表概念,將在以後的版本中支援,具體細節將另開文章解釋。

原文連線 http://mtunique.com/flink_sql/

相關文章