kafka面试题总结
文章目录
应用场景
削峰填谷,日志处理,系统解耦,异步通信
什么是kafka
scala编写,主要作用是:发布订阅,存储,处理
应用场景:构建实时数据管道,在系统和应用程序间获取数据;构建实时流应用程序,以转换或响应数据流
搭建集群
修改brokerid,配置log.dir
生产者消费者代码编写
public class KafkaProducerTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建用于连接Kafka的Properties配置
Properties props = new Properties();
props.put("bootstrap.servers", "node1.itcast.cn:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 2. 创建一个生产者对象KafkaProducer
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
// 3. 发送1-100的消息到指定的topic中
for(int i = 0; i < 10000000; ++i) {
// 一、使用同步等待的方式发送消息
// // 构建一条消息,直接new ProducerRecord
// ProducerRecord<String, String> producerRecord = new ProducerRecord<>("test", null, i + "");
// Future<RecordMetadata> future = kafkaProducer.send(producerRecord);
// // 调用Future的get方法等待响应
// future.get();
// System.out.println("第" + i + "条消息写入成功!");
// 二、使用异步回调的方式发送消息
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("test_1m", null, i + "");
kafkaProducer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
// 1. 判断发送消息是否成功
if(exception == null) {
// 发送成功
// 主题
String topic = metadata.topic();
// 分区id
int partition = metadata.partition();
// 偏移量
long offset = metadata.offset();
System.out.println("topic:" + topic + " 分区id:" + partition + " 偏移量:" + offset);
}
else {
// 发送出现错误
System.out.println("生产消息出现异常!");
// 打印异常消息
System.out.println(exception.getMessage());
// 打印调用栈
System.out.println(exception.getStackTrace());
}
}
});
}
// 4.关闭生产者
kafkaProducer.close();
}
}
public class KafkaConsumerTest {
public static void main(String[] args) throws InterruptedException {
// 1.创建Kafka消费者配置
Properties props = new Properties();
props.setProperty("bootstrap.servers", "node1.itcast.cn:9092,node2.itcast.cn:9092,node3.itcast.cn:9092");
// 消费者组(可以使用消费者组将若干个消费者组织到一起),共同消费Kafka中topic的数据
// 每一个消费者需要指定一个消费者组,如果消费者的组名是一样的,表示这几个消费者是一个组中的
props.setProperty("group.id", "test");
// 自动提交offset
props.setProperty("enable.auto.commit", "true");
// 自动提交offset的时间间隔
props.setProperty("auto.commit.interval.ms", "1000");
// 拉取的key、value数据的
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 2.创建Kafka消费者
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(props);
// 3. 订阅要消费的主题
// 指定消费者从哪个topic中拉取数据
kafkaConsumer.subscribe(Arrays.asList("test"));
// 4.使用一个while循环,不断从Kafka的topic中拉取消息
while(true) {
// Kafka的消费者一次拉取一批的数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(5));
// 5.将将记录(record)的offset、key、value都打印出来
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
// 主题
String topic = consumerRecord.topic();
// offset:这条消息处于Kafka分区中的哪个位置
long offset = consumerRecord.offset();
// key\value
String key = consumerRecord.key();
String value = consumerRecord.value();
System.out.println("topic: " + topic + " offset:" + offset + " key:" + key + " value:" + value);
}
Thread.sleep(1000);
}
}
}
kafka架构
broker:等同于rabbitmq的broker,broker是无状态的,是通过zookepper来维持状态的。
zookepper:协调管理broker,并且存储了kafka的元数据(topic,pritition,consumer等),主要作用是通知生产者消费者有新的broker加入,或者出现故障的broker
producer:顾名思义,生产者。将数据推给broker的topic
consumer:消费者,从broker的topic中拉区消息
consumer group:消费者组,一个消费者组有唯一id,包含多个消费者,这是一个可以拓展的容错性的消费者机制
partittion:分区,主体被换分成多个分区。
replicas:副本,用于出错的时候服务依然可用。
topic:主题。是一个逻辑概念,可以有很多歌主题,主题中的消息应该有相同的结构。发送到主题后的消息是无法更新的。
offset:偏移量。记录分区中将要发给consumer的消息序号。
kafka幂等性
分区有pid,每一条消息有递增的seq来保障。
事务
使用了事务,不要使用异步发送的方式。
生产者要配置事务id
props.put(“transactional.id”, “dwd_user”);
消费者
// 配置事务的隔离级别
props.put(“isolation.level”,“read_committed”);
// 关闭自动提交,一会我们需要手动来提交offset,通过事务来维护offset
props.setProperty(“enable.auto.commit”, “false”);
基本流程:
- 初始化事务
- 开启事务
- 需要使用producer来将消费者的offset提交到事务中
- 提交事务
- 如果出现异常回滚事务
public class TransactionProgram {
public static void main(String[] args) {
// 1. 调用之前实现的方法,创建消费者、生产者对象
KafkaConsumer<String, String> consumer = createConsumer();
KafkaProducer<String, String> producer = createProducer();
// 2. 生产者调用initTransactions初始化事务
producer.initTransactions();
// 3. 编写一个while死循环,在while循环中不断拉取数据,进行处理后,再写入到指定的topic
while(true) {
try {
// (1) 生产者开启事务
producer.beginTransaction();
// 这个Map保存了topic对应的partition的偏移量
Map<TopicPartition, OffsetAndMetadata> offsetMap = new HashMap<>();
// 从topic中拉取一批的数据
// (2) 消费者拉取消息
ConsumerRecords<String, String> concumserRecordArray = consumer.poll(Duration.ofSeconds(5));
// (3) 遍历拉取到的消息,并进行预处理
for (ConsumerRecord<String, String> cr : concumserRecordArray) {
// 将1转换为男,0转换为女
String msg = cr.value();
String[] fieldArray = msg.split(",");
// 将消息的偏移量保存
// 消费的是ods_user中的数据
String topic = cr.topic();
int partition = cr.partition();
long offset = cr.offset();
// offset + 1:offset是当前消费的记录(消息)对应在partition中的offset,而我们希望下一次能继续从下一个消息消息
// 必须要+1,从能消费下一条消息
offsetMap.put(new TopicPartition(topic, partition), new OffsetAndMetadata(offset + 1));
// 将字段进行替换
if(fieldArray != null && fieldArray.length > 2) {
String sexField = fieldArray[1];
if(sexField.equals("1")) {
fieldArray[1] = "男";
}
else if(sexField.equals("0")){
fieldArray[1] = "女";
}
}
// 重新拼接字段
msg = fieldArray[0] + "," + fieldArray[1] + "," + fieldArray[2];
// (4) 生产消息到dwd_user topic中
ProducerRecord<String, String> dwdMsg = new ProducerRecord<>("dwd_user", msg);
// 发送消息
Future<RecordMetadata> future = producer.send(dwdMsg);
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
producer.abortTransaction();
}
}
producer.sendOffsetsToTransaction(offsetMap, "ods_user");
int i = 1 / 0;
// (6) 提交事务
producer.commitTransaction();
}catch (Exception e) {
e.printStackTrace();
// (7) 捕获异常,如果出现异常,则取消事务
producer.abortTransaction();
}
}
}
// 一、创建一个消费者来消费ods_user中的数据
private static KafkaConsumer<String, String> createConsumer() {
// 1. 配置消费者的属性(添加对事务的支持)
Properties props = new Properties();
props.setProperty("bootstrap.servers", "node1.itcast.cn:9092");
props.setProperty("group.id", "ods_user");
// 配置事务的隔离级别
props.put("isolation.level","read_committed");
// 关闭自动提交,一会我们需要手动来提交offset,通过事务来维护offset
props.setProperty("enable.auto.commit", "false");
// 反序列化器
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 2. 构建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(props);
// 3. 订阅一个topic
kafkaConsumer.subscribe(Arrays.asList("ods_user"));
return kafkaConsumer;
}
// 二、编写createProducer方法,用来创建一个带有事务配置的生产者
private static KafkaProducer<String, String> createProducer() {
// 1. 配置生产者带有事务配置的属性
Properties props = new Properties();
props.put("bootstrap.servers", "node1.itcast.cn:9092");
props.put("acks", "all");
// 开启事务必须要配置事务的ID
props.put("transactional.id", "dwd_user");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 2. 构建生产者
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
return kafkaProducer;
}
}
分区副本机制
生产者的分区写入策略:轮询(比较均匀);随机(不适用);哈希;自定义
消费组rebalance再平衡:触发条件:消费者数量变化,分区数量变化,topic被删除、不良影响:stop the world
消费者分区分配策略:range(取余分配),roundrobin(轮询分配),粘性分配(轮询分配+出现问题再平衡,但是尽量保证和原本不变,减少上下文切换)
副本ack机制:用来保证生产者的消息可靠性。ack=0.生产者一直写,ack=1,写入leader分区后继续写;ack=all,写入全部分区都成功后继续写,性能最差但可靠性最高;
leader和follower
- Kafka中的leader和follower是相对分区有意义,不是相对broker
- Kafka在创建topic的时候,会尽量分配分区的leader在不同的broker中,其实就是负载均衡
- leader职责:读写数据
- follower职责:同步数据、参与选举(leader crash之后,会选举一个follower重新成为分区的leader
- 注意和ZooKeeper区分
- ZK的leader负责读、写,follower可以读取
- Kafka的leader负责读写、follower不能读写数据(确保每个消费者消费的数据是一致的),Kafka一个topic有多个分区leader,一样可以实现数据操作的负载均衡
理解ISR
- AR表示一个topic下的所有副本
- ISR:In Sync Replicas,正在同步的副本(可以理解为当前有几个follower是存活的)
- OSR:Out of Sync Replicas,不再同步的副本
- AR = ISR + OSR
leader选举
- Controller:controller是kafka集群的老大,是针对Broker的一个角色
- Controller是高可用的,是用过ZK来进行选举
- Leader:是针对partition的一个角色
- Leader是通过ISR来进行快速选举
- 如果Kafka是基于ZK来进行选举,ZK的压力可能会比较大。例如:某个节点崩溃,这个节点上不仅仅只有一个leader,是有不少的leader需要选举。通过ISR快速进行选举。
- leader的负载均衡
- 如果某个broker crash之后,就可能会导致partition的leader分布不均匀,就是一个broker上存在一个topic下不同partition的leader
- 通过以下指令,可以将leader分配到优先的leader对应的broker,确保leader是均匀分配的
bin/kafka-leader-election.sh --bootstrap-server node1.itcast.cn:9092 --topic test --partition=2 --election-type preferred
读写流程
写流程
- 通过ZooKeeper找partition对应的leader,leader是负责写的
- producer开始写入数据
- ISR里面的follower开始同步数据,并返回给leader ACK
- 返回给producer ACK
读流程
- 通过ZooKeeper找partition对应的leader,leader是负责读的
- 通过ZooKeeper找到消费者对应的offset
- 然后开始从offset往后顺序拉取数据
- 提交offset(自动提交——每隔多少秒提交一次offset、手动提交——放入到事务中提交)
物理结构
topic(逻辑结构)- partition - segment(.log文件) 通过offset找到segment再通过局部offset再segment上通过稀疏索引找到对应数据
理解消息不丢失机制
生产者:ack机制,配置0或1可能会丢失
消费者:(消息传递的语义性)at-least-once和exactly-once不会丢失
broker:有副本机制,所以不会丢失,除非只有一个副本。
理解数据清理
默认7天过期。达到一定条件,会被标记为待清理。
日志合并,多个版本的数据合并成最新
限制生产者消费者的速率,防止速度过快占用服务器所有的io资源