如何利用開源Flink和Pravega搭建完整的流處理架構?

Pravega 與 Flink 的設計理念類似,都以流爲基礎實現流批統一的接口便於應用使用。Pravega 團隊也希望與 Flink 一起打造從底層存儲到上層計算的統一大數據流水線架構。在開發層面,Pravega 與 Flink 也有着深度的合作,Pravega Flink Connector 的開發,特別是在 Flink 的端到端的僅一次語義實現的過程中,都得到了 Apache Flink PMC 成員的通力協作和大力支持。本文將從API的簡介及使用入手,重點介紹Pravega+Flink的流計算編程。

簡 介

Pravega是根據Apache 2.0許可證開源的流存儲引擎,爲連續流數據提供統一的Stream抽象。Pravega提供了持久化、強一致性以及高性能低延遲的數據存儲。同樣在實時大數據領域,Apache Flink是由Apache軟件基金會開發的開源分佈式處理引擎,用於對無界和有界數據流進行有狀態的計算。 Flink提供高吞吐量、低延遲的流數據計算,以及對事件發生時間處理和狀態管理的支持。Flink的應用程序在發生機器故障時具有容錯能力,並且支持exactly-once語義。

Pravega與Flink的設計理念類似,都以流爲基礎實現流批統一的接口便於應用使用。Pravega團隊也希望與Flink一起打造從底層存儲到上層計算的統一大數據流水線架構。Pravega從誕生之初就積極參加Flink社區的活動,自從2017年起在每一次的Flink Forward大會上都有相關內容的分享,包括在2018年12月第一次在中國舉辦的Flink Forward同樣也有Pravega中國團隊的參與。在開發層面,Pravega與Flink也有着深度的合作,Pravega Flink Connector 的開發,特別是在Flink的端到端的僅一次語義實現的過程中,都得到了Apache Flink PMC成員的通力協作和大力支持。

Pravega Flink Connectors

Pravega Flink Connectors實現了Flink的接口,提供了對Pravega Stream的讀取和寫入,並且結合Flink的Checkpoint機制提供了端到端的exactly-once處理語義(詳情可見上一篇文章)。Flink對數據有兩種讀和寫處理辦法,對應的,每一種API都需要定義Flink程序的數據源(Source)和數據匯(Sink)。Pravega Flink Connector打通存儲與計算之間的通道,Pravega就可以作爲統一的流存儲和消息總線,用戶可以使用統一的Flink API在Pravega Stream上進行批或者流計算,構建一個完整的實時數據倉庫。

Flink在數據源上自底向上有着4層抽象的API,對於Pravega而言需要提供三種不同的API來滿足不同的使用需求。

  • DataStream API

  • DataSet API

  • Table API

API抽象

在介紹這三種API之前,我們首先先了解一下這些API所共有的參數。

共有配置

1. PravegaConfig 類

Pravega Flink Connectors提供了一個配置對象PravegaConfig,用於爲Flink配置Pravega的上下文。PravegaConfig會自動從環境變量、配置文件屬性和程序運行時參數進行配置。

PravegaConfig信息來源如下:

設置 環境變量/ 系統屬性 / 程序參數 缺省值
Controller URI PRAVEGA_CONTROLLER_URI / pravega.controller.uri / --controller tcp://localhost:9090
Default Scope PRAVEGA_SCOPE / pravega.scope / --scope -
Credentials - -
Hostname Validation - true

創建PravegaConfig

創建PravegaConfig實例的推薦方法是利用Flink的ParameterTool

ParameterTool params = ParameterTool.fromArgs(args);
PravegaConfig config = PravegaConfig.fromParams(params);

如果您的應用程序不使用Flink提供的ParameterTool類,也可以使用fromDefaults創建PravegaConfig

PravegaConfig config = PravegaConfig.fromDefaults();

此外,PravegaConfig也提供了 builder-style API允許用戶覆蓋默認配置:

