手把手教你如何写一个FLink Connecter(一)

手把手教你如何写一个FLink Connecter(一)

--sink connector篇

前言

flink sql目前逐渐被各大公司采用用于做实时数据。相比较代码coding的形式。使用flink sql更加的直观,开发成本更加低廉。目前flink sql中最重要的连接器也支持了各个大型的生态组建。如:Kafka,DynamoDB,Firehose,Kinesis,JDBC,Elasticsearch,写入文件系统,HBase,DataGen,Print 打印测试,BlackHole(写入一个黑洞),Hive(在1.12版本后开始支持)。
但是对于一些有自研组建的公司来说,或者对于connector有特殊要求,比如必须先完成数据库的写入在发送kafka。就需要自己编写flink connector。
目前flink 社区维护的也比较好。对于flink connecter也做了比较详细的说明。可以参考:https://nightlies.apache.org/flink/flink-docs-release-1.17/zh/docs/dev/table/sourcessinks/ 阅读理解,但是相对于官网来说,还不算特别的细致入微。本文将结合Wtable一个非关系型数据库详细的说明SINK和source中look up如何实现。

Wtable介绍

WTable是58架构平台部研发的分布式kv、klist系统。有数据库和表的概念,这与MySQL等关系型数据库类似。
每个数据库由一个唯一bid(business id)标识,bid由架构平台部统一分配.
在WTable中,每条数据都包含rowKey、colKey、value和score等4个属性。

在这里插入图片描述

FLink connector概览

flink connector流程

上面这个图是又flink官网中下载的自定义connector的流程图。从总体上已经很好的介绍了如何去写一个connector。

下面重点介绍一下sink connector各个继承类都需要做什么

高能预警,下方涉及了大量的代码!!!

DynamicTableSinkFactory

public class WTableDynamicTableFactory implements DynamicTableSinkFactory {

    /**
     * 用于创建sink connector的动态Sink描述器。
     * @param context 能获取表的各种参数信息.如:表名,字段名称,字段类型等等
     */
    @Override
    public DynamicTableSink createDynamicTableSink(Context context) {
        return null;
    }

    /**
     * 用于返回connector后面写的连接器的名称。注意需要区分大小写
     */
    @Override
    public String factoryIdentifier() {
        return null;
    }

    /**
     * connector中必要的参数,写在这里面的参数。如果在创建表的时候不填写,会报错的参数。
     * 如:数据库用户名,密码等等
     */
    @Override
    public Set<ConfigOption<?>> requiredOptions() {
        return null;
    }

    /**
     * connector中可选的参数.
     * 如:批量写入时间,条数。
     */
    @Override
    public Set<ConfigOption<?>> optionalOptions() {
        return null;
    }
}

DynamicTableSink

public class WTableDynamicTableSink implements DynamicTableSink {

    /**
     * 约定数据流中支持的数据流类型。INSERT(+I),UPSERT(+u,-u),DELETE(-D)
     * 如果在数据处理中只有append,可以只支持INSERT_ONLY,
     * 但是作为一个通用的sink connector还是得支持change的方式
     * @param requestedMode
     */
    @Override
    public ChangelogMode getChangelogMode(ChangelogMode requestedMode) {
        return null;
    }

    /**
     * 插入数据的方法,需要使用sink Function
     */
    @Override
    public SinkRuntimeProvider getSinkRuntimeProvider(Context context) {
        return null;
    }

    /**
     * 副本创建,只需要简单的调用构造方法
     */
    @Override
    public DynamicTableSink copy() {
        return null;
    }

    /**
     * 给sink connector总结一个名字,用于打印日志。
     */
    @Override
    public String asSummaryString() {
        return null;
    }
}

SinkFunction继承

public class WtableSinkFunction<T> extends RichSinkFunction<T> {

    /**
     * 全局运行一次,用于创建连接器
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
    }

    /**
     * 数据库关闭
     */
    @Override
    public void close() throws Exception {
        super.close();
    }

    /**
     * 数据插入。context为上下文环境, 与前面提到的context不同
     */
    @Override
    public void invoke(T value, Context context) throws Exception {

    }
}

上面三个方法总结了一个sink connector都需要做什么。来完成整个connector的运行。

当然在数据插入的时候,无可厚非的需要设计异步插入,批量插入等等来提高sink connector的执行效率

DynamicTableSinkFactory详细说明

本段要点

  1. 如果创建sink connector的描述器
  2. 如果传入连接器需要的参数

