- kafka基础架构
- 消费者组:由多个 consumer 组成。 消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个 组内 消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即 消费者组是逻辑上的一个订阅者
- broker:一台服务器就是一个broker。一个集群由多个broker组成。一个broker可容纳多个topic
- Topic:队列,生产者消费者面向的都是一个topic
- partition:分区,为了实现拓展性,一个非常大的topic可以分布到多个broker上,一个topic可以分为多个partition。每个partition都是一个有序队列。
- Replication:副本,为保证集群中的某个节点发送故障,该节点上的 partition 数据不丢失,且 kafka 仍然能够继续工作,kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,一个 leader 和若干个 follower。
- leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 leader。
- follower : :每个分区多个副本中的“从”,实时从 leader 中同步数据,保持和 leader 数据的同步。leader 发生故障时,某个 follower 会成为新的 follower。
- 命令行操作
创建topic
bin/kafka-topics.sh --bootstrap-server 192.168.1.222:9092 --create --partitions 1 --replication-factor 3 --topic fisrtTopic
查看信息
bin/kafka-topics.sh --bootstrap-server 192.168.1.222:9092 --describe --topic fisrtTopic
topic消息存放在log文件夹中
消息序列化后存在在 0000000000000000.log中
系统默认有一个topic,__consumer_offsets,50个分区,(轮询分布在各个broker)1个副本
- kafka工作流程
topic 是逻辑上的概念,而 partition 是物理上的概念,每个 partition 对应于一个 log 文件,该 log 文件中存储的就是 producer 生产的数据。Producer 生产的数据会被不断追加到该log 文件末端,且每条数据都有自己的 offset。消费者组中的每个消费者,都会实时记录自己消费到了哪个 offset,以便出错恢复时,从上次的位置继续消费。
由于生产者生产的消息会不断追加到 log 文件末尾,为防止 log 文件过大导致数据定位效率低下,Kafka 采取了 分片和 索引机制,将每个 partition 分为多个 segment。每个 segment对应两个文件——“.index”文件和“.log”文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic 名称+分区序号。例如,first 这个 topic 有三个分区,则其对应的文件夹为 first-0,first-1,first-2
“.index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中 message 的物理偏移地址。
- 分区原则
2.数据的可靠性保障
为保证 producer 发送的数据,能可靠的发送到指定的 topic,topic 的每个 partition 收到producer 发送的数据后,都需要向 producer 发送 ack(acknowledgement 确认收到),如果producer 收到 ack,就会进行下一轮的发送,否则重新发送数据。
何时发送ack
全部的follower同步完成,才可以发送ack
ISR
Leader 维护了一个动态的 in-sync replica set (ISR),意为和 leader 保持同步的 follower 集合。当 ISR 中的 follower 完成数据的同步之后,leader 就会给 follower 发送 ack。如果 follower长 时 间 未 向 leader 同 步 数 据 , 则 该 follower 将 被 踢 出 ISR,
该 时 间 阈 值 由replica.lag.time.max.ms 参数设定。Leader 发生故障之后,就会从 ISR 中选举新的 leader。
ack应答机制
0:producer 不等待 broker 的 ack,这一操作提供了一个最低的延迟,broker 一接收到还没有写入磁盘就已经返回,当 broker 故障时有可能 丢失数据
1:producer 等待 broker 的 ack,partition 的 leader 落盘成功后返回 ack,如果在 follower同步成功之前 leader 故障,那么将会 丢失数据;
-1(all):producer 等待 broker 的 ack,partition 的 leader 和 follower 全部落盘成功后才返回 ack。但是如果在 follower 同步完成后,broker 发送 ack 之前,leader 发生故障,那么会造成数据重复
故障迁移
LEO :每个副本中的最后一个offset (Log end 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
Exactly Once = At Least Once + 幂等性
要启用幂等性,只需要将 Producer 的参数中 enable.idompotence 设置为 true 即可。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker 只会持久化一条。
消息发送流程
Kafka 的 Producer 发送消息采用的是 异步发送的方式。在消息发送的过程中,涉及到了两个线程 ——main 线程和 Sender 线程,以及 一个线程共享变量 ——RecordAccumulator。main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker。
batch.size :只有数据积累到 batch.size 之后,sender 才会发送数据。
linger.ms :如果数据迟迟未达到 batch.size,sender 等待 linger.time 之后就会发送数据。
<dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>0.11.0.0</version> </dependency>
Properties props = new Properties(); //kafka 集群,broker-list props.put("bootstrap.servers", "192.168.1.222:9092,192.168.1.222:9093,192.168.1.222:9094"); props.put("acks", "all"); //重试次数 props.put("retries", 1); //当批次超过16K或者等待时间超过1毫秒时,就会sender到broker //批次大小 默认16K props.put("batch.size", 16384); //等待时间 1毫秒 props.put("linger.ms", 1); //RecordAccumulator 缓冲区总大小 默认32M props.put("buffer.memory", 33554432); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer<String, String> producer = new KafkaProducer<>(props); for (int i = 0; i < 100; i++) { producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i))); } producer.close();
带回调函数的
回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是RecordMetadata 和 Exception,如果 Exception 为 null,说明消息发送成功,如果Exception 不为 null,说明消息发送失败。
注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。
Properties props = new Properties(); //kafka 集群,broker-list props.put("bootstrap.servers", "192.168.1.222:9092,192.168.1.222:9093,192.168.1.222:9094"); props.put("acks", "all"); //重试次数 props.put("retries", 1); //当批次超过16K或者等待时间超过1毫秒时,就会sender到broker //批次大小 默认16K props.put("batch.size", 16384); //等待时间 1毫秒 props.put("linger.ms", 1); //RecordAccumulator 缓冲区总大小 默认32M props.put("buffer.memory", 33554432); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer<String, String> producer = new KafkaProducer<>(props); for (int i = 0; i < 100; i++) { producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)), new Callback() { //回调函数,该方法会在 Producer 收到 ack 时调用,为异步调用 @Override public void onCompletion(RecordMetadata metadata, Exception exception) { if (exception == null) { System.out.println("success->" + metadata.offset()); } else { exception.printStackTrace(); } } }); } producer.close(); }
同步发送api
同步发送的意思就是,一条消息发送之后,会阻塞当前线程,直至返回 ack。
由于 send 方法返回的是一个 Future 对象,根据 Futrue 对象的特点,我们也可以实现同步发送的效果,只需在调用 Future 对象的 get 方发即可。
Properties props = new Properties(); props.put("bootstrap.servers", "hadoop102:9092");//kafka 集群,broker-list props.put("acks", "all"); props.put("retries", 1);//重试次数 props.put("batch.size", 16384);//批次大小 props.put("linger.ms", 1);//等待时间 props.put("buffer.memory", 33554432);//RecordAccumulator 缓冲区大小 props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer<String, String> producer = new KafkaProducer<>(props); for (int i = 0; i < 100; i++) { producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i))).get(); } producer.close();
消费者
自动提交offset
enable.auto.commit :是否开启自动提交 offset 功能
auto.commit.interval.ms :自动提交 offset 的时间间隔
Properties props = new Properties(); props.put("bootstrap.servers", "192.168.1.222:9092,192.168.1.222:9093,192.168.1.222:9094"); props.put("group.id", "test"); props.put("enable.auto.commit", "true"); props.put("auto.commit.interval.ms", "1000"); props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Collections.singletonList("first")); while (true) { ConsumerRecords<String, String> records = consumer.poll(100); for (ConsumerRecord<String, String> record : records) System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value()); }
手动提交offset
手动提交 offset 的方法有两种:分别是 commitSync(同步提交)和 commitAsync(异步提交)。两者的相同点是,都会将次 本次 poll 的一批数据最高的偏移量提交;不同点是,commitSync 阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而 commitAsync 则没有失败重试机制,故有可能提交失败。
同步提交
Properties props = new Properties(); //Kafka 集群 props.put("bootstrap.servers", "hadoop102:9092"); //消费者组,只要 group.id 相同,就属于同一个消费者组 props.put("group.id", "test"); props.put("enable.auto.commit", "false");//关闭自动提交 offset props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Arrays.asList("first"));//消费者订阅主题 while (true) { //消费者拉取数据 ConsumerRecords<String, String> records = consumer.poll(100); for (ConsumerRecord<String, String> record : records) { System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value()); } //同步提交,当前线程会阻塞直到 offset 提交成功 consumer.commitSync(); }
异步提交
Properties props = new Properties(); //Kafka 集群 props.put("bootstrap.servers", "hadoop102:9092"); //消费者组,只要 group.id 相同,就属于同一个消费者组 props.put("group.id", "test"); //关闭自动提交 offset props.put("enable.auto.commit", "false"); props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Arrays.asList("first"));//消费者订阅主题 while (true) { ConsumerRecords<String, String> records = consumer.poll(100);//消费者拉取数据 for (ConsumerRecord<String, String> record : records) { System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value()); } //异步提交 consumer.commitAsync(new OffsetCommitCallback() { @Override public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) { if (exception != null) { System.err.println("Commit failed for" + offsets); } } }); }