Flink 提供了 Apache Kafka 连接器使用精确一次(Exactly-once)的语义在 Kafka topic 中读取和写入数据。
实战源码地址,一键下载可用:https://gitee.com/shawsongyue/aurora.git
模块:aurora_flink_connector_kafka
主类:KafkaSourceStreamingJob
org.apache.flink
flink-connector-kafka
3.0.2-1.18
若是不引入该依赖,项目启动直接报错:Exception in thread “main” java.lang.NoClassDefFoundError: org/apache/flink/connector/base/source/reader/RecordEmitter
org.apache.flink
flink-connector-base
1.18.0
Kafka Source 提供了构建类来创建 KafkaSource的实例。以下代码片段展示了如何构建 KafkaSource
来消费 “input-topic” 最早位点的数据, 使用消费组 “my-group”,并且将 Kafka 消息体反序列化为字符串 。
KafkaSource source = KafkaSource.builder()
.setBootstrapServers(brokers)
.setTopics("input-topic")
.setGroupId("my-group")
.setStartingOffsets(OffsetsInitializer.earliest())
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
#以下属性在构建 KafkaSource 时是必须指定的:
1.Bootstrap server,通过 setBootstrapServers(String) 方法配置
2.消费者组 ID,通过 setGroupId(String) 配置
3.要订阅的 Topic / Partition
4.用于解析 Kafka 消息的反序列化器(Deserializer)
Kafka Source 提供了 3 种 Topic / Partition 的订阅方式
可以订阅 Topic 列表中所有 Partition 的消息
KafkaSource.builder().setTopics("topic-a", "topic-b");
订阅与正则表达式所匹配的 Topic 下的所有 Partition
KafkaSource.builder().setTopicPattern("topic.*");
订阅指定的 Partition
final HashSet partitionSet = new HashSet<>(Arrays.asList(
new TopicPartition("topic-a", 0), // Partition 0 of topic "topic-a"
new TopicPartition("topic-b", 5))); // Partition 5 of topic "topic-b"
KafkaSource.builder().setPartitions(partitionSet);
1.代码中需要提供一个反序列化器(Deserializer)来对 Kafka 的消息进行解析。 反序列化器通过 setDeserializer(KafkaRecordDeserializationSchema) 来指定,其中 KafkaRecordDeserializationSchema 定义了如何解析 Kafka 的 ConsumerRecord。
2.如果只需要 Kafka 消息中的消息体(value)部分的数据,可以使用 KafkaSource 构建类中的 setValueOnlyDeserializer(DeserializationSchema) 方法,其中 DeserializationSchema 定义了如何解析 Kafka 消息体中的二进制数据。
3.也可使用 Kafka 提供的解析器 来解析 Kafka 消息体。例如使用 StringDeserializer 来将 Kafka 消息体解析成字符串
import org.apache.kafka.common.serialization.StringDeserializer;
KafkaSource.builder()
.setDeserializer(KafkaRecordDeserializationSchema.valueOnly(StringDeserializer.class));
Kafka source 能够通过位点初始化器(OffsetsInitializer
)来指定从不同的偏移量开始消费
KafkaSource.builder()
// 从消费组提交的位点开始消费,不指定位点重置策略
.setStartingOffsets(OffsetsInitializer.committedOffsets())
// 从消费组提交的位点开始消费,如果提交位点不存在,使用最早位点
.setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.EARLIEST))
// 从时间戳大于等于指定时间戳(毫秒)的数据开始消费
.setStartingOffsets(OffsetsInitializer.timestamp(1657256176000L))
// 从最早位点开始消费,默认使用
.setStartingOffsets(OffsetsInitializer.earliest())
// 从最末尾位点开始消费
.setStartingOffsets(OffsetsInitializer.latest());
流模式下运行通过使用 setUnbounded(OffsetsInitializer)`也可以指定停止消费位点,当所有分区达到其指定的停止偏移量时,Kafka Source 会退出运行。
可以使用 setBounded(OffsetsInitializer) 指定停止偏移量使 Kafka Source 以批处理模式运行。当所有分区都达到其停止偏移量时,Kafka Source 会退出运行。
可以使用 setProperties(Properties) 和 setProperty(String, String) 为 Kafka Source 和 Kafka Consumer 设置任意属性
指定用于 Kafka Consumer 的客户端 ID 前缀
定义 Kafka Source 检查新分区的时间间隔
指定是否在 Flink 中注册 Kafka Consumer 的指标
指定是否在进行 checkpoint 时将消费位点提交至 Kafka broker
始终设置为 ByteArrayDeserializer
始终设置为 ByteArrayDeserializer
被 OffsetsInitializer#getAutoOffsetResetStrategy() 覆盖
会在批模式下被覆盖为 -1
为了在不重启 Flink 作业的情况下处理 Topic 扩容或新建 Topic 等场景,将 Kafka Source 配置为在提供的 Topic / Partition 订阅模式下定期检查新分区。要启用动态分区检查,partition.discovery.interval.ms设置为非负值:
KafkaSource.builder()
.setProperty("partition.discovery.interval.ms", "10000"); // 每 10 秒检查一次新分区
默认情况下,Kafka Source 使用 Kafka 消息中的时间戳作为事件时间。您可以定义自己的水印策略(Watermark Strategy) 以从消息中提取事件时间,并向下游发送水印
env.fromSource(kafkaSource, new CustomWatermarkStrategy(), "Kafka Source With Custom Watermark Strategy");
Kafka source 在 checkpoint 完成时提交当前的消费位点 ,以保证 Flink 的 checkpoint 状态和 Kafka broker 上的提交位点一致。如果未开启 checkpoint,Kafka source 依赖于 Kafka consumer 内部的位点定时自动提交逻辑,自动提交功能由 enable.auto.commit
和 auto.commit.interval.ms
两个 Kafka consumer 配置项进行配置。
注意:Kafka source 不依赖于 broker 上提交的位点来恢复失败的作业。提交位点只是为了上报 Kafka consumer 和消费组的消费进度,以在 broker 端进行监控。
Kafka consumer 的所有指标都注册在指标组 KafkaSourceReader.KafkaConsumer
下。例如 Kafka consumer 的指标 records-consumed-total
将在该 Flink 指标中汇报: .operator.KafkaSourceReader.KafkaConsumer.records-consumed-total
。
您可以使用配置项 register.consumer.metrics
配置是否注册 Kafka consumer 的指标 。默认此选项设置为 true。
关于 Kafka consumer 的指标,您可以参考 Apache Kafka 文档 了解更多详细信息。
1.要启用加密和认证相关的安全配置,只需将安全配置作为其他属性配置在 Kafka source 上即可。下面的代码片段展示了如何配置 Kafka source 以使用 PLAIN 作为 SASL 机制并提供 JAAS 配置
KafkaSource.builder()
.setProperty("security.protocol", "SASL_PLAINTEXT")
.setProperty("sasl.mechanism", "PLAIN")
.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"username\" password=\"password\";");
2.使用 SASL_SSL 作为安全协议并使用 SCRAM-SHA-256 作为 SASL 机制 。 如果在作业 JAR 中 Kafka 客户端依赖的类路径被重置了(relocate class),登录模块(login module)的类路径可能会不同,因此请根据登录模块在 JAR 中实际的类路径来改写以上配置
KafkaSource.builder()
.setProperty("security.protocol", "SASL_SSL")
// SSL 配置
// 配置服务端提供的 truststore (CA 证书) 的路径
.setProperty("ssl.truststore.location", "/path/to/kafka.client.truststore.jks")
.setProperty("ssl.truststore.password", "test1234")
// 如果要求客户端认证,则需要配置 keystore (私钥) 的路径
.setProperty("ssl.keystore.location", "/path/to/kafka.client.keystore.jks")
.setProperty("ssl.keystore.password", "test1234")
// SASL 配置
// 将 SASL 机制配置为 as SCRAM-SHA-256
.setProperty("sasl.mechanism", "SCRAM-SHA-256")
// 配置 JAAS
.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.scram.ScramLoginModule required username=\"username\" password=\"password\";");
Kafka source 的数据源分片(source split)表示 Kafka topic 中的一个 partition。Kafka 的数据源分片包括:
Kafka source 分片的状态同时存储该 partition 的当前消费位点,该分片状态将会在 Kafka 源读取器(source reader)进行快照(snapshot) 时将当前消费位点保存为起始消费位点以将分片状态转换成不可变更的分片。
可查看 KafkaPartitionSplit
和 KafkaPartitionSplitState
类来了解细节。
Kafka source 的分片枚举器负责检查在当前的 topic / partition 订阅模式下的新分片(partition),并将分片轮流均匀地分配给源读取器(source reader)。 注意 Kafka source 的分片枚举器会将分片主动推送给源读取器,因此它无需处理来自源读取器的分片请求。
Kafka source 的源读取器扩展了 SourceReaderBase
,并使用单线程复用(single thread multiplex)的线程模型,使用一个由分片读取器 (split reader)驱动的 KafkaConsumer
来处理多个分片(partition)。消息会在从 Kafka 拉取下来后在分片读取器中立刻被解析。分片的状态 即当前的消息消费进度会在 KafkaRecordEmitter
中更新,同时会在数据发送至下游时指定事件时间。
4.0.0
com.xsy
aurora_flink_connector_kafka
1.0-SNAPSHOT
11
3.8.1
UTF-8
UTF-8
1.2.75
2.17.1
1.18.0
2.11
com.alibaba
fastjson
${fastjson.version}
org.apache.flink
flink-java
${flink.version}
org.apache.flink
flink-streaming-scala_2.12
${flink.version}
org.apache.flink
flink-clients
${flink.version}
org.apache.logging.log4j
log4j-slf4j-impl
${log4j.version}
org.apache.logging.log4j
log4j-api
${log4j.version}
org.apache.logging.log4j
log4j-core
${log4j.version}
org.apache.flink
flink-connector-kafka
3.0.2-1.18
org.apache.flink
flink-connector-base
1.18.0
${project.name}
src/main/resources
src/main/java
**/*.xml
org.apache.maven.plugins
maven-shade-plugin
3.1.1
package
shade
org.apache.flink:force-shading
org.google.code.flindbugs:jar305
org.slf4j:*
org.apache.logging.log4j:*
*:*
META-INF/*.SF
META-INF/*.DSA
META-INF/*.RSA
org.aurora.KafkaStreamingJob
org.springframework.boot
spring-boot-maven-plugin
${spring.boot.version}
true
${project.build.finalName}
repackage
maven-compiler-plugin
${maven.plugin.version}
${java.version}
UTF-8
-parameters
aliyun-repos
https://maven.aliyun.com/nexus/content/groups/public/
false
aliyun-plugin
https://maven.aliyun.com/nexus/content/groups/public/
false
#kafka集群地址
kafka.bootstrapServers=localhost:9092
#kafka消费者组
kafka.group=aurora_group
rootLogger.level=INFO
rootLogger.appenderRef.console.ref=ConsoleAppender
appender.console.name=ConsoleAppender
appender.console.type=CONSOLE
appender.console.layout.type=PatternLayout
appender.console.layout.pattern=%d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n
log.file=D:\\tmprootLogger.level=INFO
rootLogger.appenderRef.console.ref=ConsoleAppender
appender.console.name=ConsoleAppender
appender.console.type=CONSOLE
appender.console.layout.type=PatternLayout
appender.console.layout.pattern=%d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n
log.file=D:\\tmp
package com.aurora;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.KafkaSourceBuilder;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.connector.kafka.source.reader.deserializer.KafkaRecordDeserializationSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.OffsetResetStrategy;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.regex.Pattern;
/**
* @author 浅夏的猫
* @description kafka 连接器使用demo作业
* @datetime 22:21 2024/2/1
*/
public class KafkaSourceStreamingJob{
private static final Logger logger = LoggerFactory.getLogger(KafkaStreamingJob.class);
public static void main(String[] args) throws Exception {
//===============1.获取参数==============================
//定义文件路径
String propertiesFilePath = "E:\\project\\aurora_dev\\aurora_flink_connector_kafka\\src\\main\\resources\\application.properties";
//方式一:直接使用内置工具类
ParameterTool paramsMap = ParameterTool.fromPropertiesFile(propertiesFilePath);
//================2.初始化kafka参数==============================
String bootstrapServers = paramsMap.get("kafka.bootstrapServers");
String topic = paramsMap.get("kafka.topic");
String group = paramsMap.get("kafka.group");
//=================3.创建kafka数据源=============================
KafkaSourceBuilder kafkaSourceBuilder = KafkaSource.builder();
//(1)设置kafka地址
kafkaSourceBuilder.setBootstrapServers(bootstrapServers);
//(2)设置消费这组id
kafkaSourceBuilder.setGroupId(group);
//(3)设置主题,支持多种主题组合
setTopic(kafkaSourceBuilder);
//(4)设置消费模式,支持多种消费模式
setStartingOffsets(kafkaSourceBuilder);
//(5)设置反序列化器
setDeserializer(kafkaSourceBuilder);
//(6)构建全部参数
KafkaSource kafkaSource = kafkaSourceBuilder.build();
//(7)动态检查新分区, 10 秒检查一次新分区
kafkaSourceBuilder.setProperty("partition.discovery.interval.ms", "10000");
//=================4.创建Flink运行环境=================
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource dataStreamSource = env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "Kafka Source");
//=================5.数据简单处理======================
dataStreamSource.flatMap(new FlatMapFunction() {
@Override
public void flatMap(String record, Collector collector) throws Exception {
logger.info("正在处理kafka数据:{}", record);
}
});
//=================6.启动服务=========================================
env.execute();
}
/**
*
* @description 主题模式设置
* 1.设置单个主题
* 2.设置多个主题
* 3.设置主题list
* 4.设置正则表达式匹配主题
* 5.订阅指定分区Partition
*
* @author 浅夏的猫
* @datetime 21:18 2024/2/5
* @param kafkaSourceBuilder
*/
private static void setTopic(KafkaSourceBuilder kafkaSourceBuilder) {
//组合1:设置单个主题
kafkaSourceBuilder.setTopics("topic_a");
//组合2:设置多个主题
// kafkaSourceBuilder.setTopics("topic_a", "topic_b");
//组合3:设置主题list
// kafkaSourceBuilder.setTopics(Arrays.asList("topic_a", "topic_b"));
//组合4:设置正则表达式匹配主题
// kafkaSourceBuilder.setTopicPattern(Pattern.compile("topic_a.*"));
//组合5:订阅指定分区Partition,指定消费主题的哪一个分区,也支持消费多个主题的多个分区
// final HashSet partitionSet = new HashSet<>(Arrays.asList(new TopicPartition("topic_a", 0), new TopicPartition("topic_b", 4)));
// kafkaSourceBuilder.setPartitions(partitionSet);
}
/**
* @description 消费模式
* 1.从消费组提交的位点开始消费,不指定位点重置策略
* 2.从消费组提交的位点开始消费,如果提交位点不存在,使用最早位点
* 3.从时间戳大于等于指定时间戳(毫秒)的数据开始消费
* 4.从最早位点开始消费
* 5.从最末尾位点开始消费,即从注册时刻开始消费
*
* @author 浅夏的猫
* @datetime 21:27 2024/2/5
* @param kafkaSourceBuilder
*/
private static void setStartingOffsets(KafkaSourceBuilder kafkaSourceBuilder){
//模式1: 从消费组提交的位点开始消费,不指定位点重置策略,这种策略会报异常,没有设置快照或设置自动提交offset:Caused by: org.apache.kafka.clients.consumer.NoOffsetForPartitionException: Undefined offset with no reset policy for partitions: [topic_a-3]
// kafkaSourceBuilder.setStartingOffsets(OffsetsInitializer.committedOffsets());
//模式2:从消费组提交的位点开始消费,如果提交位点不存在,使用最早位点
// kafkaSourceBuilder.setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.EARLIEST));
//模式3:从时间戳大于等于指定时间戳(毫秒)的数据开始消费
kafkaSourceBuilder.setStartingOffsets(OffsetsInitializer.timestamp(1657256176000L));
//模式4:从最早位点开始消费
// kafkaSourceBuilder.setStartingOffsets(OffsetsInitializer.earliest());
//模式5:从最末尾位点开始消费,即从注册时刻开始消费
// kafkaSourceBuilder.setStartingOffsets(OffsetsInitializer.latest());
}
/**
* @description 设置反序列器,支持多种反序列号方式
* 1.自定义如何解析kafka数据
* 2.使用Kafka 提供的解析器处理
* 3.只设置kafka的value反序列化
*
* @author 浅夏的猫
* @datetime 21:35 2024/2/5
* @param kafkaSourceBuilder
*/
private static void setDeserializer(KafkaSourceBuilder kafkaSourceBuilder){
//1.自定义如何解析kafka数据
// KafkaRecordDeserializationSchema kafkaRecordDeserializationSchema = new KafkaRecordDeserializationSchema<>() {
// @Override
// public TypeInformation getProducedType() {
// return TypeInformation.of(String.class);
// }
//
// @Override
// public void deserialize(ConsumerRecord consumerRecord, Collector collector) throws IOException {
// //自定义解析数据
// byte[] valueByte = consumerRecord.value();
// String value = new String(valueByte);
// //下发消息
// collector.collect(value);
// }
// };
// kafkaSourceBuilder.setDeserializer(kafkaRecordDeserializationSchema);
//2.使用Kafka 提供的解析器处理
// kafkaSourceBuilder.setDeserializer(KafkaRecordDeserializationSchema.valueOnly(StringDeserializer.class));
//3.只设置kafka的value反序列化
kafkaSourceBuilder.setValueOnlyDeserializer(new SimpleStringSchema());
}
}
1.kafka搭建参考我的另外一篇博客:https://blog.csdn.net/weixin_40736233/article/details/136002105
2.使用kafka创建两个主题:topic_a,topic_b,1个副本5个分区(windows环境脚本是.bat,linux环境是.sh)
#创建主题
kafka-topics.bat --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 5 --topic topic_a
kafka-topics.bat --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 5 --topic topic_b
#查询主题
kafka-topics.bat --bootstrap-server localhost:9092 --list
启动Flink程序消费,并且通过kafka命令启动一个生产者。模拟数据生成
#启动生产者
kafka-console-producer.bat --broker-list localhost:9092 --topic topic_a
#放开注释组合二的代码
启动Flink程序消费,并且通过kafka命令启动一个生产者。模拟数据生成
#启动两个生产者,分别生产topic_a,topic_b数据
kafka-console-producer.bat --broker-list localhost:9092 --topic topic_a
kafka-console-producer.bat --broker-list localhost:9092 --topic topic_b
只订阅topic_a下面的全部分区,不订阅topic_b,程序只会消费topic_a,不会消费topic_b
#查看消息落在哪个分区,落在0分区则消费,其他分区没有数
./kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic topic_a