代码第一行需要为connector定义名称,这里命名为dw-wtable

public static final String IDENTIFIER = "dw-wtable";

后续的DynamicTableSink是在createDynamicTableSink中创建的。所以在这个方法中需要尽可能的获取后面需要的所有的信息传入到下游中

    @Override
    public DynamicTableSink createDynamicTableSink(Context context) {
        //由flink 提供的工具,来校验create语句是否书写准确
        FactoryUtil.TableFactoryHelper helper = createTableFactoryHelper(this, context);
        helper.validate();

        //获取sink中的需要的参数,传入到下文。用于创建数据库连接器与是否做批量写入的定义
        WTableOptions wTableOptions = getConfig(helper);

        //ResolvedSchema中保存的是字段的类型,主键等等信息
        ResolvedSchema resolvedSchema = context.getCatalogTable().getResolvedSchema();

        //由于数据库是K,V形式,需要获取主键信息用于确定rowkey的定义。在语句中为
        // PRIMARY key (a) NOT ENFORCED
        //todo:为了减少代码量,这里假定主键为一个字段。如果设计到多个字段需要自己定义主键的拼接方法
        Optional<UniqueConstraint> primaryKey = resolvedSchema.getPrimaryKey();
        //对于主键的检查。如果多余一个主键会抛出异常。这里原因同上面,可以自行约定
        validatePrimaryKey(primaryKey);
        String pk = primaryKey.get().getColumns().get(0);

        //约定好help中字段的序列化方式,flink已经实现好的序列化方式有:Json,DwbeziumJson,CanalJson,MaxwellJson等等
        //如果需要特殊的格式需要进行自定义
        EncodingFormat<SerializationSchema<RowData>> valueEncodingFormat =
                helper.discoverEncodingFormat(SerializationFormatFactory.class, VALUE_FORMAT);

        //获取主键的类型
        List<String> columnNames = resolvedSchema.getColumnNames();
        int pkIndex = columnNames.indexOf(pk);
        LogicalType pkDataType =
                resolvedSchema.getColumns().get(pkIndex).getDataType().getLogicalType();
        // TODO: 为了减少代码量约定为String,如果要支持更多的类型需要对类型进行判断
        if (!(pkDataType instanceof VarCharType)) {
            throw new RuntimeException("primary key just support string. " + pkDataType);
        }

        //创建Sink的描述器。
        return new WTableDynamicTableSink(
                wTableOptions,
                resolvedSchema.toPhysicalRowDataType(),
                pkIndex,
                valueEncodingFormat);
    }

上面代码对于flink sink的参数获取做了封装,下面是获取参数的代码

     /**
     * 这里对于wtable的参数做了封装
     * @return wtable连接器参数的实体类
     */
    private WTableOptions getConfig(FactoryUtil.TableFactoryHelper helper) {
        ReadableConfig options = helper.getOptions();
        WTableOptions wTableOptions = new WTableOptions();
        wTableOptions.setNc(options.get(WTABLE_NC));
        wTableOptions.setBid(options.get(WTABLE_BID));
        wTableOptions.setPassword(options.get(WTABLE_PASSWORD));
        wTableOptions.setTableId(options.get(WTABLE_TABLEID));
        wTableOptions.setCol(options.get(WTABLE_COL));
        wTableOptions.setTtl((int) options.get(WTABLE_TTL).getSeconds());
        wTableOptions.setBatchSize(options.get(WTABLE_BATCH_SIZE));
        wTableOptions.setFlushInterval((int) (options.get(WTABLE_FLUSH_INTERVAL).getSeconds()));
        return wTableOptions;
    }

WTableOptions 实体类信息,一个标准的bean对象。可以自行通过各种插件完成get,set方法

public class WTableOptions implements Serializable {
    private static final long serialVersionUID = 1L;
    String nc;
    String password;
    String col;
    int bid;
    int tableId;
    int ttl;
    int flushInterval;
    int batchSize;
}

factoryIdentifier方法为connector的名称,只需要注意大小写即可

    @Override
    public String factoryIdentifier() {
        return IDENTIFIER;
    }

