1、kafka生产批(优化)
producer会尝试缓冲record,实现批量发送,通过以下配置控制发送时机。
- batch.size:当多条消息发送到一个分区时,生产者会进行批量发送,这个参数指定了批量消息的大小上限(以字节为单位)。
- linger.ms:这个参数指定生产者在发送批量消息前等待的时间,当设置此参数后,即便没有达到批量消息的指定大小,到达时间后生产者也会发送批量消息到broker。
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,2048); // 批量发送数据上限为 2kb
properties.put(ProducerConfig.LINGER_MS_CONFIG,1000); // 批量发送最大等待时间 1s
2、消费者组
消费组实际上是用来管理组织消费者,原则:组外广播,组内负载均衡
组外广播:消费者组不同
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g2");
组内负载均衡:消费者组相同
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
结论:
消费组组内负载均衡是针对于分区,组内的消费者在负载均衡时,一个消费者负责Topic一个或者多个分区内数据处理
3、生产者幂等性(idempotence)
幂等:多次操作的结果和一次操作的结果相同,就称为幂等性操作。读操作一定是幂等性操作,写操作一定不是幂等性操作。
Kafka的producer和broker之间默认有应答(ack)机制,当kafka的producer发送数据给broker,如果在规定的时间没有收到应答,生产者会自动重发数据,这样的操作可能造成重复数据(at least onnce语义)的产生。
kafka支持三种语义操作:at least once、at most once、at exactly once。
幂等性使用方法:开启kafka生产者幂等性支持
properties.put(ProducerConfig.ACKS_CONFIG,"all"); // ack的机制
// 0 无需应答 1 只写入Leader分区后立即ack -1(all) 写入到leader和follower分区后再进行应答
properties.put(ProducerConfig.RETRIES_CONFIG,3); // 重试发送record的次数,默认是int的最大值
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,3000); // 请求的超时时间 单位毫秒
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true); // 开启幂等性支持
4、kafka事务
kafka事务指的是在一个事务内多个Record发送或者处理是一个原子操作,不可分割,同时成功,或者同时失败。
kafka事务的隔离级别
:读未提交(默认:read_uncommitted
)、读提交(read_commmitted
)
4.1 仅生产者事务
Kafka生产者在一个事务内生产的Record是一个不可分割的整体,要么同时写入Kafka集群,或者某个出错,回滚撤销所有的写操作。
开启生产者事务的前提:
ENABLE_IDEMPOTENCE_CONFIG=true
每一个生产者,需要唯一的事务编号:TransactionID
事务的超时时间(可选):TRANSACTION_TIMEOUT_CONFIG
public class KafkaProducerDemo {
public static void main(String[] args) {
//1. 创建配置对象 指定Producer的信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.ACKS_CONFIG,"all"); // ack的机制
properties.put(ProducerConfig.RETRIES_CONFIG,3); // 重试发送record的次数
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,3000); // 请求的超时时间 单位毫秒
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true); // 开启幂等性支持
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString()); // 生产者事务的ID
properties.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG,1000); // 事务的超时时间
//2. 创建Producer对象
KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String>(properties);
// 初始化kafka的事务
producer.initTransactions();
// 开启Kafka的事务
producer.beginTransaction();
//3. 发布消息
try {
for (int i = 80; i < 100; i++) {
ProducerRecord<Integer, String> record = new ProducerRecord<Integer, String>("t1",i,"Hello World"+i);
producer.send(record);
}
// 正常操作 提交Kafka事务
producer.commitTransaction();
} catch (ProducerFencedException e) {
e.printStackTrace();
// 异常操作 回滚kafka事务
producer.abortTransaction();
}
//4. 提交
producer.flush();
producer.close();
}
}
4.2 Consume-Transfer-Produce(消费和生产并存事务)
特点:消费kafka的A主题进行业务操作,将操作的结果写入到B主题中。如果开启consume-transfer-produce
事务,读A和写B在一个事务环境中,不可分割,要么同时成功,要么同时失败。
/**
* 消费生产并存事务
*/
public class ConsumeTransferProduce {
public static void main(String[] args) {
KafkaProducer<Integer, String> producer = initProducer();
KafkaConsumer<Integer, String> consumer = initConsumer();
//1. 消费者订阅
consumer.subscribe(Arrays.asList("t5"));
//2. 初始化kafka的事务
producer.initTransactions();
while (true) {
//3. 开启kafka事务环境
producer.beginTransaction();
try {
ConsumerRecords<Integer, String> records = consumer.poll(Duration.ofSeconds(5));
Map<TopicPartition, OffsetAndMetadata> commitOffset = new HashMap<>();
for (ConsumerRecord<Integer, String> record : records) {
// 业务操作
System.out.println(record.key() + " ---> " + record.value());
// 人为模拟业务错误,测试事务
/*
if (record.value().equals("AA")){
int m = 1/0;
}
*/
// 手动维护消费的偏移量信息
commitOffset.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1));
// 消费啥内容 发送啥内容
producer.send(new ProducerRecord<Integer, String>("t6", record.key(), record.value()));
}
//4. 将事务内的消费的偏移量提交
producer.sendOffsetsToTransaction(commitOffset, "g1");
//5. 提交kafka的事务
producer.commitTransaction();
} catch (ProducerFencedException e) {
e.printStackTrace();
//6. 回滚事务
producer.abortTransaction();
}
}
}
/**
* 初始化 生产者实例
*/
public static KafkaProducer<Integer, String> initProducer() {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
properties.put(ProducerConfig.ACKS_CONFIG, "all");
properties.put(ProducerConfig.RETRIES_CONFIG, 3);
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 2000);
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString()); // 事务的ID
properties.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 1000);
return new KafkaProducer<Integer, String>(properties);
}
/**
* 初始化 消费者实例
*/
public static KafkaConsumer<Integer, String> initConsumer() {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 手动提交消费的offset
properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); // 读已提交 不会读到别的事务未提交的记录
return new KafkaConsumer<Integer, String>(properties);
}
}
5、kafka Streaming
Kafka Streams是一个用于构建应用程序和微服务的客户端库,其中的输入和输出数据存储在Kafka集群中。它结合了在客户端编写和部署标准Java和Scala应用程序的简单性,以及Kafka服务器端集群技术的优点,下面介绍一下kafka stram的概念。
Topology(拓扑):表示一个流计算任务,等价于MapReduce中的job。不同的是MapReduce的job作业最终会停止,但是Topology会一直运行在内存中,除非人工关闭该Topology
Stream:它代表了一个无限的,不断更新的Record数据集。流是有序,可重放和容错的不可变数据记录序列,其中数据记录被定义为键值对
States:用以持久化存放流计算状态结果,可以用以容错和故障恢复
Time:
- Event time(事件时间)
- Processing time(处理时间)
- Ingestion time(摄入时间)
注意:所谓的流处理就是通过Topology编织程序对Stream中Record元素的处理的逻辑/流程。
5.1 kafka stram架构
Kafka Streams通过构建Kafka生产者和消费者库并利用Kafka的本机功能来提供数据并行性,分布式协调,容错和操作简便性,从而简化了应用程序开发。
Kafka的消息分区用于存储和传递消息, Kafka Streams对数据进行分区以进行处理。 Kafka Streams使用Partition和Task的概念作为基于Kafka Topic分区的并行模型的逻辑单元。在并行化的背景下,Kafka Streams和Kafka之间有着密切的联系:
- 每个stream分区都是完全有序的数据记录序列,并映射到Kafka Topic分区。
- Stream中的数据记录映射到该Topic的Kafka消息。
- 数据记录的key决定了Kafka和Kafka Streams中数据的分区,即数据如何路由到Topic的特定分区。
任务的并行度
Kafka Streams基于应用程序的输入流分区创建固定数量的Task,每个任务(Task)分配来自输入流的分区列表(即Kafka主题)。分区到任务的分配永远不会改变,因此每个任务都是应用程序的固定平行单元。然后,任务可以根据分配的分区实例化自己的处理器拓扑; 它们还为每个分配的分区维护一个缓冲区,并从这些记录缓冲区一次一个地处理消息。因此,流任务可以独立并行地处理,无需人工干预。
用户可以启动多个KafkaStream实例,这样等价启动了多个Stream Tread,每个Thread处理1~n个Task。一个Task对应一个分区,因此Kafka Stream流处理的并行度不会超越Topic的分区数。需要值得注意的是Kafka的每个Task都维护这自身的一些状态,线程之间不存在状态共享和通信。因此Kafka在实现流处理的过程中扩展是非常高效的。
容错
Kafka Streams构建于Kafka本地集成的容错功能之上。 Kafka分区具有高可用性和复制性;因此当流数据持久保存到Kafka时,即使应用程序失败并需要重新处理它也可用。 Kafka Streams中的任务利用Kafka消费者客户端提供的容错功能来处理故障。如果任务运行的计算机故障了,Kafka Streams会自动在其余一个正在运行的应用程序实例中重新启动该任务。
此外,Kafka Streams还确保local state store也很有力处理故障容错。对于每个state store,Kafka Stream维护一个带有副本changelog的Topic,在该Topic中跟踪任何状态更新。这些changelog Topic也是分区的,该分区和Task是一一对应的。如果Task在运行失败并Kafka Stream会在另一台计算机上重新启动该任务,Kafka Streams会保证在重新启动对新启动的任务的处理之前,通过重播相应的更改日志主题,将其关联的状态存储恢复到故障之前的内容。
5.2 kafka stream lowlevel api
lowlevel api作为了解即可,在实际工作中,都会使用high api。
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>2.2.0</version>
</dependency>
案例:消费A topic的数据,进行wordCount统计,结果输入到B topic中。
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.139.156:9092");
properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-processor-application");
// 创建Topology
Topology topology = new Topology();
topology.addSource("input_name", "Atopic");
topology.addProcessor("wordCountProcessor_name", () -> new WordCountProcessor(), "input_name");
topology.addSink("output_name", "Btopic", new StringSerializer(), new LongSerializer(), "wordCountProcessor_name");
KafkaStreams streams = new KafkaStreams(topology, properties);
streams.start();
}
public class WordCountProcessor implements Processor<String, String> {
private HashMap<String, Long> wordPair;
private ProcessorContext context;
public void init(ProcessorContext context) {
wordPair = new HashMap<>();
this.context = context;
// 每隔1秒将处理的结果向下游传递
this.context.schedule(Duration.ofSeconds(1), PunctuationType.STREAM_TIME, timestamp -> {
wordPair.forEach((k,v) -> {
this.context.forward(k,v);
});
});
}
public void process(String key, String value) {
String[] words = value.split(" ");
for (String word : words) {
Long num = wordPair.getOrDefault(word, 0L);
num++;
wordPair.put(word, num);
}
context.commit();
}
public void close() {
}
}
也可以作为数据接入,解析json使用
public class WordCountProcessor implements Processor<String, String> {
private ProcessorContext context;
public void init(ProcessorContext context) {
this.context = context;
}
public void process(String key, String value) {
JSONObject jsonObject = JSONObject.parseObject(value);
StringBuilder append = new StringBuilder(jsonObject.getString("A")).append("\t").append(jsonObject.getString("B"));
context.forward(key,append.toString());
}
public void close() {
}
}
思考:,如果进行wordCount统计,当服务器出现故障或者进程丢掉的情况下,那统计的数据结果怎么办?
接下来进行一个有状态的案例:
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.139.156:9092");
properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-processor-application");
// 创建state,存放状态信息
Map<String, String> changelogConfig = new HashMap();
/**
* changlog数据清除策略
* 一、默认策略(delete) 删除超过保留期的过期数据
* 二、compact(压实) 多个key相同的数据 使用新值覆盖旧值
* */
changelogConfig.put("cleanup.policy","compact");
StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = Stores.keyValueStoreBuilder(
Stores.persistentKeyValueStore("Counts"),
Serdes.String(),
Serdes.Long()).withLoggingEnabled(changelogConfig); // 开启远程状态副本 容错故障恢复
// 创建Topology
Topology topology = new Topology();
topology.addSource("input", "Atopic");
topology.addProcessor("wordCountProcessor", () -> new WordCountProcessor(), "input");
topology.addStateStore(countStoreSupplier,"wordCountProcessor");
topology.addSink("output", "Btopic", new StringSerializer(), new LongSerializer(), "wordCountProcessor");
KafkaStreams streams = new KafkaStreams(topology, properties);
streams.start();
}
public class WordCountProcessor implements Processor<String, String> {
private KeyValueStore<String, Long> keyValueStore;
private ProcessorContext context;
public void init(ProcessorContext context) {
keyValueStore = (KeyValueStore<String, Long>) context.getStateStore("Counts");
this.context = context;
// 定期向下游输出计算结果
this.context.schedule(Duration.ofSeconds(1), PunctuationType.STREAM_TIME, timestamp -> {
KeyValueIterator<String, Long> iterator = keyValueStore.all();
while (iterator.hasNext()) {
KeyValue<String, Long> entry = iterator.next();
this.context.forward(entry.key, entry.value);
}
iterator.close();
});
}
public void process(String key, String value) {
String[] words = value.split(" ");
for (String word : words) {
Long oldValue = keyValueStore.get(word);
if (oldValue == null) {
keyValueStore.put(word, 1L);
} else {
keyValueStore.put(word, oldValue + 1L);
}
}
context.commit();
}
public void close() {
}
}
注意:事实StateStore本质是一个Topic,但是改topic的清除策略不在是delete,而是compact.