Flink 使用介绍相关文档目录
Flink 使用介绍相关文档目录
Flink内置数据源
Text file
读取磁盘或者HDFS中的文件作为数据源。
唯一的参数file path可以指定:
- file:///path/to/file.txt
- hdfs:///path/to/file.txt
注意:
- 如果不填写前缀
file://
或者hdfs://
,默认为file://
- 使用Flink读取HDFS文件系统,需要去官网下载对应Pre-bundled Hadoop包。这里给出的链接是适用于Hadoop 2.8.3。之后将这个jar复制到flink安装位置的lib目录中。
val stream = env.readTextFile("/path/to/file.txt")
socketTextStream
使用socket作为数据源。但不推荐socket在生产环境中作为数据源。原因如下:
- socket无状态,也不能replay。故无法保证数据精准投送。
- socket数据源并行度只能是1,无法很好利用并发处理性能。
SocketTextStream适合用于debug或者是测试用途。
val stream = env.socketTextStream("localhost", 9000)
fromElements
将一系列元素作为数据源。
val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.fromElements(1, 2, 3);
fromCollection
和fromElements方法类似,不同的是该方法接收一个集合对象,而不是可变参数。如下所示:
val stream = env.fromCollection(Array(1, 2, 3))
Kafka 数据源
该数据源用于接收Kafka的数据。
使用Kafka数据源之前需要先确定Kafka的版本,引入对应的Kafka Connector以来。对应关系如下所示。
Kafka 版本 | Maven 依赖 |
---|---|
0.8.x | flink-connector-kafka-0.8_2.11 |
0.9.x | flink-connector-kafka-0.9_2.11 |
0.10.x | flink-connector-kafka-0.10_2.11 |
0.11.x | flink-connector-kafka-0.11_2.11 |
1.0 以上 | flink-connector-kafka_2.11 |
引入Maven依赖。以flink-connector-kafka_2.11
为例,添加以下依赖到pom.xml
文件:
org.apache.flink
flink-connector-kafka_2.11
1.9.1
在集群中运行时,为了减少提交jar包的大小,需要将该依赖设置为provided。然后把此依赖包复制到Flink各个节点安装位置的lib目录中。
一个简单的使用例子如下:
// 设置Kafka属性
val properties = new Properties()
properties.setProperty("bootstrap.servers", "192.168.100.128:9092")
properties.setProperty("group.id", "test")
// 创建Kafka数据源,其中test为topic名称
val consumer = new FlinkKafkaConsumer[String]("test", new SimpleStringSchema(), properties)
DeserializationSchema
DeserializationSchema用于将接收到的二进制数据转换为Java或Scala对象。Kafka Connector提供了如下4种DeserializationSchema:
- TypeInformationSerializationSchema:使用Flink的TypeInformation反序列化。如果上游数据也是通过Flink TypeInformation序列化后写入的,这里使用此schema最为合适。
- JsonDeserializationSchema :将获取的数据转换为JSON格式。这里有一个坑,如果发送过来的数据不是合法的JSON格式,数据源会抛出异常导致TaskManager重启。如果需要对不合法的JSON数据容错,需要实现自定义的DeserializationSchema。
- AvroDeserializationSchema:读取Avro格式的数据。
- SimpleStringSchema:转换接收到的数据为字符串。
自定义DeserializationSchema
所有的Schema需要实现DeserializationSchema。该接口源码如下所示:
@Public
public interface DeserializationSchema extends Serializable, ResultTypeQueryable {
/**
* Deserializes the byte message.
*
* @param message The message, as a byte array.
*
* @return The deserialized message as an object (null if the message cannot be deserialized).
*/
T deserialize(byte[] message) throws IOException;
/**
* Method to decide whether the element signals the end of the stream. If
* true is returned the element won't be emitted.
*
* @param nextElement The element to test for the end-of-stream signal.
* @return True, if the element signals end of stream, false otherwise.
*/
boolean isEndOfStream(T nextElement);
}
方法解释:
- deserialize:将二进制消息转换为某类型消息。
- isEndOfStream:表示是否是最后一条数据。
以SimpleStringSchema为例展示下怎么编写自定义的DeserializationSchema。
相关代码如下:
@PublicEvolving
public class SimpleStringSchema implements DeserializationSchema, SerializationSchema {
// SerializationSchema接口的方法省略
@Override
public String deserialize(byte[] message) {
return new String(message, charset);
}
@Override
public boolean isEndOfStream(String nextElement) {
return false;
}
// ...
}
起始位置属性配置
使用示例:
myConsumer.setStartFromEarliest() // start from the earliest record possible
myConsumer.setStartFromLatest() // start from the latest record
myConsumer.setStartFromTimestamp(...) // start from specified epoch timestamp (milliseconds)
myConsumer.setStartFromGroupOffsets() // the default behaviour
方法解释:
- setStartFromEarliest:从最早儿元素开始消费
- setStartFromLatest:从最近的元素开始消费
- setStartFromTimestamp:从指定时间戳的数据开始消费
- setStartFromGroupOffsets:这是默认的配置。从消费组的offset开始消费。必须配置group.id配置项。
Topic和分区感知
Topic感知
可以使用如下构造函数创建FlinkKafkaConsumer:
FlinkKafkaConsumer(Pattern subscriptionPattern, KafkaDeserializationSchema deserializer, Properties props)
和指定topic名称不同的是,这里传入的是一个正则表达式。所有名称匹配该正则表达式的topic都会被订阅。如果配置了分区感知(配置flink.partition-discovery.interval-millis
为非负数),Job启动之后kafka新创建的topic如果匹配该正则,也会被订阅到。
分区感知
在Job运行过程中如果kafka新创建了partition,Flink可以动态感知到,然后对其中数据进行消费。整个过程仍然可以保证exactly once语义。
默认情况分区感知是禁用的。如果要开启分区感知,可以设置flink.partition-discovery.interval-millis
,即分区感知触发时间间隔。
实现自定义数据源
自定义数据源需要实现Flink提供的SourceFunction接口。
SourceFunction接口的定义如下:
@Public
public interface SourceFunction extends Function, Serializable {
void run(SourceContext ctx) throws Exception;
void cancel();
}
run方法
run方法为数据源向下游发送数据的主要逻辑。编写套路为:
- 不断调用循环发送数据。
- 使用一个状态变量控制循环的执行。当
cancel
方法执行后必须能够跳出循环,停止发送数据。 - 使用SourceContext的collect等方法将元素发送至下游。
- 如果使用Checkpoint,在SourceContext collect数据的时候必须加锁。防止checkpoint操作和发送数据操作同时进行。
cancel方法:
cancel
方法在数据源停止的时候调用。cancel
方法必须能够控制run
方法中的循环,停止循环的运行。并做一些状态清理操作。
SourceContext类
SourceContext在SourceFunction中使用,用于向下游发送数据,或者是发送watermark。
SourceContext的方法包括:
- collect:向下游发送数据。有如下三种情况:
- 如果使用ProcessingTime,该元素不携带timestamp。
- 如果使用IngestionTime,元素使用系统当前时间作为timestamp。
- 如果使用EventTime,元素不携带timestamp。需要在数据流后续为元素指定timestamp(assignTimestampAndWatermark)。
- collectWithTimestamp:向下游发送带有timestamp的数据。和collect方法一样也有如下三种情况:
- 如果使用ProcessingTime,timestamp会被忽略
- 如果使用IngestionTime,使用系统时间覆盖timestamp
- 如果使用EventTime,使用指定的timestamp
- emitWatermark:向下游发送watermark。watermark也包含一个timestamp。向下游发送watermark意味着所有在watermark的timestamp之前的数据已经到齐。如果在watermark之后,收到了timestamp比该watermark的timestamp小的元素,该元素会被认为迟到,将会被系统忽略,或者进入到旁路输出(side output)。
- markAsTemporarilyIdle:标记此数据源暂时闲置。该数据源暂时不会发送任何数据和watermark。仅对IngestionTime和EventTime生效。下游任务前移watermark的时候将不会再等待被标记为闲置的数据源的watermark。
CheckpointedFunction
如果数据源需要保存状态,那么就需要实现CheckpointedFunction中的相关方法。
CheckpointedFunction包含如下方法:
- snapshotState:保存checkpoint的时候调用。需要在此方法中编写状态保存逻辑
- initializeState:在数据源创建或者是从checkpoint恢复的时候调用。此方法包含数据源的状态恢复逻辑。
样例
Flink官方给出的样板Source。这个数据源会发送0-999到下游系统。代码如下所示:
public class ExampleCountSource implements SourceFunction, CheckpointedFunction {
private long count = 0L;
// 使用一个volatile类型变量控制run方法内循环的运行
private volatile boolean isRunning = true;
// 保存数据源状态的变量
private transient ListState checkpointedCount;
public void run(SourceContext ctx) {
while (isRunning && count < 1000) {
// this synchronized block ensures that state checkpointing,
// internal state updates and emission of elements are an atomic operation
// 此处必须要加锁,防止在checkpoint过程中,仍然发送数据
synchronized (ctx.getCheckpointLock()) {
ctx.collect(count);
count++;
}
}
}
public void cancel() {
// 设置isRunning为false,终止run方法内循环的运行
isRunning = false;
}
public void initializeState(FunctionInitializationContext context) {
// 获取存储状态
this.checkpointedCount = context
.getOperatorStateStore()
.getListState(new ListStateDescriptor<>("count", Long.class));
// 如果数据源是从失败中恢复,则读取count的值,恢复数据源count状态
if (context.isRestored()) {
for (Long count : this.checkpointedCount.get()) {
this.count = count;
}
}
}
public void snapshotState(FunctionSnapshotContext context) {
// 保存数据到状态变量
this.checkpointedCount.clear();
this.checkpointedCount.add(count);
}
}