连接器的参数的传入,这里不做过多的介绍。需要自行根据数据库判断需要传入哪些参数即可。

	 @Override
    public Set<ConfigOption<?>> requiredOptions() {
        Set<ConfigOption<?>> requiredOptions = new HashSet<>();
        requiredOptions.add(WTABLE_BID);
        requiredOptions.add(WTABLE_PASSWORD);
        requiredOptions.add(WTABLE_TABLEID);
        requiredOptions.add(WTABLE_TTL);
        requiredOptions.add(WTABLE_COL);
        return requiredOptions;
    }

    @Override
    public Set<ConfigOption<?>> optionalOptions() {
        Set<ConfigOption<?>> options = new HashSet<>();
        options.add(VALUE_FORMAT);
        options.add(WTABLE_NC);
        options.add(WTABLE_FLUSH_INTERVAL);
        options.add(WTABLE_BATCH_SIZE);
        return options;
    }

以下是对于参数定义的代码,在sql传入参数是,名字需要定义,且区分大小写

public class WTableConstant {

    public static final ConfigOption<String> VALUE_FORMAT =
            ConfigOptions.key("value.format")
                    .stringType()
                    .defaultValue("json")
                    .withDescription(
                            "Defines the format identifier for encoding value data. The identifier is used to discover a suitable format factory.");

    public static final ConfigOption<String> WTABLE_NC =
            ConfigOptions.key("wtable.nc")
                    .stringType()
                    .defaultValue("nameprod.wtable.58dns.org")
                    .withDescription("wtable password");

    public static final ConfigOption<String> WTABLE_PASSWORD =
            ConfigOptions.key("wtable.password")
                    .stringType()
                    .noDefaultValue()
                    .withDescription("wtable password");

    public static final ConfigOption<String> WTABLE_COL =
            ConfigOptions.key("wtable.col")
                    .stringType()
                    .noDefaultValue()
                    .withDescription("wtable password");

    public static final ConfigOption<Integer> WTABLE_BID =
            ConfigOptions.key("wtable.bid")
                    .intType()
                    .noDefaultValue()
                    .withDescription("wtable bid");

    public static final ConfigOption<Integer> WTABLE_TABLEID =
            ConfigOptions.key("wtable.tableid")
                    .intType()
                    .noDefaultValue()
                    .withDescription("wtable tableid");

    public static final ConfigOption<Duration> WTABLE_TTL =
            ConfigOptions.key("wtable.ttl")
                    .durationType()
                    .noDefaultValue()
                    .withDescription("wtable ttl");

    public static final ConfigOption<Duration> WTABLE_FLUSH_INTERVAL =
            ConfigOptions.key("sink.flush.interval")
                    .durationType()
                    .defaultValue(Duration.ofSeconds(1))
                    .withDescription("wtable flush interval");

    public static final ConfigOption<Integer> WTABLE_BATCH_SIZE =
            ConfigOptions.key("wtable.batch.size")
                    .intType()
                    .defaultValue(100)
                    .withDescription("wtable batch size");
}

以上为DynamicTableSinkFactory所有需要重写的方法。主要分为

  1. 创建sink table描述器,尽可能获取所有的信息传入

  2. 约定connector名称,sql中的语句:‘connector’ = ‘dw-wtable’

  3. 规定需要传入的参数

DynamicTableSink

上文提到,在createDynamicTableSink,中需要创建DynamicTableSink,下面来说明DynamicTableSink的具体流程.

本段要点:

  1. 如何约定数据流类型
  2. 定义数据写入方法
public class WTableDynamicTableSink implements DynamicTableSink {
    private WTableOptions wTableOptions;
    private DataType physicalDataType;
    private int pkIndex;
    private EncodingFormat<SerializationSchema<RowData>> valueEncodingFormat;

    /**
     * sink connector的构造方法。参数可以自己定义
     */
    public WTableDynamicTableSink(
            WTableOptions wTableOptions,
            DataType physicalDataType,
            int pkIndex,
            EncodingFormat<SerializationSchema<RowData>> valueEncodingFormat) {
        this.wTableOptions = wTableOptions;
        this.physicalDataType = physicalDataType;
        this.pkIndex = pkIndex;
        this.valueEncodingFormat = valueEncodingFormat;
    }

    /**
     * 约定数据流的类型。这里支持了 INSERT,UPDATE_AFTER,DELETE等方式。
     * 如果有特殊的数据流要求。可以自己更具ChangelogMode来定义
     */
    @Override
    public ChangelogMode getChangelogMode(ChangelogMode changelogMode) {
        return ChangelogMode.upsert();
    }

