Kafka学习笔记(整理)
目录
Kafka概述
- 定义:分布式的基于发布/订阅模式的消息队列,主要应用于大数据实时处理领域。
- 消息队列的好处:
- 解耦:允许独立的扩展或修改数据生产者消费者,只需要确保同样的接口约束。
- 可恢复性:一部分组件失效,不会影响整个系统。消息队列降低进程间的耦合度,即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后处理。
- 缓冲:有助于控制和优化数据流经过系统的速度,解决生产消息和处理消息速度不一致的情况。
- 灵活性、峰值处理能力
- 异步通信:允许把消息放入队列,并不立即处理,需要的时候再去处理。
- 消息队列的两种模式:
- 点对点模式:一对一,消费者拉数据;
- 发布订阅模式:一对多,(队列推 - 推的速度可能不匹配消费者接收速度,消费者崩溃或者资源浪费、消费者拉 - 需要不断的轮询浪费资源)
- Kafka基础架构,依赖于ZK,Kafka是用scala写的
- producer,消息生产者,向kafka broker发消息的客户端
- consumer,消息消费者,从kafka broker取消息的客户端
- consumer group,消费者组,由多个消费者组成,订阅同一个topic,每个消费者负责消费不同分区的数据,一个分区只能由一个消费者消费;消费者组之间互不影响,消费者组是逻辑上的一个订阅者
- broker,一台服务器就是一个broker,一个集群有多个broker组成,一个broker可以容纳多个topic
- topic,可以理解为一个队列,(消费者组是逻辑上的一个topic的订阅者)
- partition,为了实现扩展性,一个topic可以分为多个partition,每个partition是一个有序队列,提高读写的并发
- replica,副本,每个partition有多个副本
- leader,每个分区的leader,生产者发送数据和消费者消费数据的对象都是leader
- follower,每个分区的follower,与leader同步数据,leader发生故障时,某个follower会成为新的leader
Kafka快速入门
安装部署
- 修改conf/server.properties:分发,其他节点需要配置broker.id不重复
################需要配置内容#######################
# broker的全局唯一编号 不能重复
broker.id=102
# 删除topic功能使能
delete.topic.enable=true
# kafka运行数据存放的路径 存放的是数据 不是日志日志在logs里
log.dirs=/opt/module/kafka/data
# 配置连接Zookeeper集群地址 kafka依赖于ZK
zookeeper.connect=localhost102:2181,localhost103:2181,localhost104:2181
#######################################
# broker的全局唯一编号 不能重复
broker.id=0
# 删除topic功能使能
delete.topic.enable=true
# 处理网络请求 的 线程数量
num.network.threads=3
# 用来处理磁盘IO的现成数量
num.io.threads=8
# 发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
# 接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
# 请求套接字的缓冲区大小
socket.request.max.bytes=104857600
# kafka运行日志存放的路径
log.dirs=/opt/module/kafka/logs
# topic 在当前broker上的分区个数
num.partitions=1
# 用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
# segment文件保留的最长时间,超时将被删除
log.retention.hours=168
# 配置连接Zookeeper集群地址
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181
- 启动、关闭
# 启动:每一台都要启动,实际使用最好配置绝对路径
bin/kafka-server-start.sh -daemon config/server.properties # 使用配置的信息启动
# 关闭:
bin/kafka-server-stop.sh
- 群起脚本
#!/bin/bash
case $1 in
"start"){
for i in localhost102 localhost103 localhost104
do
echo "------------$i-------------------"
ssh $i "/opt/module/kafka/bin/kafka-server-start.sh -daemon /opt/module/kafka/config/server.properties"
done
};;
"stop"){
for i in localhost102 localhost103 localhost104
do
echo "------------$i-------------------"
ssh $i "/opt/module/kafka/bin/kafka-server-stop.sh"
done
};;
esac
- kafka命令行操作
# 查看所有命令
bin/kafka-topics.sh --zookeeper localhost102:2181
# 列出所有Topics
bin/kafka-topics.sh --zookeeper localhost102:2181 --list
# 创建Topic
bin/kafka-topics.sh --zookeeper localhost102:2181 --create --topic first --partitions 2 --replication-factor 2
# 删除Topic # delete.topic.enable=true否则只是标记删除
bin/kafka-topics.sh --zookeeper localhost102:2181 --delete --topic first
# 发送消息
bin/kafka-console-producer.sh --broker-list localhost102:9092 --topic first
# 消费消息,0.9版本之后offset存放在kafka里而不是zookeeper,使用bootstrap-server,会在本地生成50个内部topoc,存储offset
bin/kafka-console-consumer.sh --bootstrap-server localhost102:9092 --topic first
bin/kafka-console-consumer.sh --zookeeper localhost102:2181 --topic second --from-beginning
# 查看Topic
bin/kafka-topics.sh --zookeeper localhost102:2181 --describe --topic first
# 修改分区数 只能增加
bin/kafka-topics.sh --zookeeper localhost102:2181 --alter --topic first --partitions 3
Kafka架构深入
Kafka 工作流程及文件存储机制
- 消息追加到.log文件,为防止.log文件过大导致数据定位效率低下,采取分片和索引机制。一个Topic分为多个partition,每个partition分为多个segment(最多1G),每个partition在物理上以topic-partition命名,存储.log和.index文件,通过.index找到每条消息在.log中的索引位置,.index每个索引大小相同,查找速度非常快。Kafka 生产者
-
分区策略
- 分区原因:1> 方便扩展:Partition可以通过调整以适应它所在的机器,而一个topic有多个partition,因此整个集群可以适应任意大小数据。2> 提高并发:可以以partition为单位读写
- 分区原则:会将数据封装为ProducerRecord对象,1> 指明分区,2> 没有指明分区,但是有key,根据key的hash值对分区数取余得到分区值,3> 没有指明分区,没有key,第一次随机选一个分区,以后都是轮询
-
数据可靠性保证
- 为保证producer向partition发送数据能可靠的到达指定topic,producer收到ack之后发送下一轮,否则重发。(异步发送,会一直发送新消息,另外会有一个线程接收ack,ack没收到会重启,)
- 副本同步策略:一个partition的所有副本全部同步才会发送ack,(虽然延迟高,但对kafka影响较小)
- ISR:为避免有个别follower迟迟不能同步造成leader一直等下去,leader维护一个动态的in-sync replica set (ISR),意为与leader同步的follower,replica.lag.time.max.ms设定阈值时间,阈值时间内未与leader同步被踢出ISR,leader发生故障后,从ISR中选举新的leader,(0.9版本以前还有一个条件,是follower和leader同步的条数小于某个阈值被移出ISR,这样容易造成flowever频繁的被移入移出ISR,频繁读写ZK,消息的读写是按批次的,比如一次批量写入leader12条,阈值为10条,那么一开始所有ISR都会被移出)
- ack应答机制:acks参数可以配置0、1、-1,
- 0表示producer不等待ack,leader一接收(写入socket buffer)还没有写入磁盘就返回,故障时有可能丢失数据
- 1表示leader落盘后返回ack,如果follower同步之前发生故障,有可能丢失数据
- -1表示所有follower(ISR)落盘后返回ack,如果follower同步之后,broker返回ack之前发生故障,有可能数据重复,如果ISR只要leader自己,也有可能丢数据
- 故障处理细节:log文件中有HW(消费者能见的最大的offset,队列中最小的offset)和LEO(每个副本最大的offset)两个offset,来保证所有的follower和leader数据是同步的,follower故障后,恢复时读取HW,高于HW的截取掉,然后向leader同步,重新返回ISR。leader故障后,从ISR中选取一个follower为leader,为保证数据的一致性,其他follower截取掉各自高于HW的log文件,然后从新的leader同步。(多退少补)
-
Exactly Once语义
- 0.11版本之后引入幂等性,保证producer向partition写数据时不重复也不丢失,所谓的幂等性就是指 Producer不论向 Server发送多少次重复数据, Server端都只会持久化一条。
- 要启用幂等性,只需要将Producer的参数中 enable.idompotence设置为 true,ack自定为-1
- Producer在初始化的时候会被分配一个 PID,发往同一 Partition的消息会附带 Sequence Number。而Broker端会对 做缓存,当具有相同主键的消息提交时, Broker只会持久化一条。PID重启就会有变化,同时不同的partition也具有不同的主键,所以幂等性无法保证跨会话的exactly once
Kafka消费者
- 消费方式:pull(消费者la),kafka在消费者拉取数据时引入timeout,如果没有数据可供消费会等待一段时间避免一直轮询
- 分区策略:即如何将多个分区分给同一个消费者组下的多个消费者,当消费者的个数变化时都会触发
- 1>RoundRobin 轮询(最大差1个):按组,同一组内所有消费者订阅的topic当成一个整体,所有topic的分区排序后轮询分给各个消费者,若有消费者订阅了其他topic,则有可能组内其他消费者会分到其他topic的分区【只有当消费者组内的消费者消费的topic一样时才有意义,可以更均匀的将不同的分区分配到不同的消费者】(按组分,看所有topic一共有多少分区,排序,划分给消费者,不在同一个组是不同组订阅同一topic,数据复制),(问题:没订阅的消费者可能受到其他订阅者订阅的消息)
- 2> Range(默认):按topic,每个topic内的分区分别划分给订阅的消费者,(问题:消费者消费的分区有可能会越来越不对等)
- offset 的维护:0.9版本之前消费者的offset存在ZK,之后存在kafka一个内置的topic中__consumer_offsets,存储的是group+topic+partition的offset。存在kafka是按kv存入内部的topic的,共50个【这个topic默认的ack为-1】
消费者组案例
一个分区只能由一个消费者消费,当消费者个数变化是,分区重新分配给消费者组个各个消费者
Kafka高效读数据
- 顺序写磁盘:写的过程一直追加到文件末端
- 零复制技术:使用内核控制,不经过上层user space
ZK在Kafka中的作用
Kafka 集群中有一个broker 会被选举为Controller,负责管理集群broker 的上下线,所有topic 的分区副本分配和leader 选举等工作。Controller 的管理工作都是依赖于Zookeeper 的
Kafka 事务
Kafka 从0.11 版本开始引入了事务支持。事务可以保证Kafka 在Exactly Once 语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败。
- Producer事务:为了实现跨分区跨会话的事务,需要引入一个全局唯一的Transaction ID,并将 Producer获得的 PID和 Transaction ID绑定。这样当 Producer重启后就可以通过正在进行的 Transaction ID获得原来的 PID。
- Consumer事务:对于 Consumer而言,事务的保证就会相对较弱,尤其时无法保证Commit的信息被精确消费。这是由于 Consumer可以通过offset访问任意信息,而且不同的 Segment File生命周期不同,同一事务的消息可能会出现重启后被删除的情况。(默认segment存储7天,如果跨segment,有可能被删除)
Kafka API
Producer API
- Producer发送消息使用异步发送,发送过程中涉及到两个线程——main线程和sender线程,以及线程共享变量RecordAccumulator,main线程发送给RecordAccumulator,sender线程不断从RecordAccumulator拉数据发送到broker
- main线程:Producer -- Interceptor -- Serializer -- Partitioner,然后写入RecordAccumulator
- 异步发送API(见代码)
- 同步发送API,main线程和sender线程同步,使用send().get()方法,send方法返回future对象,调用get会阻塞main线程
Consumer API
- 自动提交offset:1 开启自动提交 2 自动提交时间间隔
- 手动提交offset:commitSync(同步提交),commitAsync(异步提交),都有可能造成数据漏消费或者重复消费
- 自定义存储offset:借助ConsumerRebalanceListene,可以结合数据处理和offset提交组成一个事务,将自定义的offset存储在数据库中
自定义拦截器
实现ProducerInterceptor接口
Kafka监控
Kafka Eagle
Flume对接Kafka
进行数据分类时,可以使用拦截器在source中添加key为topic的value在同一个channel中,sink为kafka,那么在kafka中会自动根据topic进行分类
面试题
代码
- 生产者
/**
* 生产者
* created by Bingmous on 2021/6/28 19:49
*/
public class ProducerTest {
public static void main(String[] args) {
//1 创建kafka生产者的配置信息,所有配置都在这个类中ProducerConfig
Properties props = new Properties();
// props.put("bootstrap.servers","10.194.227.212:9092"); //连接的kafaka集群
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092"); //连接的kafaka集群
props.put("acks","all"); //应答级别
props.put("retries",3); //重试次数
props.put("batch.size",16384); //默认批次大小,达到这个大小才提交给kafka
props.put("linger.ms",1); //默认等待时间,不管批次大小多少,达到这个等待时间就提交,避免数据太小没有达到批次大小而没有提交
props.put("buffer.memory",33554432); //默认RecordAccumulator缓冲区大小,数据先提交到这里,main进程和send进程通过这个对象联系
props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer"); //RecordAccumulator缓冲区大小,数据先提交到这里
props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer"); //RecordAccumulator缓冲区大小,数据先提交到这里
//2 创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
//3 发送数据(异步发送)
for (int i = 0; i < 10; i++) {
var producerRecord = new ProducerRecord<String, String>("mytest",String.valueOf(i),"data-"+i); //不同的数据发送方式:有无key
producer.send(producerRecord); //发送数据
// //同步发送(很少用)
// Future<RecordMetadata> future = producer.send(producerRecord);
// try {
// RecordMetadata recordMetadata = future.get(); //future对象调用get方法阻塞当前进程并返回进程的返回值
// } catch (InterruptedException e) {
// e.printStackTrace();
// } catch (ExecutionException e) {
// e.printStackTrace();
// }
}
//4 关闭资源
producer.close(); //清空内存,即使没有达到1ms也会将数据提交
}
}
- 生产者,带回调函数
/**
* 生产者,带回调函数
* created by Bingmous on 2021/6/29 10:09
*/
public class CallBackProducerTest {
public static void main(String[] args) {
//1 创建kafka生产者的配置信息,所有配置都在这个类中ProducerConfig
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
//2 创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
//3 发送数据
for (int i = 0; i < 10; i++) {
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("mytest", "data--" + i);
producer.send(producerRecord, (metadata, e) -> {
if (e == null){
System.out.println(metadata.topic()+"-" + metadata.partition()+"-"+metadata.offset());
}
});
}
//4 关闭资源
producer.close();
}
}
- 生产者,使用自定义分区器
/**
* 生产者,使用自定义分区器
* created by Bingmous on 2021/6/29 11:08
*/
public class PartitionProducerTest {
public static void main(String[] args){
//1 创建kafka生产者的配置信息,所有配置都在这个类中ProducerConfig
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.bingmous.partitioner.MyPartitioner");
//2 创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
//3 发送数据
for (int i = 0; i < 10; i++) {
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("mytest", "mypartitionerdata--" + i);
producer.send(producerRecord, (metadata, e) -> {
if (e == null){
System.out.println(metadata.topic()+"-" + metadata.partition()+"-"+metadata.offset());
}
});
}
//4 关闭资源
producer.close();
}
}
- 生产者,使用拦截器
```java
/**
* 生产者,使用拦截器
* created by Bingmous on 2021/6/29 14:53
*/
public class IntercetorProducerTest {
public static void main(String[] args) {
//1 创建kafka生产者的配置信息,所有配置都在这个类中ProducerConfig
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
ArrayList<String> list = new ArrayList<>();
list.add("com.bingmous.interceptor.TimeInterceptorTest");
list.add("com.bingmous.interceptor.CountInterceptorTest");
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,list);
//2 创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
//3 发送数据
for (int i = 0; i < 10; i++) {
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("mytest", "interceptorProducerdata--" + i);
producer.send(producerRecord, (metadata, e) -> {
if (e == null){
System.out.println(metadata.topic()+"-" + metadata.partition()+"-"+metadata.offset());
}
});
}
//4 关闭资源
producer.close();
}
}
- 分区器,发往kafka的数据如何进行分区
```java
/**
* 分区器,发往kafka的数据如何进行分区
* created by Bingmous on 2021/6/29 10:56
*/
public class MyPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//自定义分区逻辑
List<PartitionInfo> partitionInfos = cluster.availablePartitionsForTopic(topic); //通过Cluster拿到元信息
int size = partitionInfos.size(); //总的分区数
return 1; //返回分区号
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
- 拦截器
/**
* 拦截器
* created by Bingmous on 2021/6/29 14:39
*/
public class TimeInterceptorTest implements ProducerInterceptor {
//被拦截数据
@Override
public ProducerRecord onSend(ProducerRecord record) {
ProducerRecord producerRecord = new ProducerRecord<>(record.topic(),record.partition(),record.key(),System.currentTimeMillis() + ","+record.value());
return producerRecord;
}
//在数据从RecordAccumulator成功发送到kafka broker或者发送过程失败时调用,通常在producer回调逻辑触发前
//运行在producer的IO线程中,不要在该逻辑中放入很重的逻辑,否则拖慢producer的消息发送效率
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
//关闭interceptor,主要用于执行一些清理工作
//interceptor可能运行在多个线程中,在具体实现时需要自行确保线程安全,若指定了多个拦截器,则producer按顺序调用他们,
//并仅仅捕获每个interceptor可能发生的异常记录到错误日志而非向上传递
@Override
public void close() {
}
//获取配置信息和初始化数据,拦截前调用
@Override
public void configure(Map<String, ?> configs) {
}
}
- 消费者
/**
* 消费者
* created by Bingmous on 2021/6/29 12:25
*/
public class ConsumerTest {
public static void main(String[] args) {
//1 创建消费者配置信息
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092"); //连接的集群
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true); //自动提交offset,存储到kafka,只在启动时去内部的topic读取,关闭时才提交,其他的时间是存在内存中的
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,1000); //自动提交的间隔时间
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer"); //key,value的反序列化
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer"); //key,value的反序列化
props.put(ConsumerConfig.GROUP_ID_CONFIG,"testgroup"); //消费者组
// props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest"); //重置消费者的offset,只有当组第一次消费或上次消费的数据不存在offset失效后才生效
//2 创建消费者
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("mytest","mytest2")); //订阅topic
//自定义offset,分区有可能变化,需要自定义存储offset
// consumer.subscribe(Arrays.asList("mytest","mytest2"), new ConsumerRebalanceListener() {
// //该方法会在 Rebalance 之前调用
// @Override
// public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
//
// }
//
// //该方法会在 Rebalance 之后调用
// @Override
// public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
//
// }
// });
while (true) { //持续拉取数据
//3 获取数据
ConsumerRecords<String, String> records = consumer.poll(100); //拉到空数据的超时时间
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.topic()+"-"+record.partition() + "-" + record.key() + "-" + record.value());
}
//手动提交offset,需要关闭自动提交(ENABLE_AUTO_COMMIT_CONFIG,false)
// consumer.commitSync(); //同步提交,当前线程会阻塞,直到offset提交成功
// consumer.commitAsync(new OffsetCommitCallback() {//异步提交成功或失败调用的方法
// @Override
// public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
// if (exception!=null) {
// System.err.println("commit failed for " + offsets);
// }
// }
// });
}
// consumer.close();
}
}
---
本文来自博客园,作者:Bingmous,转载请注明原文链接:https://www.cnblogs.com/bingmous/p/15643706.html