之前做过数据平台,对于实时数据采集,使用了Flink。现在想想,在数据开发平台中,Flink的身影几乎无处不在,由于之前是边用边学,总体有点混乱,借此空隙,整理一下Flink的内容,算是一个知识积累,同时也分享给大家。
注意:由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是 Flink-1.19.x,Flink支持多种语言,这里的所有代码都是使用java,JDK版本使用的是19。
代码参考:https://github.com/forever1986/flink-study.git
上一章了解了一些常见的输出算子,那么如果希望输出的外部系统Flink并未提供,这就需要用户自己写一个自定义的输出算子。
在写自定义输出算子之前,先来了解一下基本原理:这里是基于SinkV2 plus版本讲解。
1)先要实现一个Sink接口,下面源码注释Sink接口的用途:
package org.apache.flink.api.connector.sink2;
import org.apache.flink.annotation.Experimental;
import org.apache.flink.annotation.PublicEvolving;
import org.apache.flink.api.common.JobID;
import org.apache.flink.api.common.JobInfo;
import org.apache.flink.api.common.TaskInfo;
import org.apache.flink.api.common.operators.MailboxExecutor;
import org.apache.flink.api.common.operators.ProcessingTimeService;
import org.apache.flink.api.common.serialization.SerializationSchema;
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.metrics.groups.SinkWriterMetricGroup;
import org.apache.flink.util.UserCodeClassLoader;
import java.io.IOException;
import java.io.Serializable;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.function.Consumer;
/**
* Sink只是声明返回一个SinkWriter,真正执行写入的是SinkWriter
*/
@PublicEvolving
public interface Sink<InputT> extends Serializable {
/**
* 创建一个SinkWriter(这个是旧版本)
*/
@Deprecated
SinkWriter<InputT> createWriter(InitContext context) throws IOException;
/**
* 创建一个SinkWriter
*/
default SinkWriter<InputT> createWriter(WriterInitContext context) throws IOException {
return createWriter(new InitContextWrapper(context));
}
}
2)真正在taskManager执行的是一个SinkWriter
package org.apache.flink.api.connector.sink2;
import org.apache.flink.annotation.PublicEvolving;
import org.apache.flink.api.common.eventtime.Watermark;
import java.io.IOException;
/**
* 真正在taskManager执行的
*/
@PublicEvolving
public interface SinkWriter<InputT> extends AutoCloseable {
/**
* 来自上一个算子的数据,element的数据。
*/
void write(InputT element, Context context) throws IOException, InterruptedException;
/**
* 在启动一次checkpoint或者输入结束时调用的方法(如果是有批量写出,可以再次实现)
*/
void flush(boolean endOfInput) throws IOException, InterruptedException;
/**
* 水位线设置(这个后面涉及到再细讲水位线)
*/
default void writeWatermark(Watermark watermark) throws IOException, InterruptedException {}
}
3)理论上实现上面2个接口,即可实现一个Sink。但是Sink可以扩展功能,比如checkpoint、事务等,这些Flink都提供了接口,只需要继承接口并实现功能。
4)如果要实现checkpoint功能,要实现以下几点:
5)如果要实现两段式事务,要实现以下几点:
了解了基本的原理,这里通过一个简单的案例来自定义一个Sink
示例说明:将上游算子来的String数据,写入到minIO对象存储中。前提是已经部署一个minIO服务器,并创建flink-data桶
代码参考:lesson06子模块
1)在flink-study父项目中的pom引入minio的dependencyManagement版本管理
<dependency>
<groupId>io.miniogroupId>
<artifactId>minioartifactId>
<version>${minio.version}version>
dependency>
2)在lesson06子模块中引入minio依赖
<dependency>
<groupId>io.miniogroupId>
<artifactId>minioartifactId>
dependency>
3)实现MinIoSinkWriter类
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import org.apache.flink.api.connector.sink2.SinkWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
/**
* 真正运行在taskManager的代码
*/
public class MinIoSinkWriter implements SinkWriter<String> {
// minIO的客户端
private final MinioClient minioClient;
// 桶
private final String bucket;
// 子任务Id
private final int subtaskId;
public MinIoSinkWriter(String endpoint, String accessKey, String secretKey, String bucket, int subtaskId) {
this.minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
this.bucket = bucket;
this.subtaskId = subtaskId;
}
/**
* 每次有数据过来时,写入到minIO
*/
@Override
public void write(String element, Context context) throws IOException, InterruptedException {
try {
String objectName = String.format("flink-%d-%d.txt", subtaskId, System.currentTimeMillis());
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.stream(new ByteArrayInputStream(element.getBytes()), element.length(), -1)
.build());
} catch (Exception e) {
throw new RuntimeException("Upload to MinIO failed", e);
}
}
/**
* 在检查点开启或者输入结束时调用
*/
@Override
public void flush(boolean endOfInput) throws IOException, InterruptedException {
System.out.println("flush"+endOfInput);
}
/**
* 子任务销毁时调用
*/
@Override
public void close() throws Exception {
// 可以用于关闭不必要资源,这里minioClient无需关闭,则不用写什么内容
}
}
4)实现MinIOSink类
import org.apache.flink.api.connector.sink2.Sink;
import org.apache.flink.api.connector.sink2.SinkWriter;
import org.apache.flink.api.connector.sink2.WriterInitContext;
import java.io.IOException;
/**
* Sink是用于定义所需的内容,比如SinkWriter等
*/
public class MinIOSink implements Sink<String> {
private final String endpoint;
private final String accessKey;
private final String secretKey;
private final String bucket;
public MinIOSink(String endpoint, String accessKey, String secretKey, String bucket) {
this.endpoint = endpoint;
this.accessKey = accessKey;
this.secretKey = secretKey;
this.bucket = bucket;
}
/**
* 定义写入的SinkWriter,会被taskManager调用(该方法已经废弃,可使用新的方法)
*/
@Override
public SinkWriter<String> createWriter(InitContext context) throws IOException {
return new MinIoSinkWriter(endpoint, accessKey, secretKey, bucket, context.getTaskInfo().getIndexOfThisSubtask());
}
/**
* 定义写入的SinkWriter,会被taskManager调用
*/
@Override
public SinkWriter<String> createWriter(WriterInitContext context) throws IOException {
return new MinIoSinkWriter(endpoint, accessKey, secretKey, bucket, context.getTaskInfo().getIndexOfThisSubtask());
}
}
5)编写MinIOSinkDemo
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class MinIOSinkDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
// 假设输入数据为字符串流
DataStream<String> dataStream = env.fromElements("Hello", "MinIO", "Flink");
// 配置MinIO连接参数
String endpoint = "http://127.0.0.1:9005/";
String accessKey = "minioadmin";
String secretKey = "minioadmin";
String bucket = "flink-data";
// 添加自定义Sink
dataStream.sinkTo(new MinIOSink(endpoint, accessKey, secretKey, bucket));
env.execute("Write to MinIO Example");
}
}
6)运行结果:
结语:本章完成了对输出算子底层的原理解析以及如何自定义一个Sink。下一章,将来了解一个经常被忽略的内容,就是Flink的数据类型。