PravegaConfig config = PravegaConfig.fromDefaults()
    .withControllerURI("tcp://...")
    .withDefaultScope("SCOPE-NAME")
    .withCredentials(credentials)
    .withHostnameValidation(false);

使用PravegaConfig

隨連接器庫提供的所有各種源和接收器類都具有builder-style API,可接受通用配置的PravegaConfig。 通過withPravegaConfigPravegaConfig對象傳遞給相應的構建器。 例如:

PravegaConfig config = ...;

FlinkPravegaReader<MyClass> pravegaSource = FlinkPravegaReader.<MyClass>builder()
    .forStream(...)
    .withPravegaConfig(config)
    .build();

值得注意的是,source或sink的stream可以使用完整的stream名稱(使用/分隔scope和stream,例如my-scope/my-stream),也可以在設置DefaultScope的情況下使用不完整的stream名稱(例如my-stream)。

2. 序列化/反序列化

序列化/反序列化是指Flink程序中的數據元素與Pravega Stream中存儲的二進制消息相互轉換的過程。

Flink定義了數據序列化/反序列化的標準接口,核心接口是:

Flink內置的序列化器包括:

Pravega Connector可以使用Flink的序列化接口。例如,要將每個事件讀取爲UTF-8字符串:

DeserializationSchema<String> schema = new SimpleStringSchema();
FlinkPravegaReader<String> reader = new FlinkPravegaReader<>(..., schema);
DataStream<MyEvent> stream = env.addSource(reader);

與其他應用程序的互操作性

更常見的情況是,Pravega的數據由其它客戶端程序注入,使用Flink處理。此類應用程序使用的Pravega客戶端庫定義了用於處理事件數據的io.pravega.client.stream.Serializer接口。Pravega提供了內置的適配器,實現了序列化方法的轉化,使得Flink程序能夠正常讀寫Pravega中的數據

下面是一個示例,將實現了Pravega Serializer 接口的內置Java POJO類序列化器JavaSerializer傳遞給適配器的構造函數,最終數據轉化爲DataStream<MyEvent>進行進一步處理:

import io.pravega.client.stream.impl.JavaSerializer;
...
DeserializationSchema<MyEvent> adapter = new PravegaDeserializationSchema<>(
    MyEvent.class, new JavaSerializer<MyEvent>());
FlinkPravegaReader<MyEvent> reader = new FlinkPravegaReader<>(..., adapter);
DataStream<MyEvent> stream = env.addSource(reader);

Pravega序列化程序必須實現java.io.Serializable才能在Flink程序中使用。

3. Stream Cuts

StreamCut表示Pravega流中的特定位置,可以從與Pravega客戶端的各種API交互中獲得,它包含一組segment和offset的鍵值對。偏移量始終指向事件邊界,因此沒有指向不完整事件的offset。Pravega中的Checkpoint底層也由這一API實現。讀客戶端可以接受StreamCut作爲給定流的開始和/或結束位置。由於數據在不斷產生以及不斷地下沉至第二級存儲,stream的開始和結束位置都會發生變化,因此Pravega使用StreamCut.UNBOUNDED表示Stream中的不斷變化的位置。這樣的設計有助於使得Flink使用一套統一的API讀取用戶自定義的有邊界和無邊界的數據流。

DataStream API

DataStream API是Flink最常用的API,主要負責數據流的處理。數據流通過Source來創建,結果通過Sink返回,中間可以進行豐富的有狀態的transformation操作。Pravega Flink Connectors擴展了Flink的RichParallelSourceFunctionRichSinkFunction,以DataStream API實現了Pravega的數據讀寫。

FlinkPravegaReader

使用io.pravega.connectors.flink.FlinkPravegaReader的實例作爲Flink程序的數據源(Source)。FlinkPravegaReader讀取Pravega的一個或多個Stream,抽象爲Flink的DataStream

使用StreamExecutionEnvironment::addSource方法將Pravega Stream作爲DataStream打開。