    /**
     * 数据在这里写入。定义sinkFunction
     */
    @Override
    public SinkRuntimeProvider getSinkRuntimeProvider(Context context) {
        //协定了序列化的描述器。用于后续序列化value
        SerializationSchema<RowData> valueEncoder =
                valueEncodingFormat.createRuntimeEncoder(context, physicalDataType);
        // TODO: 如果需要修改sink并行度,可以在这里加入参数。修改
        return SinkFunctionProvider.of(
                new WTableSinkFunction(wTableOptions, pkIndex, valueEncoder));
    }

    @Override
    public DynamicTableSink copy() {
        return new WTableDynamicTableSink(
                wTableOptions, physicalDataType, pkIndex, valueEncodingFormat);
    }

    @Override
    public String asSummaryString() {
        return "wtable-sink";
    }
}

这个方法主要用于构建sink connector的描述器。较为简单。这里不做更多的介绍

RichSinkFunction

一般而言,所有的数据库都会提供批量插入,修改等方法。用于减少网络交互。提高效率
最后写入的方法与对java功底要求更高一些,需要考虑如何做到批量插入
本段要点:

  1. 如何约定批量与周期型插入
public class WTableSinkFunction extends RichSinkFunction<RowData> {

    // 用于数据写入。String为key。后面为value
    // TODO 未来这边应该是 RowData
    private final Map<String, Tuple2<Boolean, Object>> reduceBuffer = new HashMap<>();
    //创建一个周期型线程池,用于固定时间写入数据
    private transient ScheduledExecutorService scheduler;
    private transient volatile boolean closed = false;
    //线程池调度器
    private transient ScheduledFuture<?> scheduledFuture;
    private transient volatile Exception flushException;
    //wtable客户端
    private transient WtableClient wtableClient;

    // wtable 配置
    private int tableId;
    private byte[] colKey;
    private int ttl;
    private int batchSize;
    private int pkIndex;

    final AtomicInteger counter = new AtomicInteger();
    private WTableOptions wTableOptions;
    private SerializationSchema<RowData> valueEncoder;

    public WTableSinkFunction(
            WTableOptions wTableOptions, int pkIndex, SerializationSchema<RowData> valueEncoder) {
        this.wTableOptions = wTableOptions;
        this.pkIndex = pkIndex;
        this.valueEncoder = valueEncoder;

        tableId = wTableOptions.getTableId();
        colKey = wTableOptions.getCol().getBytes(StandardCharsets.UTF_8);
        ttl = wTableOptions.getTtl();
        batchSize = wTableOptions.getBatchSize();
    }

    /**
     * sink的open方法。只在初始化的时候运行一次。
     * 周期性线程池调度器在这里创建。并在固定时间进行写入。
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        // init client.
        String password = wTableOptions.getPassword();
        int bid = wTableOptions.getBid();
        String nc = wTableOptions.getNc();
        wtableClient = WtableClient.getInstance(new WtableProperty(nc, bid, password));

        //在创建表的时候可以约定,写入周期.也可以设置默认值
        int flushInterval = wTableOptions.getFlushInterval();
        //创建周期型线程池。
        this.scheduler =
                Executors.newScheduledThreadPool(1, new ExecutorThreadFactory("wtable-sink"));

        //周期性线程池的使用方法可以参考网络
        this.scheduledFuture =
                this.scheduler.scheduleWithFixedDelay(
                        () -> {
                            synchronized (WTableSinkFunction.this) {
                                if (!closed) {
                                    try {
                                        flush();
                                    } catch (Exception e) {
                                        flushException = e;
                                    }
                                }
                            }
                        },
                        flushInterval,
                        flushInterval,
                        TimeUnit.SECONDS);
    }

    @Override
    public void close() throws Exception {
        if (!closed) {
            closed = true;

            if (this.scheduledFuture != null) {
                scheduledFuture.cancel(false);
                this.scheduler.shutdown();
            }

            if (reduceBuffer.size() > 0) {
                try {
                    flush();
                } catch (Exception e) {
                    throw new RuntimeException("Writing records to wtable failed.", e);
                }
            }
        }

        wtableClient.close();
        super.close();
    }

    /**
     * 数据处理阶段。每来一条数据,都会执行这个方法。如果数据库不考虑批量写入。可以将写入的方法用在此处
     */
    @Override
    public void invoke(RowData value, Context context) throws Exception {
        String key = value.getString(pkIndex).toString();
        boolean flag = changeFlag(value.getRowKind());

        //将数据保存到缓冲区。积累一定量后批量写入
        synchronized (this) {
            if (flag) {
                byte[] jsonValue = valueEncoder.serialize(value);
                SetArg setArg =
                        new SetArg(
                                tableId,
                                key.getBytes(StandardCharsets.UTF_8),
                                colKey,
                                jsonValue,
                                0,
                                ttl);
                reduceBuffer.put(key, Tuple2.of(flag, setArg));
            } else {
                reduceBuffer.put(
                        key,
                        Tuple2.of(
                                flag,
                                new DelArg(tableId, key.getBytes(StandardCharsets.UTF_8), colKey)));
            }
        }
    }

