输出算子(sink)
连接到外部系统
Flink 的 DataStream API 专门提供了向外部写入数据的方法:addSink。与 addSource类似,
addSink 方法对应着一个“Sink”算子,主要就是用来实现与外部系统连接、并将数据提交写
入的;Flink程序中所有对外的输出操作,一般都是利用Sink算子完成的。
与Source 算子非常类似,除去一些Flink预实现的Sink,一般情况下Sink算子的创建是
通过调用DataStream的addSink()方法实现的。
stream.addSink(new SinkFunction(…))
addSource 的参数需要实现一个SourceFunction接口;类似地,addSink方法同样需要传入
一个参数,实现的是SinkFunction接口。在这个接口中只需要重写一个方法invoke(),用来将指
定的值写入到外部系统中。这个方法在每条数据记录到来时都会调用:
default void invoke(IN value, Context context) throws Exception
当然,SinkFuntion 多数情况下同样并不需要我们自己实现。Flink官方提供了一部分的框
架的Sink连接器
如图所示,列出了Flink官方目前支持的第三方系统连接器:
我们可以看到,像Kafka之类流式系统,Flink提供了完美对接,Source/Sink两端都能连
接,可读可写;而对于Elasticsearch、文件系统(FileSystem)、 JDBC 等数据存储系统,则只
提供了输出写入的Sink连接器。
除Flink 官方之外,Apache Bahir 作为给Spark和Flink提供扩展支持的项目,也实现了一
些其他第三方系统与Flink的连接器 如图所示。
除此以外,就需要用户自定义实现Sink连接器了。
输出到文件
最简单的输出方式,当然就是写入文件了。对应着读取文件作为输入数据源,Flink 本来
也有一些非常简单粗暴的输出到文件的预实现方法:如 writeAsText()、writeAsCsv(),可以直
接将输出结果保存到文本文件或Csv文件。目前这些简单的方法已经要被弃用。
Flink 为此专门提供了一个流式文件系统的连接器:StreamingFileSink,它继承自抽象类
RichSinkFunction,而且集成了 Flink 的检查点(checkpoint)机制,用来保证精确一次(exactly
once)的一致性语义。
StreamingFileSink 为批处理和流处理提供了一个统一的Sink,它可以将分区文件写入Flink
支持的文件系统。它可以保证精确一次的状态一致性,大大改进了之前流式文件输出的方式。
它的主要操作是将数据写入桶(buckets),每个桶中的数据都可以分割成一个个大小有限的分
区文件。
StreamingFileSink 支持行编码(Row-encoded)和批量编码(Bulk-encoded,比如 Parquet)
格式。这两种不同的方式都有各自的构建器(builder),调用方法也非常简单,可以直接调用
StreamingFileSink 的静态方法:
⚫ 行编码:StreamingFileSink.forRowFormat t(basePath,rowEncoder)。
⚫ 批量编码:StreamingFileSink.forBulkFormat(basePath,bulkWriterFactory)。
在创建行或批量编码 Sink 时,我们需要传入两个参数,用来指定存储桶的基本路径
(basePath)和数据的编码逻辑(rowEncoder或bulkWriterFactory)。
下面我们就以行编码为例,将一些测试数据直接写入文件:
import org.apache.flink.api.common.serialization.SimpleStringEncoder
import org.apache.flink.core.fs.Path
import
org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink
import
org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.Defa
ultRollingPolicy
73
74
import org.apache.flink.streaming.api.scala._
import java.util.concurrent.TimeUnit
object SinkToFileTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(4)
val stream = env.fromElements(
Event("Mary", "./home", 1000L),
Event("Bob", "./cart", 2000L),
Event("Alice", "./prod?id=100", 3000L),
Event("Alice", "./prod?id=200", 3500L),
Event("Bob", "./prod?id=2", 2500L),
Event("Alice", "./prod?id=300", 3600L),
Event("Bob", "./home", 3000L),
Event("Bob", "./prod?id=1", 2300L),
Event("Bob", "./prod?id=3", 3300L)
)
val fileSink = StreamingFileSink
.forRowFormat(
new Path("./output"),
new SimpleStringEncoder[String]("UTF-8")
)
//通过.withRollingPolicy()方法指定“滚动策略”
.withRollingPolicy(
DefaultRollingPolicy.builder()
.withRolloverInterval(TimeUnit.MINUTES.toMillis(15))
.withInactivityInterval(TimeUnit.MINUTES.toMillis(5))
.withMaxPartSize(1024 * 1024 * 1024)
.build()
)
.build
stream.map(_.toString).addSink(fileSink)
env.execute()
}
}
这里我们创建了一个简单的文件Sink,通过withRollingPolicy()方法指定了一个“滚动策
略”。上面的代码设置了在以下3种情况下,我们就会滚动分区文件:
⚫ 至少包含15分钟的数据
⚫ 最近5分钟没有收到新的数据
75
⚫ 文件大小已达到1 GB
输出到Kafka
Flink官方为Kafka提供了Source和Sink的连接器,我们可以用它方便地从Kafka读写数
据。Flink与Kafka的连接器提供了端到端的精确一次(exactly once)语义保证,这在实际项
目中是最高级别的一致性保证。
(1)添加Kafka 连接器依赖
由于我们已经测试过从Kafka数据源读取数据,连接器相关依赖已经引入,这里就不重复
介绍了。
(2)启动Kafka集群
(3)编写输出到Kafka的示例代码
我们可以直接将用户行为数据保存为文件clicks.csv,读取后不做转换直接写入Kafka,主
题(topic)命名为“clicks”
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer
import java.util.Properties
object SinkToKafka {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val properties = new Properties()
properties.put("bootstrap.servers", "master:9092")
val stream = env.readTextFile("input/clicks.csv")
stream
.addSink(new FlinkKafkaProducer[String](
"clicks",
new SimpleStringSchema(),
properties
))
env.execute()
}
}
这里我们可以看到,addSink()方法传入的参数是一个FlinkKafkaProducer。
FlinkKafkaProducer继承了抽象类TwoPhaseCommitSinkFunction,这是一个实现了“两阶段提
76
交”的RichSinkFunction。两阶段提交提供了Flink向Kafka写入数据的事务性保证,能够真正
做到精确一次(exactly once)的状态一致性。
(4)运行代码,在Linux主机启动一个消费者, 查看是否收到数据
kafka-console-consumer.sh --bootstrap-server master:9092 --topic clicks
输出到Redis
Flink没有直接提供官方的Redis连接器,不过Bahir项目还是担任了合格的辅助角色,为
我们提供了Flink-Redis的连接工具。但版本升级略显滞后,目前连接器版本为1.1,支持的
Scala版本最新到2.11。
具体测试步骤如下:
(1)导入的Redis连接器依赖
<dependency>
<groupId>org.apache.bahir</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<version>1.0</version>
</dependency>
(2)启动Redis集群
(3)编写输出到Redis的示例代码
连接器为我们提供了一个RedisSink,它继承了抽象类RichSinkFunction,这就是已经实现
好的向Redis写入数据的SinkFunction。我们可以直接将Event数据输出到Redis:
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.redis.RedisSink
import
org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfi
g
import org.apache.flink.streaming.connectors.redis.common.mapper.{RedisCommand,
RedisCommandDescription, RedisMapper}
object SinkToRedis {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val conf = new FlinkJedisPoolConfig.Builder().setHost("master").build()
env.addSource(new ClickSource)
.addSink(new RedisSink[Event](conf, new MyRedisMapper()))
env.execute()
}
}
这里RedisSink的构造方法需要传入两个参数:
77
⚫ JFlinkJedisConfigBase:Jedis的连接配置。
⚫ RedisMapper:Redis映射类接口,说明怎样将数据转换成可以写入Redis的类型。
接下来主要就是定义一个Redis的映射类,实现RedisMapper接口。
class MyRedisMapper extends RedisMapper[Event] {
override def getKeyFromData(t: Event): String = t.user
override def getValueFromData(t: Event): String = t.url
override def getCommandDescription: RedisCommandDescription = new
RedisCommandDescription(RedisCommand.HSET, "clicks")
}
在这里我们可以看到,保存到Redis时调用的命令是HSET,所以是保存为哈希表(hash),
表名为“clicks”;保存的数据以user为key,以url为value,每来一条数据就会做一次转换。
(4)运行代码,Redis查看是否收到数据。
首先启动 redis服务
redis-server redis.conf
然后启动和客户端
redis-cli
输入密码
运行结果