代碼示例:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// Define the Pravega configuration
PravegaConfig config = PravegaConfig.fromParams(params);

// Define the event deserializer
DeserializationSchema<MyClass> deserializer = ...

// Define the data stream
FlinkPravegaReader<MyClass> pravegaSource = FlinkPravegaReader.<MyClass>builder()
    .forStream(...)
    .withPravegaConfig(config)
    .withDeserializationSchema(deserializer)
    .build();
DataStream<MyClass> stream = env.addSource(pravegaSource);

Pravega Flink Connectors提供builder-style API構造FlinkPravegaReader的實例,通常需要指定PravegaConfig,讀取的Stream名稱以及反序列化方法。如果需要並行讀取多個Stream,可以反覆調用forStream。 Stream也可以跨scope被Flink所讀取,只需指定完整的stream名稱scope/stream 即可。更多參數可參閱此表forStream提供了一個重載方法,可以接受StreamCut類型的參數以處理歷史流數據。

FlinkPravegaReader支持並行化,可使用setParallelism方法配置要執行的並行實例的數量。 每個實例消耗一個或多個segment。

讀客戶端與Flink checkpoints和savepoints兼容,其中會包含從正確位置恢復所需的所有信息,讀客戶端支持倒回到Pravega Checkpoint位置從故障中恢復。

Checkpoint的工作過程分爲兩步:

  • FlinkPravegaReader在初始化期間註冊 ReaderCheckpointHook,Job manager中的master hook處理程序啓動triggerCheckpoint request 到ReaderCheckpointHookReaderCheckpointHook處理程序通知Pravega檢查當前讀客戶端狀態。這是一個非阻塞調用,一旦Pravega讀者完成了檢查點,就會返回。

  • Pravega將發送CheckPoint事件作爲數據流流的一部分,並且在接收事件時,FlinkPravegaReader將啓動triggerCheckpoint請求以有效地讓Flink繼續並完成檢查點過程。

FlinkPravegaReader默認開啓性能指標(Metrics)監控,可使用.enableMetrics(false)選項禁用。用戶可以實時地觀察Flink消費Pravega數據的運行狀態,性能指標包括:

名稱 說明
readerGroupName Reader組名稱
scope Reader組的範圍名稱
streams 作爲Reader組一部分的流的完全限定名稱 (i.e., scope/stream)
onlineReaders 當前在線/可用的Reader
segmentPositions StreamCut信息,指示Reader到目前爲止所閱讀的位置。
unreadBytes 尚未讀取的總字節數

FlinkPravegaWriter

使用io.pravega.connectors.flink.FlinkPravegaWriter的實例作爲Flink程序裏面的數據匯(Sink)。使用DataStream::addSink方法將寫客戶端的實例添加到Flink程序中。

代碼示例:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// Define the Pravega configuration
PravegaConfig config = PravegaConfig.fromParams(params);

// Define the event serializer
SerializationSchema<MyClass> serializer = ...

// Define the event router for selecting the Routing Key
PravegaEventRouter<MyClass> router = ...

// Define the sink function
FlinkPravegaWriter<MyClass> pravegaSink = FlinkPravegaWriter.<MyClass>builder()
   .forStream(...)
   .withPravegaConfig(config)
   .withSerializationSchema(serializer)
   .withEventRouter(router)
   .withWriterMode(EXACTLY_ONCE)
   .build();

DataStream<MyClass> stream = ...
stream.addSink(pravegaSink);

FlinkPravegaWriter同樣利用builder API構造,通常需要指定PravegaConfig,寫入的Stream名稱,寫入的模式,路由函數以及序列化方法。完整參數列表可參閱此表

寫入Pravega stream的每個事件都有一個關聯的路由鍵。路由鍵是事件分segment的依據以及有序性保證的基礎,詳細信息可參閱之前Pravega系列文章。用戶需要提供io.pravega.connectors.flink.PravegaEventRouter接口的實現,例如,爲了保證特定於傳感器 id 的寫入順序,您可以提供如下所示的路由實現。

