Kafka
分布式基于发布/订阅模式的消息队列
消息队列
消息队列优点
-
解耦
只要确保遵守同样的接口约束,允许独立的扩展或修改两边的处理过程
-
可恢复性
系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理
-
缓冲
有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况
-
灵活性
消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃
-
异步通信
消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
两种模式
点对点模式(一对一)
-
消费者主动拉取数据,消息收到后消息清除
-
消息生产者生产消息发送到Queue中,然后消息消费者从Queue中取出并且消费消息。
-
消息被消费以后,queue中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
发布/订阅模式(一对多)
- 消费者消费数据后不会清除消息
- 消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。
基础架构
1)Producer **:**消息生产者,就是向kafka broker发消息的客户端;
2)Consumer **:**消息消费者,向kafka broker取消息的客户端;
3)Consumer Group (CG):消费者组,由多个consumer组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
4)Broker **:**一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic。
5)Topic **:**可以理解为一个队列,生产者和消费者面向的都是一个topic;
6)Partition:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列;
7)Replica:副本,为保证集群中的某个节点发生故障时,该节点上的partition数据不丢失,且kafka仍然能够继续工作,kafka提供了副本机制,一个topic的每个分区都有若干个副本,一个leader和若干个follower。
8)leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是leader。
**9)follower:**每个分区多个副本中的“从”,实时从leader中同步数据,保持和leader数据的同步。leader发生故障时,某个follower会成为新的leader。
-
Kafka集群
Kafka集群由多个Broker组成,每个Broker拥有唯一Id.Kafka集群中有多个Topic,每个Topic可有多个分区(partition),每个分区可有多个副本(replication)
-
一个Topic的多个分区可以存在一个Broker中,一个分区的多个副本只能存在不同的Broker中。
-
一个分区的多个副本由一个leader和多个follower组成
-
生产者和消费者读写数据面向leader
-
follower主要同步leader数据,当leader故障后,follower代替leader
-
-
生产者
- 生产者往Topic中发布消息
-
消费者
- 消费者从topic中消费消息
- 消费者消费消息以消费者组为单位进行
- 一个消费者组内的一个消费者可以同时消费一个topic中多个分区的消息
- 一个topic中的一个分区的消息同时只能被一个消费者组中的一个消费者消费
-
Zookeeper
- Kafka集群中的工作需要Zookeeper,例如每个broker启动后需要向Zookeeper注册
- Broker中大哥(controller)的选举(争抢策略)
- Kafka 0.9版本之前消费者组的offset维护在Zookeeper中,0.9之后维护在Kafka内部
命令
查当前服务器所有topic
kafka-topics.sh --list --bootstrap-server hadoop102:9092
创建topic
kafka-topics.sh --creat --bootstrap-server hadoop102:9092 --topic second --partitions 2 --replication-factor 3
删除topic
kafka-topics.sh --bootstrap-server hadoop102:2181 --delete --topic first
修改topic分区(分区只能改大不能改小,因为分区中可能存在消费者正在读数据)
kafka-topics.sh --describe --bootstrap-server hadoop102:9092 --topic first
生产者
kafka-console-producer.sh --broker-list hadoop102:9092 --topic first
消费者
kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first
消费者消费数据offset重置问题:
新启动的消费者组消费不到topic中的数据
- 消费者重置消费
kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first --from-beginning
指定消费者组
先在consumer.properties中指定group id 或者在命令中指定
kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first --consumer.config /opt/module/kafka_2.11-2.4.1/config/consumer.properties
kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first --group aa
如果有两个分区,组里有两个消费者,则两个消费者轮流消费
如果有两个分区,组里有三个消费者,则有一个消费者休息
工作流程和文件存储机制
-
Kafka中消息是以topic进行分类,生产者生产消息,消费者消费消息,都是面向topic的
-
topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是producer生产的数据。
-
Producer生产的数据会被不断追加到该log文件末端,且每条数据都有自己的offset。消费者组中的每个消费者,都会实时记录自己消费到了哪个offset,以便出错恢复时,从上次的位置继续消费
-
Kafka采取了分片和索引机制,将每个partition分为多个segment。每个segment对应两个文件——“.index”文件和“.log”文件
-
Kafka不会对每个文件存一个索引,而是采用稀疏索引,多个文件存入一个索引
生产者
分区原因
(1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了
(2)可以提高并发,因为可以以Partition为单位读写了
分区原则
将producer发送的数据封装成一个ProducerRecord对象
-
指明 partition 的情况下,直接将指明的值直接作为 partiton 值;
-
没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值;
-
既没有 partition 值又没有 key 值的情况下, kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成,kafka再随机一个分区进行使用.
数据可靠性保证
-
ack机制保证数据的可靠性
-
为什么kafka选的是全部follwer同步完再发送ACK:
半数的机制:机器数要有2n+1台(挂掉一半还能运行),但是全部同步完,只用要求n+1台
-
ISR
- in-sync replica set (ISR)
- 意为和leader保持同步的follower集合。当ISR中的follower完成数据的同步之后,leader就会给producer发送ack。如果follower长时间未向leader同步数据,则该follower将被踢出ISR
- 时间阈值由replica.lag.time.max.ms参数设定。Leader发生故障之后,就会从ISR中选举新的leader
-
ack应答
-
acks参数设置
-
0:提供了一个最低的延迟,partition的leader接收到消息还没有写入磁盘就已经返回ack,当leader故障时有可能丢失数据;
1: partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据;
-1(all): partition的leader和follower全部落盘成功后才返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复
-
-
Log文件的HW和LEO
LEO:指的是每个副本最大的offset;
HW:指的是消费者能见到的最大的offset,ISR队列中最小的LEO。
(1)follower故障
follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。
(2)leader故障
leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。
注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
Exactly Once
ack -1:Producer到Server之间不会丢失数据, At Least Once. ack 0:生产者每条消息只会被发送一次,At Most Once
幂等性结合At Least Once语义,就构成了Kafka的Exactly Once语义。即:At Least Once + 幂等性 = Exactly Once
幂等性就是指Producer不论向Server发送多少次重复数据,Server端都只会持久化一条
但是PID重启就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区跨会话的Exactly Once
消费者
消费方式是pull。push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。
分配策略
RoundRobin,Range , Sticky
RoundRobin:轮询分配
Sticky:黏性,第一次分配和RoundRobin一样,再分配(消费者增加/减少)的时候,原有的分配不变,只变增多/减少的部分
Range:范围分区,分区数对消费者取余
offset
consumer需要实时记录自己消费到了哪个offset,以便故障恢复后继续消费
Kafka 0.9版本之前,consumer默认将offset保存在Zookeeper中,从0.9版本开始,consumer默认将offset保存在Kafka一个内置的topic中,该topic为**__consumer_offsets**
高效读写数据
- producer生产数据写入到log文件中,写的过程是追加到文件末端,顺序写。顺序写省去了大量磁头寻址的时间
- Kafka数据持久化是直接持久化到Pagecache
- I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能
- I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间
- 充分利用所有空闲内存(非 JVM 内存)。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担
- 读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据
- 如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用
- 持久化到Pagecache上可能会造成宕机丢失数据的情况,但这可以被Kafka的Replication机制解决
相关API
Producer
异步
public class KafkaProducerDemo {
/**
* 生产者,异步发送,不带回调
* 配置类:ProducerConfig
* ProducerConfig.BOOTSTRAP_SERVERS_CONFIG;
* ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG;
* 可以直接通过ProducerConfig和ConsumerConfig调
*/
public static void main(String[] args) {
//0.创建配置对象
Properties properties = new Properties();
//kafka集群.broker-list
properties.put("bootstrap.servers","hadoop102:9092");
//ack级别
properties.put("acks","all");
//批次大小
properties.put("batch.size",16384);
// 等待时间
properties.put("linger.ms",1);
// RecordAccumulator缓冲区大小
properties.put("buffer.memory",33554432);
//kv序列化器
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
// 1. 创建生产者对象<S
Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
// 2. 生产数据
for (int i = 0; i < 10; i++) {
//1.指定partition
// kafkaProducer.send(new ProducerRecord<String, String>("second",0,null,"jojo"+i));
//2.指定key
// kafkaProducer.send(new ProducerRecord<String, String>("second","1","==jojo=="+i));
//3.黏性
kafkaProducer.send(new ProducerRecord<String, String>("second","==jojo=="+i));
}
//3.关闭对象
kafkaProducer.close();
}
}
public class KafkaProducerDemo2 {
/**
* 生产者,异步发送,带回调
* 配置类:ProducerConfig
* ProducerConfig.BOOTSTRAP_SERVERS_CONFIG;
* ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG;
* 可以直接通过ProducerConfig和ConsumerConfig调
*
*/
public static void main(String[] args) {
//0.创建配置对象
Properties properties = new Properties();
//kafka集群.broker-list
properties.put("bootstrap.servers","hadoop102:9092");
//ack级别
properties.put("acks","all");
//批次大小
properties.put("batch.size",16384);
// 等待时间
properties.put("linger.ms",1);
// RecordAccumulator缓冲区大小
properties.put("buffer.memory",33554432);
//kv序列化器
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer"); properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
// 1. 创建生产者对象<S
Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
// 2. 生产数据
for (int i = 0; i < 10; i++) {
//1.指定partition
// kafkaProducer.send(new ProducerRecord<String, String>("second",0,null,"jojo"+i));
//2.指定key
// kafkaProducer.send(new ProducerRecord<String, String>("second","1","==jojo=="+i));
//3.黏性
// kafkaProducer.send(new ProducerRecord<String, String>("second","==jojo=="+i));
kafkaProducer.send(new ProducerRecord<String, String>("second", "jojojojojo" + i), new Callback() {
/**
* 消息发送完成后,会调用此方法
* @param recordMetadata 消息的元数据
* @param e 当消息发送过程中,如果抛出异常,会传入该方法
*/
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e!=null)
System.out.println("消息发送失败:"+e.getMessage());
else System.out.println("消息发送成功"+recordMetadata.topic()+";"+recordMetadata.partition()+";"+
recordMetadata.offset());
}
});
}
//3.关闭对象
kafkaProducer.close();
}
}
同步
同步和异步的区别,对线程进行了阻塞,使用回调函数,当消息成功发送后,程序再继续向下执行
public class KafkaProducerDemo3 {
/**
* 生产者,同步发送,带回调
* 配置类:ProducerConfig
* ProducerConfig.BOOTSTRAP_SERVERS_CONFIG;
* ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG;
* 可以直接通过ProducerConfig和ConsumerConfig调
*/
public static void main(String[] args) throws ExecutionException, InterruptedException {
//0.创建配置对象
Properties properties = new Properties();
//kafka集群.broker-list
properties.put("bootstrap.servers","hadoop102:9092");
//ack级别
properties.put("acks","all");
//批次大小
properties.put("batch.size",16384);
// 等待时间
properties.put("linger.ms",1);
// RecordAccumulator缓冲区大小
properties.put("buffer.memory",33554432);
//kv序列化器
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
// 1. 创建生产者对象<S
Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
// 2. 生产数据
for (int i = 0; i < 10; i++) {
//1.指定partition
// kafkaProducer.send(new ProducerRecord<String, String>("second",0,null,"jojo"+i));
//2.指定key
// kafkaProducer.send(new ProducerRecord<String, String>("second","1","==jojo=="+i));
//3.黏性
// kafkaProducer.send(new ProducerRecord<String, String>("second","==jojo=="+i));
Future<RecordMetadata> second = kafkaProducer.send(new ProducerRecord<String, String>("second", "jojojojojo" + i), new Callback() {
/**
* 消息发送完成后,会调用此方法
* @param recordMetadata 消息的元数据
* @param e 当消息发送过程中,如果抛出异常,会传入该方法
*/
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null)
System.out.println("消息发送失败:" + e.getMessage());
else System.out.println("消息发送成功" + recordMetadata.topic() + ";" + recordMetadata.partition() + ";" +
recordMetadata.offset());
}
});
System.out.println("=================");
//阻塞当前线程,一直等到该方法的结果返回为止
RecordMetadata recordMetadata = second.get();
System.out.println("========消息发送完成========");
}
//3.关闭对象
kafkaProducer.close();
}
}
指定分区器
public class KafkaProducerDemo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//0.创建配置对象
Properties properties = new Properties();
//kafka集群.broker-list
properties.put("bootstrap.servers","hadoop102:9092");
//ack级别
properties.put("acks","all");
//批次大小
properties.put("batch.size",16384);
// 等待时间
properties.put("linger.ms",1);
// RecordAccumulator缓冲区大小
properties.put("buffer.memory",33554432);
//kv序列化器
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
// 配置设定自定义的partition
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.jojo.partition.MyPartitioner");
// 1. 创建生产者对象
Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
// 2. 生产数据
String va;
for (int i = 0; i < 10; i++) {
if (i%2==0){
va="jojo";
}
else {
va=new String(UUID.randomUUID().toString());
}
Future<RecordMetadata> second = kafkaProducer.send(new ProducerRecord<String, String>("second", va + i), new Callback() {
/**
* 消息发送完成后,会调用此方法
* @param recordMetadata 消息的元数据
* @param e 当消息发送过程中,如果抛出异常,会传入该方法
*/
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null)
System.out.println("消息发送失败:" + e.getMessage());
else System.out.println("消息发送成功" + recordMetadata.topic() + ";" + recordMetadata.partition() + ";" +
recordMetadata.offset());
}
});
System.out.println("=================");
//阻塞当前线程,一直等到该方法的结果返回为止
RecordMetadata recordMetadata = second.get();
System.out.println("========消息发送完成========");
}
//3.关闭对象
kafkaProducer.close();
}
}
public class MyPartitioner implements Partitioner {
/**
* 消息含有jojo的进入0号分区
* valueBytes是要判断的东西
* @param topic The topic name
* @param key The key to partition on (or null if no key)
* @param keyBytes The serialized key to partition on( or null if no key)
* @param value The value to partition on or null
* @param valueBytes The serialized value to partition on or null
* @param cluster The current cluster metadata
* @return
*/
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
String data = valueBytes.toString();
int partition;
if (data.contains("jojo"))
partition=0;
else partition=1;
return partition;
}
public void close() {
}
public void configure(Map<String, ?> configs) {
}
}
Consumer
public class KafkaConsumerDemo {
public static void main(String[] args) {
// 0. 创建配置
Properties properties = new Properties();
properties.put("bootstrap.servers", "hadoop102:9092");
properties.put("group.id", "bb");
properties.put("enable.auto.commit", "true");
properties.put("auto.commit.interval.ms", "1000");
properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 1. 创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
// 2. 订阅主题
ArrayList<String> topics = new ArrayList<String>();
topics.add("first");
topics.add("second");
kafkaConsumer.subscribe(topics);
//3. 持续消费数据
while (true){
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
for (ConsumerRecord<String, String> record : records) {
System.out.println("消费到:"+record.topic()+";"+record.partition()+";"+record.key()+";"+record.value());
}
}
}
}
offset
offset重置
/**
* offset重置问题
* 重置的情况:
* 1.新的组当前消费组在kafka中没有消费记录
* auto.offset.reset Valid Values: [latest, earliest, none]
* 2.要消费的offset对应的消息已经被删除
*/
public class KafkaConsumerDemo2 {
public static void main(String[] args) {
// 0. 创建配置
Properties properties = new Properties();
properties.put("bootstrap.servers", "hadoop102:9092");
properties.put("group.id", "bb");
properties.put("enable.auto.commit", "true");
properties.put("auto.commit.interval.ms", "1000");
// properties.put("enable.auto.commit", "false");//关闭自动提交offset
// auto.offset.reset Valid Values: [latest, earliest, none]
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 1. 创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
// 2. 订阅主题
ArrayList<String> topics = new ArrayList<String>();
topics.add("first");
topics.add("second");
kafkaConsumer.subscribe(topics);
//3. 持续消费数据
while (true){
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
for (ConsumerRecord<String, String> record : records) {
System.out.println("消费到:"+record.topic()+";"+record.partition()+";"+record.key()+";"+record.value());
}
}
}
}
offset提交方式
手动提交和自动提交的区别:
当producer生产了第一波数据之后
手动提交和自动提交都能正常消费数据,手动提交是自己记录offset,自动提交会把offset提交到producer
此时,在不关闭consumer的情况下,producer生产第二波数据还是正常消费
但如果此时把两个consumer关了,再重新启动
手动提交的consumer会把以前(包括第一波)的数据全部消费一遍
自动提交的consumer只会消费第二波的数据
自动提交
/**
* offset提交
* 1.自动提交
* enable.auto.commit true
* auto.commit.interval.ms 1000
*
* 2.手动提交
* enable.auto.commit false
*
*/
public class KafkaConsumerDemo3 {
public static void main(String[] args) {
// 0. 创建配置
Properties properties = new Properties();
properties.put("bootstrap.servers", "hadoop102:9092");
properties.put("group.id", "bb");
// 自动提交offset
properties.put("enable.auto.commit", "true");
// offset提交的问题
properties.put("auto.commit.interval.ms", "1000");
properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 1. 创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
// 2. 订阅主题
ArrayList<String> topics = new ArrayList<String>();
topics.add("first");
topics.add("second");
kafkaConsumer.subscribe(topics);
//3. 持续消费数据
while (true){
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
for (ConsumerRecord<String, String> record : records) {
System.out.println("消费到:"+record.topic()+";"+record.partition()+";"+record.key()+";"+record.value());
}
}
}
}
手动提交
手动提交分为:
/**
* offset提交
* 1.自动提交
* enable.auto.commit true
* auto.commit.interval.ms 1000
*
* 2.手动提交
* enable.auto.commit false
*
*/
public class KafkaConsumerDemo3 {
public static void main(String[] args) {
// 0. 创建配置
Properties properties = new Properties();
properties.put("bootstrap.servers", "hadoop102:9092");
properties.put("group.id", "bb");
// 自动提交offset
properties.put("enable.auto.commit", "false");
// offset提交的问题
properties.put("auto.commit.interval.ms", "1000");
properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 1. 创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
// 2. 订阅主题
ArrayList<String> topics = new ArrayList<String>();
topics.add("first");
topics.add("second");
kafkaConsumer.subscribe(topics);
//3. 持续消费数据
while (true){
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
for (ConsumerRecord<String, String> record : records) {
System.out.println("消费到:"+record.topic()+";"+record.partition()+";"+record.key()+";"+record.value());
}
// 同步提交offset
// kafkaConsumer.commitSync();
// 异步提交offset 有一个callback
kafkaConsumer.commitAsync(new OffsetCommitCallback() {
// 当offset提交完成后会调用该方法
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
System.out.println("offset提交结束");
}
});
}
}
}
commitSync(同步提交)和commitAsync(异步提交)
无论是同步提交还是异步提交offset,都有可能会造成数据的漏消费或者重复消费。
先提交offset后消费,有可能造成数据的漏消费;而先消费后提交offset,有可能会造成数据的重复消费
所以需要通过mysql之类的组件把消费和提交做成事务
拦截器
对于producer而言,interceptor使得用户在消息发送前以及producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等
同时,producer允许用户指定多个interceptor按序作用于同一条消息从而形成一个拦截链(interceptor chain)。
Intercetpor的实现接口是org.apache.kafka.clients.producer.ProducerInterceptor
// 拦截器producer
public class KafkaProducerDemo4 {
/**
* 生产者,异步发送,不带回调
* 配置类:ProducerConfig
* ProducerConfig.BOOTSTRAP_SERVERS_CONFIG;
* ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG;
* 可以直接通过ProducerConfig和ConsumerConfig调
*/
public static void main(String[] args) {
//0.创建配置对象
Properties properties = new Properties();
//kafka集群.broker-list
properties.put("bootstrap.servers","hadoop102:9092");
properties.put("acks","all");
properties.put("batch.size",16384);
properties.put("linger.ms",1);
properties.put("buffer.memory",33554432);
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
// 设置拦截器
List<String>inters=new ArrayList<>();
inters.add("com.jojo.kafka.interceptor.MyTimeInterceptor");
inters.add("com.jojo.kafka.interceptor.MyCountInterceptor");
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,inters);
// 1. 创建生产者对象
Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
// 2. 生产数据
for (int i = 0; i < 10; i++) {
//3.黏性
kafkaProducer.send(new ProducerRecord<String, String>("second","333==jojo==***"+i));
}
//3.关闭对象
kafkaProducer.close();
}
}
// 拦截器1
public class MyTimeInterceptor implements ProducerInterceptor<String,String> {
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
//1.获取消息的value
String value = record.value();
String res = System.currentTimeMillis()+"-->"+value;
//2.重新构建新的消息对象
ProducerRecord<String, String> stringStringProducer = new ProducerRecord<String, String>(record.topic(), record.partition(), record.key(), res);
return stringStringProducer;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
/**
* 拦截器的核心处理方法
* @param record 被拦截处理的消息
* @return
*/
}
//拦截器2
public class MyCountInterceptor implements ProducerInterceptor<String,String> {
private int success;
private int fail;
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
if (exception!=null){
fail++;
}
else {
success++;
}
}
@Override
public void close() {
System.out.println("success:"+success+";fail:"+fail);
}
@Override
public void configure(Map<String, ?> configs) {
}
}
Kafka和Flume
Kafka source:从Kafka中读数据
- 对于Flume来讲,是一个Source角色,对于Kafka来讲,是一个消费者角色
Kafka Sink:把数据存入Kafka
- 对于Flume来讲,是Sink角色,对于Kafka来讲,是生产者角色
Kafka Channel:把Kafka作为缓冲
-
The Kafka channel can be used for multiple scenarios:
-
With Flume source and sink - it provides a reliable and highly available channel for events【xxxSource->KafkaChannel->xxxSink】
-
With Flume source and interceptor but no sink - it allows writing Flume events into a Kafka topic, for use by other apps
【xxxSource->KafkaChannel】
-
With Flume sink, but no source - it is a low-latency, fault tolerant way to send events from Kafka to Flume sinks such as HDFS, HBase or Solr
【KafkaChannel->xxxSink】
-
Flume接Kafka
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5FdxgTa1-1680873643258)(.\Flume接Kafka.png)]
分析:选用netcat Source,Memory Channel,Kafka Sink
netcat-flume-kafka.conf
#Named
a1.sources=r1
a1.channels=c1
a1.sinks=k1
#Source
a1.sources.r1.type = netcat
a1.sources.r1.bind = 0.0.0.0
a1.sources.r1.port = 6666
#Channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 10000
a1.channels.c1.transactionCapacity = 100
#Sink
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.topic = flumetopic
a1.sinks.k1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.useFlumeEventFormat=true
a1.sinks.k1.kafka.producer.acks = -1
#bind
a1.sources.r1.channels = c1
a1.sinks.k1.channel=c1
flume-ng agent -c $FLUME_HOME/cong -f $FLUME_HOME/jobs/kafka/netcat-flume-kafka.conf -n a1 -Dflume.root.logger=INFO,console
2.包含“atguigu”的数据发给topicat主题,包含“shangguigu”的数据发送topicst主题,其他数据发给topicother主题
分析:在flume中使用拦截器,添加header
再使用多路选择器分别放入不同的Channel 和Sink
或者
在Sink中根据header中的值直接传入不同的topic【Kafka Sink uses the topic
and key
properties from the FlumeEvent headers to send events to Kafka.】
步骤
1.编写拦截器
public class DataValueInterceptor implements Interceptor {
@Override
public void initialize() {
}
@Override
public Event intercept(Event event) {
Map<String, String> headers = event.getHeaders();
String body = new String(event.getBody(), StandardCharsets.UTF_8);
if (body.contains("at")){
headers.put("topic","topicat");
}
else if (body.contains("st")){
headers.put("topic","topicst");
}
return event;
}
@Override
public List<Event> intercept(List<Event> list) {
for (Event event : list) {
intercept(event);
}
return list;
}
@Override
public void close() {
}
public static class MyBuilder implements Builder{
@Override
public Interceptor build() {
return new DataValueInterceptor();
}
@Override
public void configure(Context context) {
}
}
}
2.上传至flume/lib
3.编写flume config
touch netcat-flume-kafka-topic.conf
#Named
a1.sources=r1
a1.channels=c1
a1.sinks=k1
#Source
a1.sources.r1.type = netcat
a1.sources.r1.bind = 0.0.0.0
a1.sources.r1.port = 6666
#Interceptor
a1.sources.r1.interceptors=i1
a1.sources.r1.interceptors.i1.type=com.jojo.kafka.flumeInterceptor.DataValueInterceptor$MyBuilder
#Channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 10000
a1.channels.c1.transactionCapacity = 100
#Sink
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.topic = topicother
a1.sinks.k1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.useFlumeEventFormat=true
a1.sinks.k1.kafka.producer.acks = -1
#bind
a1.sources.r1.channels = c1
a1.sinks.k1.channel=c1
4.启动topicat,topicst,topicother
kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic topicat
kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic topicst
kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic topicother
flume-ng agent -c $FLUME_HOME/conf -f $FLUME_HOME/job/kafka/netcat-flume-kafka-topic.conf -n a1 -Dflume.root.logger=INFO,console
kafka接flume
分析:把Kafka作为source
编写flume config
vim kafka-flume-logger.conf
#Named
a1.sources=r1
a1.channels=c1
a1.sinks=k1
#Source
a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource
a1.sources.r1.batchSize=100
a1.sources.r1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092
a1.sources.r1.kafka.topics = first
a1.sources.r1.kafka.consumer.group.id = flume
#Channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
#Sink
a1.sinks.k1.type = logger
#bind
a1.sources.r1.channels = c1
a1.sinks.k1.channel=c1
4.往topic first中生产数据
启动flume
flume-ng agent -c $FLUME_HOME/conf -f $FLUME_HOME/job/kafka/kafka-flume-logger.conf -n a1 -Dflume.root.logger=INFO,console
kafkaChannel接xxxSink
分析:没有source,用KafkaChannel接sink
编写flume config
vim kafkachannel-flume-logger.conf
#Named
a1.channels=c1
a1.sinks=k1
#Channel
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.channels.c1.kafka.topic = first
a1.channels.c1.kafka.consumer.group.id = flume
a1.channels.c1.kafka.consumer.auto.offset.reset = latest
a1.channels.c1.parseAsFlumeEvent = false
#Sink
a1.sinks.k1.type = logger
#bind
a1.sinks.k1.channel=c1
4.往topic first中生产数据
启动flume
flume-ng agent -c $FLUME_HOME/conf -f $FLUME_HOME/job/kafka/kafkachannel-flume-logger.conf -n a1 -Dflume.root.logger=INFO,console
xxxSource接kafkaChannel
分析:用其他的source接KafkaChannel
编写flume config
vim netcat-flume-kafkachannel.conf
#Named
a1.sources=r1
a1.channels=c1
#Source
a1.sources.r1.type = netcat
a1.sources.r1.bind = 0.0.0.0
a1.sources.r1.port = 6666
#Channel
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.channels.c1.kafka.topic = first
a1.channels.c1.parseAsFlumeEvent = false
#bind
a1.sources.r1.channel=c1
4.往topic first中生产数据
启动flume
flume-ng agent -c $FLUME_HOME/conf -f $FLUME_HOME/job/kafka/netcat-flume-kafkachannel.conf -n a1 -Dflume.root.logger=INFO,console
kafka监控
使用kafka-eagle