1. 问题
flink 版本一升级,好多api 都红了, 记一下日志。
flink消费kafka有几种指定topic 的方式:
- 指定单独的topic
- 指定topic 列表
- 指定正则表达式
有时这些还不满足功能, 我就想根据自己的想法,从外部存储里获取我想要的topic,这样可以不重启代码的前提下,最大的灵活性消费自己想要的topic。
2.方案
关键在这类,
模仿 getTopicListSubscriber 新建一个方法, list 是从外部存储读取的。
别模仿list这个, 模仿getTopicPatternSubscriber, 防止外部提供的topic 不存在,整体报错。
然后在KafkaSourceBuilder 中重载一个setTopicPattern 方法调用上面方法就可以了。
注意设置分区发现时间:setProperty("partition.discovery.interval.ms","10000")
package org.apache.flink.connector.kafka.source.enumerator.subscriber;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.common.TopicPartition;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
public interface KafkaSubscriber extends Serializable {
Set<TopicPartition> getSubscribedTopicPartitions(AdminClient adminClient);
// ----------------- factory methods --------------
static KafkaSubscriber getTopicListSubscriber(List<String> topics) {
return new TopicListSubscriber(topics);
}
static KafkaSubscriber getTopicPatternSubscriber(Pattern topicPattern) {
return new TopicPatternSubscriber(topicPattern);
}
static KafkaSubscriber getTopicFunctionSubscriber(String param) {
return new TopicFunctionSubscriber(param);
}
static KafkaSubscriber getPartitionSetSubscriber(Set<TopicPartition> partitions) {
return new PartitionSetSubscriber(partitions);
}
}
3. 使用场景
想做一个通用功能,起多个任务,把topic灵活分配给这些任务,把kafka中的数据写进es。
根据kafka 的数据量,起n个任务, 每个任务根据 KafkaSource 构建时指定的参数
从mysql里读取消费哪些topic 。
4. flink sql 动态topic
flink sql 中也想实现上面的效果,关键在这俩类:
KafkaDynamicTableFactory : 主要是验证输入配置项。
KafkaDynamicSource,
关键代码在:380行
final KafkaSourceBuilder<RowData> kafkaSourceBuilder = KafkaSource.builder();
if (topics != null) {
kafkaSourceBuilder.setTopics(topics);
} else {
kafkaSourceBuilder.setTopicPattern(topicPattern);
}
把从sql 传进来的参数, 传递给上面api 的 KafkaSourceBuilder
5. 遗留问题
怎么把获取外部topic 的逻辑传递进去。
写api的方式都好办, sql 开发时怎么把取topic的逻辑传递给 自己定义的KafkaSubscriber 子类。
- param :api 和 sql 都可以传递这个参数
- getTopic: 这是个自定义函数, 利用param 参数获取哪些topic 需要消费的函数。 api的方式可以在使用前把函数内容传给TopicFunctionSubscriber 。 sql方式怎么把这个函数传过去。下面赋值方式只适合api , 不是纯sql方式。
- 试试反射吧, 可以,参数传递类名进去。
- 还是有缺陷, 最好是穿个方法的参数进去。 本打算是对现有topic 分组,然后根据组名字获取需要实收哪些topic
----函数接口
@FunctionalInterface
public interface GetTopic {
List<String> getTopic( String param);
}
-------jar包外实现, api直接写, sql方式的话得单独上传一个jar 包了
mport org.apache.flink.connector.kafka.source.enumerator.subscriber.GetTopic;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class GetTopic1 implements GetTopic , Serializable {
@Override
public List<String> getTopic(String param) {
List<String> list = new ArrayList<>();
list.add("topic");
return list;
}
}
------'topic_function' = 'sql.GetTopic1#222' 类名+ 方法参数
----sql 方式调用
String source="CREATE TABLE source_table (\n" +
" op string, \n" +
" data ROW<empUid string > , \n" +
" occur_time string ,\n" +
" `ts` TIMESTAMP(3) METADATA FROM 'timestamp' , \n " +
" WATERMARK FOR ts AS ts \n " +
")\n" +
"with (\n" +
" 'connector' = 'kafka',\n" +
" 'topic_function' = 'sql.GetTopic1#222',\n" +
" 'properties.bootstrap.servers' = '10.86.25.6:9092',\n" +
" 'properties.group.id' = 'testGroup1111',\n" +
" 'scan.startup.mode' = 'latest-offset',\n" +
" 'scan.topic-partition-discovery.interval'='10s' , \n " +
" 'format' = 'json'\n" +
")";
------api方式调用
KafkaSource<User> source = KafkaSource.<User>builder()
.setBootstrapServers("")
.setTopicFunction("sql.GetTopic1#11")
.setGroupId("my-group")
.setProperty("partition.discovery.interval.ms", "10000")
.setStartingOffsets(OffsetsInitializer.latest())
// .setDeserializer(new MyJsonSchema("source_table"))
.setValueOnlyDeserializer(new MyUserSchema()) // 自定义的 schema 要和source上声名的一致
// .setValueOnlyDeserializer(new SimpleStringSchema())
.build();
public class TopicFunctionSubscriber implements KafkaSubscriber {
private static final long serialVersionUID = -7471048577725467797L;
private static final Logger LOG = LoggerFactory.getLogger(TopicFunctionSubscriber.class);
private GetTopic getTopic;
private String param;
public TopicFunctionSubscriber(String param) {
this.param=param.split("#")[1];
try {
this.getTopic = (GetTopic) Class.forName(param.split("#")[0]).newInstance();
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public Set<TopicPartition> getSubscribedTopicPartitions(AdminClient adminClient) {
LOG.debug("Fetching descriptions for all topics on Kafka cluster");
final Map<String, TopicDescription> allTopicMetadata = getAllTopicMetadata(adminClient);
List<String> list=getTopic.getTopic(this.param );
System.out.println("消费的topic:"+list.toString());
Set<TopicPartition> subscribedTopicPartitions = new HashSet<>();
allTopicMetadata.forEach(
(topicName, topicDescription) -> {
if (list.contains(topicName)) {
for (TopicPartitionInfo partition : topicDescription.partitions()) {
subscribedTopicPartitions.add(
new TopicPartition(
topicDescription.name(), partition.partition()));
}
}
});
return subscribedTopicPartitions;
}
}