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 工作流程及文件存储机制

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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?