    /**
     * 写入逻辑。根据数据流的类型分为set数据集与del数据集
     * 由于设定了批量写入的最大条数。按照设定的batchSize,进行分组。分开几次进行插入
     * By the way:这里的逻辑处理只能处理按照指定的周期插入。无法满足,条数和周期数任意一中达成了就执行插入.
     *             如果想实现任意一条满足插入,可以使用google的RateLimiter来实现
     */
    private synchronized void flush() {
        Collection<List<SetArg>> setArgs;
        Collection<List<DelArg>> delArgs;
        synchronized (this) {
            counter.set(0);
            setArgs =
                    reduceBuffer.entrySet().stream()
                            .filter(v -> v.getValue().f0)
                            .map(v -> (SetArg) v.getValue().f1)
                            .collect(
                                    Collectors.groupingBy(
                                            it -> counter.getAndIncrement() / batchSize))
                            .values();

            counter.set(0);
            delArgs =
                    reduceBuffer.entrySet().stream()
                            .filter(v -> !v.getValue().f0)
                            .map(v -> (DelArg) v.getValue().f1)
                            .collect(
                                    Collectors.groupingBy(
                                            it -> counter.getAndIncrement() / batchSize))
                            .values();
            reduceBuffer.clear();
        }

        if (setArgs != null && !setArgs.isEmpty()) {
            setArgs.forEach(
                    params -> {
                        try {
                            wtableClient.mSetEx(params);
                        } catch (WtableException e) {
                            throw new RuntimeException(e);
                        }
                    });
        }

        if (delArgs != null && !delArgs.isEmpty()) {
            delArgs.forEach(
                    params -> {
                        try {
                            wtableClient.mDel(params);
                        } catch (WtableException e) {
                            throw new RuntimeException(e);
                        }
                    });
        }
    }

    /**
     * wtable连接器对于insert与delete有各自的方法。这里判断数据需要走那种类型
     */
    private boolean changeFlag(RowKind rowKind) {
        switch (rowKind) {
            case INSERT:
            case UPDATE_AFTER:
                return true;
            case DELETE:
            case UPDATE_BEFORE:
                return false;
            default:
                throw new UnsupportedOperationException(
                        String.format(
                                "Unknown row kind, the supported row kinds is: INSERT, UPDATE_BEFORE, UPDATE_AFTER,"
                                        + " DELETE, but get: %s.",
                                rowKind));
        }
    }

    private void checkFlushException() {
        if (flushException != null) {
            throw new RuntimeException("Writing records to wtable failed.", flushException);
        }
    }
}

本段的主要难点为如何高性能的插入数据。这个和数据库的种类有关。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,以下是一个简单的 Flink 入门程序。 首先,您需要安装 Flink 并设置环境变量。然后,您需要为您的项目添加 flink 的 Maven 依赖项,如下所示: ```xml <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-java</artifactId> <version>1.9.1</version> </dependency> ``` 接下来,您可以编您的 Flink 程序,如下所示: ```java import org.apache.flink.api.common.functions.FlatMapFunction; import org.apache.flink.api.java.DataSet; import org.apache.flink.api.java.ExecutionEnvironment; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.util.Collector; public class FlinkWordCount { public static void main(String[] args) throws Exception { final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); DataSet<String> text = env.fromElements( "Hello World", "Hello Flink", "Hello Flink and Kafka" ); DataSet<Tuple2<String, Integer>> counts = text.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() { @Override public void flatMap(String value, Collector<Tuple2<String, Integer>> out) { for (String word : value.split("\\s")) { out.collect(new Tuple2<>(word, 1)); } } }) .groupBy(0) .sum(1); counts.print(); } } ``` 这个程序中,我们首先创建了一个 ExecutionEnvironment 对象,然后使用它来读取一个包含三行字符串的数据集。接下来,我们应用一个 flatMap 函数来将每行字符串划分为单词,并为每个单词创建一个二元组。最后,我们按照单词分组,并计算每个单词出现的次数。 希望这个简单的程序能帮助您入门 Flink
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值