private static class SensorEventRouter<SensorEvent> implements PravegaEventRouter<SensorEvent> {
        @Override
        public String getRoutingKey(SensorEvent event) {
            return event.getId();
        }
    }

用戶可以進一步使用FlinkPravegaUtils::writeToPravegaInEventTimeOrder 方法將給定的 DataStream按照事件時間順序寫入Pravega流,該方法將自動對事件按事件時間進行排序 (基於每個鍵)。

FlinkPravegaWriter根據性能以及持久化保證的權衡,支持三種寫模式:

  1. 最多一次(BEST_EFFORT):任何寫入失敗都將被忽略, 因此可能會出現數據丟失。

  2. 最少一次(ATLEAST_ONCE):所有事件都在Pravega持續存在。由於重試或在失敗和後續恢復的情況下, 可能會發生重複的事件。

  3. 僅一次(EXACTLY_ONCE):集成Flink Checkpoint功能使用事務性寫入,在 Pravega 中保留所有事件且僅寫入一次。

默認情況下, 啓用ATLEAST_ONCE選項。

DataSet API

DataSet API是Flink進行批處理程序中使用的API。Pravega Stream作爲數據源和數據匯。Pravega Flink Connectors擴展了Flink的RichInputFormatRichOutputFormat,實現了DataSet API的Pravega讀寫。參數與DataStream API類似。

FlinkPravegaInputFormat

使用io.pravega.connectors.flink.FlinkPravegaInputFormat的實例用作Flink批處理程序中的數據源。 輸入格式將流的事件作爲DataSet(Flink Batch API的基本抽象)讀取。此輸入格式並行處理stream segments,而不遵循路由鍵順序。使用ExecutionEnvironment::createInput方法將Pravega Stream作爲DataSet打開。

代碼示例:

// Define the Pravega configuration
PravegaConfig config = PravegaConfig.fromParams(params);

// Define the event deserializer
DeserializationSchema<EventType> deserializer = ...

// Define the input format based on a Pravega stream
FlinkPravegaInputFormat<EventType> inputFormat = FlinkPravegaInputFormat.<EventType>builder()
    .forStream(...)
    .withPravegaConfig(config)
    .withDeserializationSchema(deserializer)
    .build();

