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所共有的參數。
共有配置
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
。 通過withPravegaConfig
將PravegaConfig
對象傳遞給相應的構建器。 例如:
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定義了數據序列化/反序列化的標準接口,核心接口是:
-
org.apache.flink.api.common.serialization.SerializationSchema
-
org.apache.flink.api.common.serialization.DeserializationSchema
Flink內置的序列化器包括:
-
org.apache.flink.api.common.serialization.SimpleStringSchema
-
org.apache.flink.api.common.serialization.TypeInformationSerializationSchema
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中的數據
-
io.pravega.connectors.flink.serialization.PravegaSerializationSchema
-
io.pravega.connectors.flink.serialization.PravegaDeserializationSchema
下面是一個示例,將實現了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的RichParallelSourceFunction
和RichSinkFunction
,以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 到ReaderCheckpointHook
。ReaderCheckpointHook
處理程序通知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
根據性能以及持久化保證的權衡,支持三種寫模式:
-
最多一次(BEST_EFFORT):任何寫入失敗都將被忽略, 因此可能會出現數據丟失。
-
最少一次(ATLEAST_ONCE):所有事件都在Pravega持續存在。由於重試或在失敗和後續恢復的情況下, 可能會發生重複的事件。
-
僅一次(EXACTLY_ONCE):集成Flink Checkpoint功能使用事務性寫入,在 Pravega 中保留所有事件且僅寫入一次。
默認情況下, 啓用ATLEAST_ONCE
選項。
DataSet API
DataSet API是Flink進行批處理程序中使用的API。Pravega Stream作爲數據源和數據匯。Pravega Flink Connectors擴展了Flink的RichInputFormat
和RichOutputFormat
,實現了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提供了FlinkPravegaTableSource
和FlinkPravegaTableSink
使用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系列的最後一篇, 希望這系列文章能讓你對流和流式實時計算有一個初步的瞭解,也歡迎感興趣的朋友後續繼續交流。下面是我們這個系列的文章標題,以備參考:
-
Flink流計算編程–Pravega+Flink
作者簡介
滕昱:就職於 DellEMC 非結構化數據存儲部門 (Unstructured Data Storage) 團隊並擔任軟件開發總監。2007 年加入 DellEMC 以後一直專注於分佈式存儲領域。參加並領導了中國研發團隊參與兩代 DellEMC 對象存儲產品的研發工作並取得商業上成功。從 2017 年開始,兼任 Streaming 存儲和實時計算系統的設計開發與領導工作。
黃飛情,現就職於DellEMC,10年+ 存儲、分佈式虛擬化、雲計算開發以及架構設計經驗,現從事流存儲和實時計算系統的設計與開發工作;
周煜敏,復旦大學計算機專業研究生,從本科起就參與 DellEMC 分佈式對象存儲的實習工作。現參與 Flink 相關領域研發工作。