流处理之 Source 与 Sink
实验介绍
在前面的实验中我们介绍过 Flink 的编程模型,其中有提到过 Source 和 Sink。Source 表示我们的程序从哪里获取数据,而 Sink 表示我们的程序要将处理之后的数据写到哪里去。本节实验中,我们除了要学习 Flink 提供的 Source 与 Sink 之外,还要学习自定义的 Source 与 Sink。
知识点
-
Source
- 从文件读取数据
- 从 Socket 读取数据
- 从集合读取数据
- 自定义 Source
-
Sink
- 写入文件(txt)
- 写入 Socket
- 自定义 Sink
Source
Source 中,从文件、Socket 和集合读取数据分别对应几个方法:
- env.readTextFile()
- env.socketTextStream()
- env.fromCollection()
- env.fromElement()
- env.generateSequence()
readTextFile、socketTextStream和fromCollection这三个方法在前面的实验中已经频繁使用了,所以这里不再举例。需要额外说明的是,fromCollection方法我们在前面使用的时候传入的参数是List对象,实际上所有序列以及迭代器对象都可以。如:
val iterator = Iterator(1,2,3,4)
val inputStream = env.fromCollection(iterator)
下面介绍新认识的两个方法以及自定义 Source:
fromElements(elements:_*)
从⼀个给定的对象序列中创建⼀个数据流,所有的对象必须是相同类型的。如:
val lst1 = List(1,2,3,4,5)
val lst2 = List(6,7,8,9,10)
val inputStream = env.fromElement(lst1, lst2)
generateSequence(from, to)
从给定的间隔中并⾏地产⽣⼀个数字序列。
val inputStream = env.generateSequence(1,10)
自定义 Source
一般来说,Flink 官方提供的 Source 和第三方依赖提供的 Source 已经完全可以满足我们日常的开发需求了,但是如果存在不能满足的情况,那么就需要我们自己去实现一个 Source 了。虽然这种情况少之又少,但其依然是一个很重要的知识点。
细心的同学可能已经发现了,我们在前面的实验中通过,env 对象是有一个 addSource 方法的,这个方法就是我们自定义 Source 用的。自定义 Source 总得来说可以分为两步:
- 自定义一个类
MySource,继承SourceFunction并重写其方法 - 将
MySource的实例对象作为参数传入addSource
在我们 FlinkLearning 工程的 com.vlab.source 包下创建一个名为 Source 的 Scala object,代码如下:
package com.vlab.source
import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.scala._
object Source {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val data = env.addSource(new MySource())
data.setParallelism(1).print()
env.execute()
}
class MySource extends SourceFunction[Integer]{
var flag = true
override def run(ctx: SourceFunction.SourceContext[Integer]): Unit = {
var count = 0
val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
while(flag){
for (elem <- list) {
ctx.collect(elem)
count += 1
if (count == 20)
cancel()
}
}
}
override def cancel(): Unit = {
flag = false
}
}
}
在上面的代码中,我们定义了一个名为 MySource 的类,该类继承了 SourceFunction 类,在 SourceFunction 类后面我们加了一个 Integer 的泛型,该泛型表示我们自定义 Source 中要生成的对象类型为 Integer。如果你要生成其它类型的对象(如:People),则替换成相应的类名就好。在 MySource 类当中有两个方法需要实现:run() 和 cancal()。cancel 方法是在取消数据生成的时候需要调用的,run 方法则是在生产数据的时候调用的。需要注意的是:run 方法有一个 ctx,该参数是整个自定义 Source 的上下文环境,我们在 run 方法中生成的数据,需要通过 ctx 对象,才能发送到 Flink 环境中被使用,或者说 ctx 对象是我们自定义 Source 和 Flink 环境之间的一个桥梁,注意代码中的 ctx.collect(elem)。
最后我们将 MySource() 类的实例对象作为参数传给 addSource() 方法,运行程序则看到类似如下输出(输出顺序可能不一致):
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
Kafka Source
Flink 作为一个流处理框架,肯定少不了和 Kafka 交互。如果要以 Kafka 作为 Source,需要在 pom.xml 文件中添加如下依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_2.12</artifactId>
<version>1.14.3</version>
</dependency>
打开终端,执行 cd /opt/kafka_2.13-3.8.1 进入到 Kafka 安装目录,然后执行以下命令启动 Kafka 自带的 Zookeeper:
sudo nohup bin/zookeeper-server-start.sh config/zookeeper.properties &
执行下面的命令启动 Kafka:
sudo bin/kafka-server-start.sh -daemon config/server.properties
执行下面的命令创建一个名为 vlab的 Topic:
sudo bin/kafka-topics.sh --zookeeper localhost:2181 --create --replication-factor 1 --partitions 1 --topic vlab
执行下面的命令启动一个 Console Producer:
sudo bin/kafka-console-producer.sh --broker-list localhost:9092 --topic vlab
至此,我们已经在 Kafka 中创建了一个名为 vlab的 Topic,并且启动了一个 Console Producer 负责生产数据,接下来在 com.vlab.source 包下创建一个 KafkaSource object,完整代码如下:
package com.vlab.source
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
import org.apache.kafka.common.serialization.StringDeserializer
import java.util.Properties
/**
* @projectName FlinkLearning
* @package com.vlab.source
* @className com.vlab.source.KafkaSource
* @description ${description}
* @author pblh123
* @date 2025/2/8 11:46
* @version 1.0
*
*/
object KafkaSource {
def main(args: Array[String]): Unit = {
// 获取当前执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
import org.apache.flink.streaming.api.scala._
// 配置kafka属性
val properties = new Properties()
properties.setProperty("bootstrap.servers", "192.168.137.81:9092")
properties.setProperty("key.deserializer", classOf[StringDeserializer].getName)
properties.setProperty("value.deserializer", classOf[StringDeserializer].getName)
properties.setProperty("auto.offset.reset", "latest")
// 创建 Kafka 消费者并读取 Kafka 数据流
val kafkaSource = new FlinkKafkaConsumer[String](
"vlab", // Kafka 主题名称
new SimpleStringSchema(), // 消息反序列化器
properties // Kafka 配置属性
)
// 从 Kafka 获取数据流
val dataStream = env.addSource(kafkaSource)
// 设置数据流的并行度为1并打印输出
dataStream.setParallelism(1).print()
// 启动执行环境,开始执行数据流处理,"KafkaSource" 为作业名称
env.execute("KafkaSource")
}
}
在上面的代码中,我们在 prop 对象中添加了 Kafka 的相关属性,然后通过 addSource 方法将 Kafka 和 Flink 做了绑定,监听了 vlab 这个 Topic,最后将接收到的数据做了打印。注意使用的 FlinkKafkaConsumer 类最后有个 011 标识,这个是和 Kafka 版本相关的,由于环境里安装的是 Kafka 3.8.1 的版本,在导包的时候注意。
最后在终端中发送数据,就可以在控制台看到相应输出了。
Sink
Flink 官方提供的 Sink,主要包含写入到文件和 Socket 中。
- writeAsText
- writeAsCsv
- writeToSocket
其中,writeAsText 将元素以字符串形式逐⾏写⼊(TextOutputFormat),这些字符串通过调⽤每个元素的 toString()⽅法来获取。writeAsCsv 将元组以逗号分隔写⼊⽂件中(CsvOutputFormat),⾏及字段之间的分隔符可以通过参数配置。每个字段的值来⾃对象的 toString()⽅法。
我们通过代码来实验一下,在我们 FlinkLearning 工程的 com.vlab.sink 包下创建一个名为 FlinkSink 的 Scala object。代码如下:
package com.vlab.sink
import org.apache.flink.core.fs.FileSystem
import org.apache.flink.streaming.api.scala._
/**
* @projectName FlinkLearning
* @package com.vlab.source
* @className com.vlab.source.FlinkSink
* @description ${description}
* @author pblh123
* @date 2025/2/8 12:01
* @version 1.0
*
*/
object FlinkSink {
def main(args: Array[String]): Unit = {
// 参数数量判断
// 当命令行参数数量不等于3时,打印错误信息并退出程序
if (args.length != 3) {
System.err.println("Usage: FlinkSink <input path> <txt output path> <csv output path>")
System.exit(5)
}
// 初始化输入和输出路径
val inputPath = args(0)
val outputPath = args(1)
val csvOutputPath = args(2)
// 获取执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 设置并行度为1,确保输出到一个文件
env.setParallelism(1)
// 读取文本文件
val data = env.readTextFile(inputPath)
// 打印数据到控制台
data.print()
// 将数据写入文本文件,检查文件系统类型,并指定路径(绝对路径更可靠)
data.writeAsText(outputPath, FileSystem.WriteMode.OVERWRITE)
// 将数据转换为CSV格式并写入文件
data.map(line => {
val fields = line.split(" ")
(fields(0), fields(1), fields(2))
}).writeAsCsv(csvOutputPath, FileSystem.WriteMode.OVERWRITE)
// 执行Flink作业
env.execute("Flink Sink")
}
}
然后在 /home/vlab/ 路径下创建一个名为 score.txt 的文件,文件内容如下:
赵露 语文 60
李阳 语文 70
刘明 语文 88
朱晓明 语文 90
李艳 语文 89
然后运行代码,所有输出结果如下图。
自定义 Sink
与自定义 Source 类似,自定义 Sink 也需要两步:
- 自定义一个类
MySink,继承SinkFunction并重写其方法 - 将
MySink的实例对象作为参数传入addSink
package com.vlab.sink
import org.apache.flink.streaming.api.functions.sink.SinkFunction
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
/**
* @projectName FlinkLearning
* @package com.vlab.sink
* @className com.vlab.sink.FlinkUDFSink
* @description ${description}
* @author pblh123
* @date 2025/2/8 12:34
* @version 1.0
*
*/
object FlinkUDFSink {
def main(args: Array[String]): Unit = {
// 参数数量判断
if (args.length != 1) {
System.err.println("Usage: FlinkSink <input path>")
System.exit(5)
}
// 获取输入路径,这里假设输入路径作为程序的第一个参数传入
val inputPath = args(0)
// 设置环境
// 初始化流执行环境,这是Flink程序执行的基础环境设置
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 设置并行度为1,这意味着所有操作都将在单个线程上执行,确保顺序性
env.setParallelism(1)
// 读取数据
// 从指定的文件路径读取文本数据,这提供了一个数据源
val data = env.readTextFile(inputPath)
// 添加自定义Sink
// 将数据流发送到自定义的Sink中,MySink用于处理每个数据项并输出相关信息
data.addSink(new MySink())
// 执行环境
// 启动Flink作业执行,"Flink UDF Sink"是作业的名称,用于在Flink UI中识别作业
env.execute("Flink UDF Sink")
}
/**
* 自定义Sink类
* 该类继承自SinkFunction,并重写了invoke方法,用于处理每个输入数据
*/
class MySink() extends SinkFunction[String] {
/**
* 重写invoke方法处理数据
*
* @param value 输入的数据项
* @param context 上下文,提供了访问Flink功能的方法
*/
override def invoke(value: String, context: SinkFunction.Context): Unit = {
// 获取当前处理时间
val time = context.currentProcessingTime()
// 获取当前水印
val waterMark = context.currentWatermark()
// 打印数据项、处理时间和水印
println(s"$value : $time : $waterMark")
}
}
}
在上面的代码中,我们定义了 MySink 类,该类继承了 SinkFunction 类,并重写了该类的 invoke 方法。在该方法中,我们通过 context 对象获取了 currentProcessingTime(处理时间戳)和 currentWatermark (水印)信息,然后和 data 中的元素一并打印输出。关于 currentProcessingTime 和 currentWatermark 大家暂时不用纠结,在后面的实验中我们会重点介绍。最后输出的结果如下:
实验总结
本节实验中我们介绍了 Flink 官方提供的 Source 和 Sink,以及自定义 Source 和 Sink。实际上在工作中我们很少使用自定义的方式去实现 Source 和 Sink,因为很多第三方 jar 包已经为我们实现了各种场景下的读取和写入需求。但是作为 Flink 知识体系里很重要的一个知识点,我们必须去了解它。