DataSource<EventType> dataSet = env.createInput(inputFormat, TypeInformation.of(EventType.class)
                                   .setParallelism(2);

FlinkPravegaOutputFormat

使用io.pravega.connectors.flink.FlinkPravegaOutputFormat的實例用作Flink批處理程序中的數據匯。 使用DataSet::output方法將寫客戶端的實例添加到Flink程序中。

代碼示例:

// Define the Pravega configuration
PravegaConfig config = PravegaConfig.fromParams(params);

// Define the event serializer
SerializationSchema<EventType> serializer = ...

// Define the event router for selecting the Routing Key
PravegaEventRouter<EventType> router = ...

// Define the input format based on a Pravega Stream
FlinkPravegaOutputFormat<EventType> outputFormat = FlinkPravegaOutputFormat.<EventType>builder()
    .forStream(...)
    .withPravegaConfig(config)
    .withSerializationSchema(serializer)
    .withEventRouter(router)
    .build();

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
Collection<EventType> inputData = Arrays.asList(...);
env.fromCollection(inputData)
   .output(outputFormat);
env.execute("...");

Table API

Table API 是一種關係型API,用戶可以使用簡單的SQL操作數據,降低了大數據分析的門檻。更重要的是,Table API統一了Flink裏批和流不同的API。在同一套API下,批處理的查詢返回有限數據集,而流處理的查詢則會持續返回流式結果。因此,這也是Flink社區貢獻的熱點和Flink未來的發展重點。Pravega Flink Connectors提供了FlinkPravegaTableSourceFlinkPravegaTableSink使用Flink Table API讀寫Pravega數據,以便用戶使用SQL語句操作數據。用戶既可以在代碼中使用Pravega Descriptor指定數據源,也可以通過聲明式的YAML配置文件啓用SQL client.

FlinkPravegaTableSource/Sink

Pravega Stream可以用作Flink Table程序中的Table源。 Flink Table API面向Flink的TableSchema類,它們描述了table字段。 然後使用FlinkPravegaTableSource/Sink的具體子類將流數據解析爲符合table模式的Row對象進行相應的讀寫。

FlinkPravegaTableSource 通過 TableEnvironment::registerTableSource連接,Sink的創建過程與Source類似,FlinkPravegaTableSink 通過 table.writeToSink(sink)寫入。

代碼示例:

以下示例使用Table API從Pravega Stream讀取JSON格式的用戶網站訪問事件:

// define table schema definition
Schema schema = new Schema()
        .field("user", Types.STRING())
        .field("uri", Types.STRING())
        .field("accessTime", Types.SQL_TIMESTAMP()).rowtime(
                new Rowtime().timestampsFromField("accessTime")
                        .watermarksPeriodicBounded(30000L));

// define pravega reader configurations using Pravega descriptor
Pravega pravega = new Pravega();
pravega.tableSourceReaderBuilder()
        .withReaderGroupScope(stream.getScope())
        .forStream(stream)
        .withPravegaConfig(pravegaConfig);

 // Streaming Source
StreamExecutionEnvironment execEnvRead = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(execEnvRead);

StreamTableDescriptor desc = tableEnv.connect(pravega)
        .withFormat(new Json().failOnMissingField(true).deriveSchema())
        .withSchema(schema)
        .inAppendMode();

final Map<String, String> propertiesMap = DescriptorProperties.toJavaMap(desc);
final TableSource<?> source = TableFactoryService.find(StreamTableSourceFactory.class, propertiesMap)
        .createStreamTableSource(propertiesMap);

tableEnv.registerTableSource("MyTableRow", source);
String sqlQuery = "SELECT user, count(uri) from MyTableRow GROUP BY user";
Table result = tableEnv.sqlQuery(sqlQuery);
...

利用TableEnvironment::connect連接Pravega的相應stream,並通過withFormat方法指定數據格式,withSchema方法指定表結構。之後通過相應的工廠模式,以及傳入的裝飾器的Map來構造FlinkPravegaTableSource的具體子類。

FlinkPravegaTableSource/Sink支持Flink流和批處理環境。 數據讀取在流處理環境中使用FlinkPravegaReader/Writer實現;在批處理環境中使用FlinkPravegaInputFormat/OutputFormat實現。

快速入門

程序中使用

在編寫Flink代碼之前,需要確保Flink集羣能夠訪問到Pravega集羣,並且將Pravega Connector的依賴添加到項目中,pom.xml如下:

<dependency>
  <groupId>io.pravega</groupId>
  <artifactId>pravega-connectors-flink_2.11</artifactId>
  <version>0.3.2</version>
</dependency>

用戶應該根據所運行的Pravega的版本使用對應的Pravega Connector版本。在運行/部署應用程序時,Pravega Flink Connector不屬於Flink的核心運行時,因此用戶需要保證其代碼必須是應用程序代碼artifacts (JAR文件)的一部分。

使用SQL客戶端

Flink SQL Client是在Flink 1.6中引入的,旨在提供一種簡單的方法來編寫,調試和提交Table API程序到Flink集羣,而無需編寫Java或Scala代碼。SQL Client CLI允許在命令行上檢索和可視化運行的分佈式應用程序的實時結果。

Pravega Stream支持通過Flink的SQL客戶端使用標準SQL命令訪問。爲此,必須下載以下文件(可使用maven)並複製到Flink集羣library 路徑:$FLINK_HOME/lib

  • Pravega connector jar

  • Flink JSON jar(以json格式序列化/反序列化數據)

  • Flink Avro jar(以avro格式序列化/反序列化數據)

之後準備如下的SQL客戶端YAML格式的配置文件,並確保任何相關的Pravega Stream已經創建。詳細格式標準參見文檔

tables:
  - name: sample
    type: both
    update-mode: append
    # declare the external system to connect to
    connector:
      type: pravega
      version: "1"
      metrics: true
      connection-config:
        controller-uri: "tcp://localhost:9090"
        default-scope: wVamQsOSaCxvYiHQVhRl
      reader:
        stream-info:
          - stream: streamX
      writer:
        stream: streamX
        mode: atleast_once
        txn-lease-renewal-interval: 10000
        routingkey-field-name: category
    format:
      type: json
      fail-on-missing-field: true
      derive-schema: true
    schema:
      - name: category
        type: VARCHAR
      - name: value
        type: INT

functions: [] 

execution:
  # 'batch' or 'streaming' execution
  type: streaming
  # allow 'event-time' or only 'processing-time' in sources
  time-characteristic: event-time
  # interval in ms for emitting periodic watermarks
  periodic-watermarks-interval: 200
  # 'changelog' or 'table' presentation of results
  result-mode: table
  # parallelism of the program
  parallelism: 1
  # maximum parallelism
  max-parallelism: 128
  # minimum idle state retention in ms
  min-idle-state-retention: 0
  # maximum idle state retention in ms
  max-idle-state-retention: 0

deployment:
  # general cluster communication timeout in ms
  response-timeout: 5000
  # (optional) address from cluster to gateway
  gateway-address: ""
  # (optional) port from cluster to gateway
  gateway-port: 0

之後使用命令$FLINK-HOME/bin/sql-client.sh embedded -d <SQL_configuration_file> 以嵌入式模式運行SQL client shell,若能成功運行SELECT 'Hello World',即可以運行SQL命令與Pravega進行交互。

Pravega系列文章計劃

Pravega根據Apache 2.0許可證開源,0.5版本即將發佈。我們歡迎對流式存儲感興趣的大咖們加入Pravega社區,與Pravega共同成長。本篇文章爲Pravega系列的最後一篇, 希望這系列文章能讓你對流和流式實時計算有一個初步的瞭解,也歡迎感興趣的朋友後續繼續交流。下面是我們這個系列的文章標題,以備參考:

  1. 實時流處理(Streaming)統一批處理(Batch)的最後一塊拼圖:Pravega

  2. 開源Pravega架構解析:如何通過分層解決流存儲的三大挑戰?

  3. Pravega應用實戰:爲什麼雲原生特性對流存儲至關重要

  4. “ToB” 產品必備特性: Pravega的動態彈性伸縮

  5. 取代 ZooKeeper!高併發下的分佈式一致性開源組件 StateSynchronizer

  6. 分佈式一致性解決方案 - 狀態同步器 (StateSynchronizer) API 示例

  7. 流處理系統正確性基石:ExactlyOnce 的設計和實現

  8. Flink流計算編程–Pravega+Flink

作者簡介

滕昱:就職於 DellEMC 非結構化數據存儲部門 (Unstructured Data Storage) 團隊並擔任軟件開發總監。2007 年加入 DellEMC 以後一直專注於分佈式存儲領域。參加並領導了中國研發團隊參與兩代 DellEMC 對象存儲產品的研發工作並取得商業上成功。從 2017 年開始,兼任 Streaming 存儲和實時計算系統的設計開發與領導工作。

黃飛情,現就職於DellEMC,10年+ 存儲、分佈式虛擬化、雲計算開發以及架構設計經驗,現從事流存儲和實時計算系統的設計與開發工作;

周煜敏,復旦大學計算機專業研究生,從本科起就參與 DellEMC 分佈式對象存儲的實習工作。現參與 Flink 相關領域研發工作。

參考鏈接

  1. https://www.pravega.io

  2. http://pravega.io/connectors/flink/docs/latest/

  3. https://github.com/pravega/flink-connectors

  4. https://github.com/pravega/pravega-samples/tree/master/flink-connector-examples

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