Flink 自定义 ClickHouse Table Connector 的简单实现
本次实现基于 Flink 1.18 版本,具体的对象之间的关系可以先参考官网的图:

先拿官网上的 Socket 示例来说一下实现过程:
- 首先编写 SocketDynamicTableFactory实现DynamicTableSourceFactory接口。
- 在 SocketDynamicTableFactory中会返回SocketDynamicTableSource,同时返回实现了ScanTableSource接口。
- 在 SockeDynamicTableSource中返回了SocketSourceFunction,而具体的逻辑就是写在SocketSourceFunction中的。
- SocketSourceFunction需要继承- RichSourceFunction<RowData>类同时实现- ResultTypeQueryable<RowData>接口,在其中的- run方法中实现主要的逻辑,将结果发送至下游。
- 另外关于序列化部分,需要编写 ChangelogCsvFormatFactory实现DeserializationFormatFactory接口,在其中会返回ChangelogCsvFormat。
- 而 ChangelogCsvFormat会实现DecodingFormat<DeserializationSchema<RowData>>接口,并在其中返回ChangelogCsvDeserializer。
- 同时 ChangelogCsvDeserializer又实现了DeserializationSchema<RowData>接口,并在其主要的方法deserialize中实现二进制反序列化的过程,也就是转换为RowData的过程。
官网的示例链接为:https://nightlies.apache.org/flink/flink-docs-release-1.18/docs/dev/table/sourcessinks/
下面我们基于这个原理来实现一个简单的 ClickHouse Table Source,我们不做复杂的字段映射,仅完成指定表数据的读取,简单的将这个过程过一遍。
需要说明的是 Connector 项目和客户端的项目必须拆分为两个项目,因为 Connector 项目需要通过 Flink 的 ClassLoader 进行加载,也就是需要放到 Flink 的 lib 目录下然后再重启才可以,所以如果是一个整体的项目,那么 Flink 将会报错找不到具体的 Connector。
我们首先来创建 Connector 的项目,使用 Maven 来管理,pom.xml 文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>flink-1.18-table-source-example</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>Flink Table Source Example</name>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <flink.version>1.18.0</flink.version>
        <log4j.version>2.17.1</log4j.version>
    </properties>
    <repositories>
        <repository>
            <id>aliyun-maven</id>
            <name>阿里云 central仓和jcenter仓的聚合仓</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>
    <dependencies>
        <!-- Table API 开发 -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-java-bridge</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-common</artifactId>
            <version>1.18.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- ClickHouse Client -->
        <dependency>
            <groupId>com.clickhouse</groupId>
            <!-- or clickhouse-grpc-client if you prefer gRPC -->
            <artifactId>clickhouse-http-client</artifactId>
            <version>0.5.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <version>5.2.3</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass></mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
                        <phase>package</phase> <!-- bind to the packaging phase -->
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
这里引入的 Flink Table API 开发相关的包都标记为 provided ,因为 Flink 本身已经存在这个包了,然后 ClickHouse 相关包需要作为依赖打进去,如果不打进去的话需要把独立的包放到 Flink 的 lib 目录下,因为 Connector 依赖 ClickHouse Client,所以这俩依赖必须同时加载,在提交任务时包含依赖是无效的,如果依赖不存在那么在提交任务时会报 NoClassDefFoundError 的错误。这里为了将依赖打进去,所以下面使用了 Maven 的插件。
我们在 ClickHouse 中有下面这么一张表:
CREATE TABLE user_score
(
    `name` String,
    `score` Int32,
    `user_id` FixedString(16)
)
ENGINE = MergeTree
ORDER BY user_id
我们计划要在 Flink 中定义的 ClickHouse Table 如下:
CREATE TABLE user_score (name STRING, score INT, user_id BYTES)
WITH (
  'connector' = 'clickhouse',
  'hostname' = 'localhost',
  'port' = '8123',
  'username' = 'default',
  'password' = '',
  'database' = 'default',
  'table' = 'user_score',
  'format' = 'clickhouse-row'
);
首先我们创建 ClickHouse 连接所需配置的容器类:
package org.example.source.clickhouse;
import java.io.Serializable;
public class ClickHouseConnection implements Serializable {
    private final String hostname;
    private final int port;
    private final String username;
    private final String password;
    private final String database;
    private final String table;
    public ClickHouseConnection(String hostname, int port, String username, String password, String database, String table) {
        this.hostname = hostname;
        this.port = port;
        this.username = username;
        this.password = password;
        this.database = database;
        this.table = table;
    }
    public String getEndpoint() {
        StringBuilder builder = new StringBuilder();
        builder.append("http://")
                .append(this.hostname)
                .append(":")
                .append(this.port)
                .append("/")
                .append(this.database)
                .append("?user=")
                .append(username);
        if(!"".equals(password)) {
            builder.append("&password=")
                    .append(password);
        }
        return builder.toString();
    }
    public String getTable() {
        return table;
    }
}
这个类必须要实现 Serializable 接口,否则 Flink 会报错无法序列化。
然后我们创建 ClickHouseDynamicTableFactory 内容如下:
package org.example.source.clickhouse;
import org.apache.flink.api.common.serialization.DeserializationSchema;
import org.apache.flink.configuration.ConfigOption;
import org.apache.flink.configuration.ConfigOptions;
import org.apache.flink.configuration.ReadableConfig;
import org.apache.flink.table.connector.format.DecodingFormat;
import org.apache.flink.table.connector.source.DynamicTableSource;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.factories.DeserializationFormatFactory;
import org.apache.flink.table.factories.DynamicTableSourceFactory;
import org.apache.flink.table.factories.FactoryUtil;
import org.apache.flink.table.types.DataType;
import java.util.HashSet;
import java.util.Set;
public class ClickHouseDynamicTableFactory implements DynamicTableSourceFactory {
    public static final ConfigOption<String> HOSTNAME = ConfigOptions.key("hostname")
            .stringType()
            .noDefaultValue();
    public static final ConfigOption<Integer> PORT = ConfigOptions.key("port")
            .intType()
            .defaultValue(8123);
    public static final ConfigOption<String> USERNAME = ConfigOptions.key("username")
            .stringType()
            .defaultValue("default");
    public static final ConfigOption<String> PASSWORD = ConfigOptions.key("password")
            .stringType()
            .defaultValue("");
    public static final ConfigOption<String> DATABASE = ConfigOptions.key("database")
            .stringType()
            .defaultValue("default");
    public static final ConfigOption<String> TABLE = ConfigOptions.key("table")
            .stringType()
            .noDefaultValue();
    @Override
    public DynamicTableSource createDynamicTableSource(Context context) {
        // 内置验证工具
        final FactoryUtil.TableFactoryHelper helper = FactoryUtil.createTableFactoryHelper(this, context);
        final DecodingFormat<DeserializationSchema<RowData>> decodingFormat = helper.discoverDecodingFormat(
                DeserializationFormatFactory.class,
                FactoryUtil.FORMAT);
        helper.validate();
        // 获取已经验证的参数
        final ReadableConfig options = helper.getOptions();
        final String hostname = options.get(HOSTNAME);
        final int port = options.get(PORT);
        final String username = options.get(USERNAME);
        final String password = options.get(PASSWORD);
        final String database = options.get(DATABASE);
        final String table = options.get(TABLE);
        ClickHouseConnection clickHouseConnection = new ClickHouseConnection(hostname, port, username, password, database, table);
        final DataType producedDataType =
                context.getCatalogTable().getResolvedSchema().toPhysicalRowDataType();
        // 返回 DynamicTableSource
        return new ClickHouseDynamicTableSource(clickHouseConnection, decodingFormat, producedDataType);
    }
    @Override
    public String factoryIdentifier() {
        return "clickhouse";
    }
    @Override
    public Set<ConfigOption<?>> requiredOptions() {
        final Set<ConfigOption<?>> options = new HashSet<>();
        options.add(HOSTNAME);
        options.add(TABLE);
        options.add(FactoryUtil.FORMAT); // use pre-defined option for format
        return options;
    }
    @Override
    public Set<ConfigOption<?>> optionalOptions() {
        final Set<ConfigOption<?>> options = new HashSet<>();
        options.add(PORT);
        options.add(USERNAME);
        options.add(PASSWORD);
        options.add(DATABASE);
        options.add(TABLE);
        return options;
    }
}
其中定义了各类参数,也就是 Flink SQL 中传入的参数,主要是进行了初步的参数校验等,其中 factoryIdentifier  返回的就是 connector 中的定义标识。
然后返回了 ClickHouseDynamicTableSource ,其中就包括传入的连接参数等信息,然后我们继续创建 ClickHouseDynamicTableSource 类:
package org.example.source.clickhouse;
import org.apache.flink.api.common.serialization.DeserializationSchema;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.connector.ChangelogMode;
import org.apache.flink.table.connector.format.DecodingFormat;
import org.apache.flink.table.connector.source.DynamicTableSource;
import org.apache.flink.table.connector.source.ScanTableSource;
import org.apache.flink.table.connector.source.SourceFunctionProvider;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.types.DataType;
public class ClickHouseDynamicTableSource implements ScanTableSource {
    private final ClickHouseConnection clickHouseConnection;
    private final DecodingFormat<DeserializationSchema<RowData>> decodingFormat;
    private final DataType producedDataType;
    public ClickHouseDynamicTableSource(
            ClickHouseConnection clickHouseConnection,
            DecodingFormat<DeserializationSchema<RowData>> decodingFormat,
            DataType producedDataType) {
        this.clickHouseConnection = clickHouseConnection;
        this.decodingFormat = decodingFormat;
        this.producedDataType = producedDataType;
    }
    @Override
    public ChangelogMode getChangelogMode() {
        return decodingFormat.getChangelogMode();
    }
    @Override
    public ScanRuntimeProvider getScanRuntimeProvider(ScanContext runtimeProviderContext) {
        // 发送到集群的运行时上下文
        final DeserializationSchema<RowData> deserializer = decodingFormat.createRuntimeDecoder(
                runtimeProviderContext,
                producedDataType);
        DataStructureConverter converter = runtimeProviderContext.createDataStructureConverter(producedDataType);
        // 创建 SourceFunction<RowData>
        final SourceFunction<RowData> sourceFunction = new ClickHouseSourceFunction(
                clickHouseConnection,
                deserializer,
                converter);
        // 第二个参数设置是否是有界流
        return SourceFunctionProvider.of(sourceFunction, true);
    }
    @Override
    public DynamicTableSource copy() {
        // 实现拷贝
        return new ClickHouseDynamicTableSource(clickHouseConnection, decodingFormat, producedDataType);
    }
    @Override
    public String asSummaryString() {
        return "ClickHouse Table Source";
    }
}
然后这里主要设置了一些集群上下文信息,包括反序列化器、数据的转换器等,然后将通过 SourceFunctionProvider 返回 SourceFunction 实例,第二个参数就表示是否是有界流,如果是无界流要设置为 false 。
最后再来创建 ClickHouseSourceFunction :
package org.example.source.clickhouse;
import com.clickhouse.client.ClickHouseClient;
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.ClickHouseResponse;
import com.clickhouse.data.ClickHouseFormat;
import com.clickhouse.data.ClickHouseRecord;
import org.apache.flink.api.common.serialization.DeserializationSchema;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.typeutils.ResultTypeQueryable;
import org.apache.flink.streaming.api.functions.source.RichSourceFunction;
import org.apache.flink.table.connector.source.DynamicTableSource;
import org.apache.flink.table.data.RowData;
import org.apache.flink.types.Row;
import org.apache.flink.types.RowKind;
public class ClickHouseSourceFunction extends RichSourceFunction<RowData> implements ResultTypeQueryable<RowData> {
    private final ClickHouseConnection clickHouseConnection;
    private final DeserializationSchema<RowData> deserializer;
    private final DynamicTableSource.DataStructureConverter converter;
    private volatile boolean isRunning = true;
    public ClickHouseSourceFunction(ClickHouseConnection clickHouseConnection, DeserializationSchema<RowData> deserializer, DynamicTableSource.DataStructureConverter converter) {
        this.clickHouseConnection = clickHouseConnection;
        this.deserializer = deserializer;
        this.converter = converter;
    }
    @Override
    public TypeInformation<RowData> getProducedType() {
        return deserializer.getProducedType();
    }
    @Override
    public void run(SourceContext<RowData> ctx) throws Exception {
        String endpoint = clickHouseConnection.getEndpoint();
        String table = clickHouseConnection.getTable();
        ClickHouseNode clickHouseNode = ClickHouseNode.of(endpoint);
        while (isRunning) {
            try (ClickHouseClient client = ClickHouseClient.newInstance(clickHouseNode.getProtocol())) {
                ClickHouseResponse response = client.read(endpoint)
                        .format(ClickHouseFormat.RowBinaryWithNamesAndTypes)
                        .query("select name, score, user_id from " + table)
                        .executeAndWait();
                for(ClickHouseRecord record : response.records()) {
                    Row row = new Row(RowKind.INSERT, record.size());
                    row.setField(0, record.getValue("name").asString());
                    row.setField(1, record.getValue("score").asInteger());
                    row.setField(2, record.getValue("user_id").asBinary());
                    ctx.collect((RowData) converter.toInternal(row));
                }
                response.close();
                cancel();
            } catch (Throwable t) {
                t.printStackTrace(); // print and continue
            }
            Thread.sleep(1000);
        }
    }
    @Override
    public void cancel() {
        isRunning = false;
    }
}
主要的业务代码在 run 方法中,这里会读取数据并发送至下游,下游以批的方式进行计算,其实原始数据还是流。我们这里查询完一波数据之后直接接将循环退出,下游会将这批数据作为整体进行计算。
由于我们直接就查询出了结果,所以这里直接可以在这里创建 Row 并转换为 RowData 发送到下游,不需要再经过反序列化处理了,因为反序列化只能传入 byte[] 类型的参数,一来一回比较麻烦,这里直接就处理了。但是我们还必须定义一套反序列化的类,因为在 Flink SQL 中 format 参数是必传的,我们可以随便传入一个,比如常用的 csv 也可以,但是这样会造成困扰,所以我们专门定义一个为我们 Connector 使用的 format 即可,仅仅让参数校验通过。好像没找到其他方法可以使得 format 参数不传,这里先暂且这样实现。
另外调用 row.setField 的时候,第一个参数一定是位置参数,不能是字符串,否则会报错:
Accessing a field by name is not supported in position-based field mode.
然后我们来创建反序列化相关的类,首先是 ClickHouseFormatFactory :
package org.example.source.clickhouse;
import org.apache.flink.api.common.serialization.DeserializationSchema;
import org.apache.flink.configuration.ConfigOption;
import org.apache.flink.configuration.ReadableConfig;
import org.apache.flink.table.connector.format.DecodingFormat;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.factories.DeserializationFormatFactory;
import org.apache.flink.table.factories.DynamicTableFactory;
import java.util.Collections;
import java.util.Set;
public class ClickHouseFormatFactory implements DeserializationFormatFactory {
    @Override
    public DecodingFormat<DeserializationSchema<RowData>> createDecodingFormat(DynamicTableFactory.Context context, ReadableConfig formatOptions) {
        // 返回 DecodingFormat<DeserializationSchema<RowData>> 的实现
        return new ClickHouseFormat();
    }
    @Override
    public String factoryIdentifier() {
        return "clickhouse-row";
    }
    @Override
    public Set<ConfigOption<?>> requiredOptions() {
        return Collections.emptySet();
    }
    @Override
    public Set<ConfigOption<?>> optionalOptions() {
        return Collections.emptySet();
    }
}
这里返回的标识就是 clickhouse-row 我们在所有不同的 ClickHouse Connector 中都可以引用这一个,然后返回了 ClickHouseFormat 我们继续来创建:
package org.example.source.clickhouse;
import org.apache.flink.api.common.serialization.DeserializationSchema;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.table.connector.ChangelogMode;
import org.apache.flink.table.connector.format.DecodingFormat;
import org.apache.flink.table.connector.source.DynamicTableSource;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.types.DataType;
import org.apache.flink.types.RowKind;
public class ClickHouseFormat implements DecodingFormat<DeserializationSchema<RowData>> {
    public ClickHouseFormat() {
    }
    @Override
    public DeserializationSchema<RowData> createRuntimeDecoder(DynamicTableSource.Context context, DataType producedDataType) {
        final TypeInformation<RowData> producedTypeInfo = context.createTypeInformation(
                producedDataType);
        final DynamicTableSource.DataStructureConverter converter = context.createDataStructureConverter(producedDataType);
        // 返回 DeserializationSchema<RowData> 的实现
        return new ClickHouseDeserializer(converter, producedTypeInfo);
    }
    @Override
    public ChangelogMode getChangelogMode() {
        return ChangelogMode.newBuilder()
                .addContainedKind(RowKind.INSERT)
                // 批处理不能添加除 INSERT 之外的其他操作
//                .addContainedKind(RowKind.DELETE)
                .build();
    }
}
这里需要注意的一点就是,getChangelogMode 方法中定义了支持的操作,如果是批处理模式那么只支持 INSERT 操作,其余的都不支持,否则将会报错:
Querying a table in batch mode is currently only possible for INSERT-only table sources. But the source for table 'default_catalog.default_database.user_score' produces other changelog messages than just INSERT.
因为批处理就是一批数据,相当于只有插入操作,而流处理可以支持各类操作。
这里返回了 ClickHouseDeserializer 然后我们来实现它:
package org.example.source.clickhouse;
import org.apache.flink.api.common.serialization.DeserializationSchema;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.table.connector.RuntimeConverter;
import org.apache.flink.table.connector.source.DynamicTableSource.DataStructureConverter;
import org.apache.flink.table.data.RowData;
import java.io.IOException;
public class ClickHouseDeserializer implements DeserializationSchema<RowData> {
    private final DataStructureConverter converter;
    private final TypeInformation<RowData> producedTypeInfo;
    public ClickHouseDeserializer(
            DataStructureConverter converter,
            TypeInformation<RowData> producedTypeInfo) {
        this.converter = converter;
        this.producedTypeInfo = producedTypeInfo;
    }
    @Override
    public void open(InitializationContext context) throws Exception {
        converter.open(RuntimeConverter.Context.create(ClickHouseDeserializer.class.getClassLoader()));
    }
    @Override
    public RowData deserialize(byte[] message) throws IOException {
        return null;
    }
    @Override
    public boolean isEndOfStream(RowData nextElement) {
        return false;
    }
    @Override
    public TypeInformation<RowData> getProducedType() {
        return producedTypeInfo;
    }
}
这个写法也非常简单,因为我们在 SourceFunction 中直接进行了数据处理,所以这里 deserialize 直接返回空即可,我们也不会调用它。
以上这样,ClickHouse 的 Connector 就定义好了,然后我们安装到本地 Maven 仓库,以便于开发时可以引用它:
mvn install
安装成功后即可创建一个项目来使用它。
项目的 pom.xml 定义如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>flink-1.18-example</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Flink Table Example</name>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <flink.version>1.18.0</flink.version>
        <log4j.version>2.17.1</log4j.version>
    </properties>
    <repositories>
        <repository>
            <id>aliyun-maven</id>
            <name>阿里云 central仓和jcenter仓的聚合仓</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-java-bridge</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner_2.12</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>${log4j.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>${log4j.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>flink-1.18-table-source-example</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                            </transformers>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                            <shadedArtifactAttached>true</shadedArtifactAttached>
                            <shadedClassifierName>jar-with-dependencies</shadedClassifierName>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
这里我们引入了 Flink 的相关依赖,但是都标记为 provided ,然后我们也引入了我们自己的 Connector 但是也标记为 provided ,因为我们之后运行时要放到 Flink 的 lib 下,所以不需要带上。另外我们也不需要引入 ClickHouse 的依赖,因为我们的 Connector 中已经包含了。
这里我们使用了 maven-shade-plugin 进行打包,主要是为了合并 resources 下面的 services 中的内容,这个等下我们会说,然后我们创建一个测试代码,内容如下:
package org.example;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
public class ClickHouseTableExample {
    public static void main(String[] args) {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setRuntimeMode(RuntimeExecutionMode.BATCH);
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
        tableEnv.executeSql("CREATE TABLE user_score (\n" +
                "    `name` STRING,\n" +
                "    `score` INTEGER,\n" +
                "    `user_id` BYTES\n" +
                ") WITH (\n" +
                "    'connector' = 'clickhouse',\n" +
                "    'hostname' = 'localhost',\n" +
                "    'port' = '8123',\n" +
                "    'username' = 'default',\n" +
                "    'password' = 'wSqDxDAt',\n" +
                "    'database' = 'default',\n" +
                "    'table' = 'user_score',\n" +
                "    'format' = 'clickhouse-row'\n" +
                ");\n").print();
        tableEnv.executeSql("SELECT sum(score), name from user_score group by name;").print();
    }
}
这是一段很简单的代码,主要就是做了一点统计,其中的模式设置为了 BATCH ,其实如果 Source Connector 定义为批,那么运行模式既可以设置为流也可以设置为批,如果是设置为流在聚合时,所有的计算过程都会更新出来,而如果设置为批,则只有一个最终的结果,结果是在 SourceFunction 退出后才会最终输出。如果 SourceFunction 是无限循环,那么永远也得不到最终的结果,但是流运行模式可以不断地得到当前的结果。如果 Source Connector 定义为流,那么当前的运行模式只能设置为流,所有的聚合结果都会根据流的到来实时输出。
现在程序还无法运行,这时候会报错:
Could not find any factory for identifier 'clickhouse' that implements 'org.apache.flink.table.factories.Factory' in the classpath.
这是因为具体的 DynamicTableSource 和 DynamicTableSink 是通过 Java 的 SPI 提供发现的,简单来说定义方法如下,我们在项目的 resources 目录下,对于 Maven 详细的目录就是 src/main/resources 下创建子目录 META-INF/services ,然后创建文件 org.apache.flink.table.factories.Factory ,内容如下:
org.example.source.clickhouse.ClickHouseDynamicTableFactory
org.example.source.clickhouse.ClickHouseFormatFactory
这样程序在运行时就会找到对应 Class 的位置从而加载它。
在本地运行时,需要将相关的 provided 注释掉并运行即可,然后如果是打包提交集群时,默认如果其他依赖包也有 SPI 相关的文件,那么会把当前项目的覆盖掉,所以上面在 Maven 中配置了下面的内容:
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
这个表示将当前项目的 ServicesResource 和打包中已经存在的进行合并,这样我们上面写的文件就会带到发布的包中了。
最后上面 pom.xml 中的 filters 也必须配置,否则一些多余的文件不过滤掉在运行时会报错:
Invalid signature file digest for Manifest main
表示签名无效,去掉后才可以正常运行。
这样我们先将 Connector 放到 Flink 集群中所有的 lib 目录下,然后重启 Flink 集群,最后再将当前项目通过 mvn package 打成的包提交到集群运行就可以了。
Reference:
- https://developer.aliyun.com/article/1045096
- https://www.modb.pro/db/634537
- https://juejin.cn/post/7212901628769189947
Flink 自定义 ClickHouse Table Connector 的简单实现的更多相关文章
- 自定义 Azure Table storage 查询过滤条件
		本文是在Azure Table storage 基本用法一文的基础上,介绍如何自定义 Azure Table storage 的查询过滤条件.如果您还不太清楚 Azure Table storage ... 
- Flink自定义Sink
		Flink自定义Sink Flink 自定义Sink,把socket数据流数据转换成对象写入到mysql存储. #创建Student类 public class Student { private i ... 
- IOS自定义日历控件的简单实现(附思想及过程)
		因为程序要求要插入一个日历控件,该空间的要求是从当天开始及以后的六个月内的日历,上网查资料基本上都说只要获取两个条件(当月第一天周几和本月一共有多少天)就可以实现一个简单的日历,剩下的靠自己的简单逻辑 ... 
- Flink系统之Table API 和 SQL
		Flink提供了像表一样处理的API和像执行SQL语句一样把结果集进行执行.这样很方便的让大家进行数据处理了.比如执行一些查询,在无界数据和批处理的任务上,然后将这些按一定的格式进行输出,很方便的让大 ... 
- Java利用自定义注解、反射实现简单BaseDao
		在常见的ORM框架中,大都提供了使用注解方式来实现entity与数据库的映射,这里简单地使用自定义注解与反射来生成可执行的sql语句. 这是整体的目录结构,本来是为复习注解建立的项目^.^ 好的,首先 ... 
- Flink实战(六) - Table API & SQL编程
		1 意义 1.1 分层的 APIs & 抽象层次 Flink提供三层API. 每个API在简洁性和表达性之间提供不同的权衡,并针对不同的用例. 而且Flink提供不同级别的抽象来开发流/批处理 ... 
- Flink源码分析 - 剖析一个简单的Flink程序
		本篇文章首发于头条号Flink程序是如何执行的?通过源码来剖析一个简单的Flink程序,欢迎关注头条号和微信公众号"大数据技术和人工智能"(微信搜索bigdata_ai_tech) ... 
- Flink 自定义source和sink,获取kafka的key,输出指定key
		--------20190905更新------- 沙雕了,可以用 JSONKeyValueDeserializationSchema,接收ObjectNode的数据,如果有key,会放在Objec ... 
- flink 自定义触发器 定时或达到数量触发
		flink 触发器 触发器确定窗口(由窗口分配程序形成)何时准备由窗口函数处理.每个WindowAssigner都带有一个默认触发器. 如果默认触发器不适合需求,我们就需要自定义触发器. 主要方法 触 ... 
- 【Flink】使用之前,先简单了解一下Flink吧!
		目录 Flink简单介绍 概述 无边界数据流和有边界数据流 技术栈核心组成 架构体系 重要角色 Flink与Spark架构概念转换 Flink简单介绍 概述 在使用Flink之前,我们需要大概知 ... 
随机推荐
- 开源好用的所见即所得(WYSIWYG)编辑器:Editor.js
			@ 目录 特点 基于区块 干净的数据 界面与交互 插件 标题和文本 图片 列表 Todo 表格 使用 安装 创建编辑器实例 配置工具 本地化 自定义样式 今天介绍一个开源好用的Web所见即所得(WYS ... 
- chrome 快速执行 snippets 1. F12 2. Ctrl+Shift+P 3. show snippets 4. 上下选择 5. Ctrl + Enter
			chrome 快速执行 snippets F12 Ctrl+Shift+P show snippets 上下选择 Ctrl + Enter 
- stm32芯片的SPI接口调试总结之轮询模式
			一 概念 1 组成 SPI系统可直接与各个厂家生产的多种标准外围器件接口,它只需4条线:串行时钟线(SCK).主机输入/从机输出数据线(MISO).主机输出/从机输入数据线(MOSI)和低电平有效的从 ... 
- ARM的无线ble IP Cordio-B50 stack and profiles简析
			一 简介 人家英文写的很清楚,我就不蹩脚额翻译了. Cordio-B50 stack is designed specifically for Bluetooth low energy single- ... 
- [置顶]
        java动态控制线程的启动和停止
			最近项目有这样的需求:原来系统有个计算的功能,但该功能执行时间会很长(大概需要几个小时才能完成),如果执行过程中出现了错误的话,也只能默默的等待错误执行完成才行,无法做到动态的对该功能进行停止. 我了 ... 
- C++类的访问权限
			首先明确一个类的用户有三种: 一类用户:类的成员和友元 二类用户:子类的成员及子类的友元 三类用户:外部的用户代码(通过类的对象或指针) 一个类有三种成员 private:只有一类用户可以访问priv ... 
- 29_SDL多线程与锁机制
			目录 一.简介 二.代码实现: 2.1.声明 2.2.创建锁.消费者 2.3.销毁 2.4.实现生产者逻辑 2.5.实现销毁者逻辑 2.6.创建生产者 三.分装SDL锁机制 condmutex.h c ... 
- 一种OSD 简单实现 (文字反色---opencv、字体切换---freetype2(中文、空格))
			PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明 本文作为本人csdn blog的主站的备份.(Bl ... 
- Android JNI静态和动态注册 、Java Reflect(C或C++层反射和JAVA层反射)、Java 可变参数(JNI实现)
			PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明 本文作为本人csdn blog的主站的备份.(Bl ... 
- js实现展开多级数组
			1.递归 function steamrollArray(arr) { let res = [] for (const a of arr) { if(a instanceof Array){ res ... 
