Flink Sink
使用 Flink 进行数据处理时,数据经 Data Source 流入,通过系列 Transformations 的转化,最终可以通过 Sink 将计算结果进行输出,Flink Data Sinks 就是用于定义数据流最终的输出位置。Flink 提供了几个较为简单的 Sink API 用于日常的开发,具体如下:
writeAsText
writeAsText 用于将计算结果以文本的方式并行地写入到指定文件夹下,除了路径参数是必选外,该方法还可以通过指定第二个参数来定义输出模式,有以下两个可选值:
WriteMode.NO_OVERWRITE:当指定路径上不存在任何文件时,才执行写出操作;
WriteMode.OVERWRITE:不论指定路径上是否存在文件,都执行写出操作;如果原来已有文件,则进行覆盖。
使用示例如下:
streamSource.writeAsText("D:\\out", FileSystem.WriteMode.OVERWRITE);
writeAsCsv
writeAsCsv 用于将计算结果以 CSV 的文件格式写出到指定目录,除了路径参数是必选外,该方法还支持传入输出模式,行分隔符,和字段分隔符三个额外的参数,其方法定义如下:
writeAsCsv(String path, WriteMode writeMode, String rowDelimiter, String fieldDelimiter)
print \ printToErr 是测试当中最常用的方式,用于将计算结果以标准输出流或错误输出流的方式打印到控制台上。
writeUsingOutputFormat
采用自定义的输出格式将计算结果写出,writeAsText 和 writeAsCsv 其底层调用的都是该方法,源码如下:
public DataStreamSink<T> writeAsText(String path, WriteMode writeMode) { TextOutputFormat<T> tof = new TextOutputFormat<>(new Path(path)); tof.setWriteMode(writeMode); return writeUsingOutputFormat(tof); }
writeToSocket
writeToSocket 用于将计算结果以指定的格式写出到 Socket 中,示例如下:
streamSource.writeToSocket("192.168.0.226", 9999, new SimpleStringSchema());
Streaming Connectors
Flink 中还内置了系列的 Connectors 连接器,用于将计算结果输入到常用的存储系统或者消息中间件中,具体如下:
- Apache Kafka (支持 source 和 sink)
- Apache Cassandra (sink)
- Amazon Kinesis Streams (source/sink)
- Elasticsearch (sink)
- Hadoop FileSystem (sink)
- RabbitMQ (source/sink)
- Apache NiFi (source/sink)
- Google PubSub (source/sink)
整合 Kafka Sink
addSink
Flink 提供addSink 方法用来调用自定义的 Sink 或者第三方的连接器,想要将计算结果写出到 Kafka,需要使用该方法来调用 Kafka 的生产者 FlinkKafkaProducer,具体代码如下:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 1.指定Kafka的相关配置属性 Properties properties = new Properties(); properties.setProperty("bootstrap.servers", "192.168.200.0:9092"); // 2.接收Kafka上的数据 DataStream<String> stream = env .addSource(new FlinkKafkaConsumer<>("flink-stream-in-topic", new SimpleStringSchema(), properties)); // 3.定义计算结果到 Kafka ProducerRecord 的转换 KafkaSerializationSchema<String> kafkaSerializationSchema = new KafkaSerializationSchema<String>() { @Override public ProducerRecord<byte[], byte[]> serialize(String element, @Nullable Long timestamp) { return new ProducerRecord<>("flink-stream-out-topic", element.getBytes()); } }; // 4. 定义Flink Kafka生产者 FlinkKafkaProducer<String> kafkaProducer = new FlinkKafkaProducer<>("flink-stream-out-topic", kafkaSerializationSchema, properties, FlinkKafkaProducer.Semantic.AT_LEAST_ONCE, 5); // 5. 将接收到输入元素*2后写出到Kafka stream.map((MapFunction<String, String>) value -> value + value).addSink(kafkaProducer); env.execute("Flink Streaming");
Producer分区
使用 FlinkKafkaProducer 往 kafka 中写数据时,如果不单独设置 partition 策略,默认使用 FlinkFixedPartitioner,该 partitioner 分区的方式是 task 所在的并发 id 对 topic 总 partition 数取余:parallelInstanceId % partitions.length
-
如果 sink 为 4,paritition 为 1,则 4 个 task 往同一个 partition 中写数据
-
sink task < partition 个数时会有部分 partition 没有数据写入,如 sink task 为 2,partition 总数为 4,则后面两个 partition 将没有数据写入
-
如果构建 FlinkKafkaProducer 时,partition 设置为 null,此时会使用 kafka producer 默认分区方式,非 key 写入的情况下,使用 round-robin 的方式进行分区,每个 task 都会轮循的写下游的所有 partition。该方式下游的 partition 数据会比较均衡,但是缺点是 partition 个数过多的情况下需要维持过多的网络连接,即每个 task 都会维持跟所有 partition 所在 broker 的连接
-
Semantic.NONE:Flink 不会有任何语义的保证,产生的记录可能会丢失或重复。Semantic.AT_LEAST_ONCE(默认设置):保证不会丢失任何记录(但是记录可能会重复),Semantic.EXACTLY_ONCE:使用 Kafka 事务提供精确一次语义。无论何时,在使用事务写入 Kafka 时,都要记得为所有消费 Kafka 消息的应用程序设置所需的 isolation.level(read_committed 或 read_uncommitted - 后者是默认值)
建用于输出测试的主题:
bin/kafka-topics.sh --create \ --bootstrap-server hadoop001:9092 \ --replication-factor 1 \ --partitions 1 \ --topic flink-stream-out-topic # 查看所有主题 bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092
启动一个 Kafka 消费者,用于查看 Flink 程序的输出情况:
bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic flink-stream-out-topic
在 Kafka 生产者上发送消息到 Flink 程序,观察 Flink 程序转换后的输出情况,具体如下:
bin/kafka-console-producer.sh --broker-list 192.168.21.120:9092 --topic fk-stream-in-topic
bin/kafka-console-consumer.sh --bootstrap-server 192.168.21.120:9092 --topic fk-stream-out-topic
自定义 Sink
Flink 还支持使用自定义的 Sink 来满足多样化的输出需求。想要实现自定义的 Sink ,需要直接或者间接实现 SinkFunction 接口。通常情况下,我们都是实现其抽象类 RichSinkFunction,相比于 SinkFunction ,其提供了更多的与生命周期相关的方法。两者间的关系如下:
自定义一个 FlinkToMySQLSink 为例,将计算结果写出到 MySQL 数据库中,具体步骤如下:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
继承自 RichSinkFunction,实现自定义的 Sink :
public class FlinkToMySQLSink extends RichSinkFunction<Employee> { private PreparedStatement stmt; private Connection conn; @Override public void open(Configuration parameters) throws Exception { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://192.168.0.229:3306/employees" + "?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false", "root", "123456"); String sql = "insert into emp(name, age, birthday) values(?, ?, ?)"; stmt = conn.prepareStatement(sql); } @Override public void invoke(Employee value, Context context) throws Exception { stmt.setString(1, value.getName()); stmt.setInt(2, value.getAge()); stmt.setDate(3, value.getBirthday()); stmt.executeUpdate(); } @Override public void close() throws Exception { super.close(); if (stmt != null) { stmt.close(); } if (conn != null) { conn.close(); } } }
想要使用自定义的 Sink,同样是需要调用 addSink 方法,具体如下:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); Date date = new Date(System.currentTimeMillis()); DataStreamSource<Employee> streamSource = env.fromElements( new Employee("hei", 10, date), new Employee("bai", 20, date), new Employee("ying", 30, date)); streamSource.addSink(new FlinkToMySQLSink()); env.execute();
浙公网安备 33010602011771号