Flink 系列之十三 - Data Stream API的输出算子底层原理

之前做过数据平台,对于实时数据采集,使用了Flink。现在想想,在数据开发平台中,Flink的身影几乎无处不在,由于之前是边用边学,总体有点混乱,借此空隙,整理一下Flink的内容,算是一个知识积累,同时也分享给大家。

注意由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是 Flink-1.19.x,Flink支持多种语言,这里的所有代码都是使用java,JDK版本使用的是19
代码参考:https://github.com/forever1986/flink-study.git

目录

  • 1 基本原理
  • 2 自定义Sink

上一章了解了一些常见的输出算子,那么如果希望输出的外部系统Flink并未提供,这就需要用户自己写一个自定义的输出算子。

1 基本原理

在写自定义输出算子之前,先来了解一下基本原理:这里是基于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功能,要实现以下几点:

  • Sink实现接口SupportsWriterState,并实现方法restoreWriter(这个是从checkpoint中回复是初始化SinkWriter)和getWriterStateSerializer(这个是Sink的序列化数据)
  • SinkWriter要改为实现StatefulSinkWriter接口,该接口比SinkWriter接口多了snapshotState方法(返回要存入checkpoint数据)
  • 实现一个WriterState类,这个用于保存到checkpoint的数据,可以自定义
  • 实现SimpleVersionedSerializer接口的序列化,也就是将WriterState类序列化

5)如果要实现两段式事务,要实现以下几点:

  • 两段式事务,就是会将数据写入到目标,但是数据只是标记为预写入(这个要你的目标支持两段式协议,比如MySQL的XA等)
  • Sink实现接口SupportsCommitter,并实现方法createCommitter(第二段提交)和getCommittableSerializer(序列化)
  • SinkWriter实现接口CommittingSinkWriter,并实现prepareCommit(第一段预提交)

2 自定义Sink

了解了基本的原理,这里通过一个简单的案例来自定义一个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)运行结果:

Flink 系列之十三 - Data Stream API的输出算子底层原理_第1张图片

结语:本章完成了对输出算子底层的原理解析以及如何自定义一个Sink。下一章,将来了解一个经常被忽略的内容,就是Flink的数据类型。

你可能感兴趣的:(flink,flink,大数据,输出算子,底层原理)