kafka模式
1.点对点 (一对一,消费者主动拉取数据,消息收到后清除)
producer--> topic---> consume收到--->确认---->消息队列删除
2.发布订阅模式(一对多,消费者消费之后不会清除消息)
将消息发送到topic 中,同时有多个消费者消费,和点对点不同,发布到topic的消息会被所有订阅者消费。
producer-->publish-------》topic.------>多个consume
kafka架构(某一个分区只能被同一个组里的一个消费者消费)
Producer:生产者,负责将客户端生产的消息发送到 Kafka 中,可以支持消息的异步发送和批量发送;
broker:服务代理节点,Kafka 集群中的一台服务器就是一个 broker,可以水平无限扩展,同一个 Topic 的消息可以分布在多个 broker 中;
Consumer:消费者,通过连接到 Kafka 上来接收消息,用于相应的业务逻辑处理。
ZooKeeper:给第三方提供服务
Consumer Group:消费者组,指的是多个消费者共同组成一个组来消费一个 Topic 中的消息
)Topic 与 Partition
在 Kafka 中消息是以 Topic 为单位进行归类的,Topic 在逻辑上可以被认为是一个 Queue,Producer 生产的每一条消息都必须指定一个 Topic,然后 Consumer 会根据订阅的 Topic 到对应的 broker 上去拉取消息。
为了提升整个集群的吞吐量,Topic 在物理上还可以细分多个分区,一个分区在磁盘上对应一个文件夹。由于一个分区只属于一个主题,很多时候也会被叫做主题分区(Topic-Partition)。
2)Leader 和 Follower
一个分区会有多个副本,副本之间是一主(Leader)多从(Follower)的关系,Leader 对外提供服务,这里的对外指的是与客户端程序进行交互,而 Follower 只是被动地同步 Leader 而已,不能与外界进行交互。
当然了,你可能知道在很多其他系统中 Follower 是可以对外提供服务的,比如 MySQL 的从库是可以处理读操作的,但是在 Kafka 中 Follower 只负责消息同步,不会对外提供服务。
Kafka 多副本机制
Kafka 为分区引入了多副本机制,同一分区的不同副本中保存的信息是相同的,通过多副本机制实现了故障的自动转移,当集群中某个 broker 失效时仍然能保证服务可用,可以提升容灾能力。
ps:0.9版本之前的offset存储在zookeeper,0.9版本之后的存储在本地,kafka本地,kafka存本地存储7天由系统创建,记录消费位置,便于挂掉之后重新启动。放在zookeeper里并发过高。
kafka的topic是逻辑上的概念,partion是物理上的概念 kafka默认保持七天,.log文件存的是数据,index文件是索引,分片大小默认为1G
由于生产者生产的消息会不断追加到log文件的末尾,为防止log文件过大导致数据定位效率低下,kafka采取了分片和索引机制,将每个partion分为多个segment,每个segment对应两个文件index和log文件,这些文件位于同一个文件夹下,该文件的命名规则为topic名称+分区序号。如first这个topic有三个分区,则其对应的文件夹为first-0,first-1,first-2,index和log文件以当前segment的第一条消息的offset命名
分区策略:
1.分区原因:方便在集群中拓展,每个partion可以通过调整以适应它所在的机器,而一个topic又可以有多个partion组成。
2.分区原则:
我们需要将生产者producer发送的数据封装成一个producerRecord对象
1.指明partion的情况下,直接将指明的值作为partition的值
2.没有指明partion的情况下,但是有key,将key的hash值与topic的partition数取余得partition值
3.即没有partition值又没有key的情况下,第一次调用时随机生成一个随机整数,后面每次调用在这个整数上自增,将这个值与topic可用的partition总数取余得到partition值,也就时长说的round-robin算法
生产者 数据可靠性:
topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement)确认收到。如果producer收到ack就会进行下一轮的发送,否则就重新发送数据
何时发送ack:
确保有follower与leader同步完成,leader再发送ack,这样才能保证leader挂掉之后,能在follower中选举出新的leader
副本数据同步策略:
方案:
1.半数以上同步完成 优点:延迟低 缺:选举新的leader时,容忍n台节点的故障,需要2n+1个副本,会造成大量的数据冗余
2.全部同步完成才发送 优点:选举新的leader时,容忍n台节点的故障,需要n+1个副本 缺延迟高 网络延迟高,但是网络延迟对kafka的影响较小
ISR: in-sync replica set 意为和leader保持同步的follower集合。当ISR中的follower完成数据的同步之后,leader就会向follower发送ack,如果长时间未向leader同步数据,则将该follower踢出ISR,该时间由replica.lag.time.max.ms参数设定。Leader发生故障之后,就会从ISR中选举新的leader
为什么踢出同步条数:频繁操作zookeeper,批次发送的大小超出了同步条数,会频繁的将follower剔除和拉进ISR,频繁的操作ISR和ZK
ACK应答机制:
对于某些不太重要的数据,对数据的可靠性要求不高的,能够容忍数据的少量丢失,所以没必要等ISR中的follower全部接受成功
ack参数配置:
acks:
0:producer不等待broker的ack,这一操作提供了最低的延迟,broker一接受到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据
1:producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据
-1 or all :producer等待broker的ack,partition的leader和ISR里的follower全部落盘成功后才返回ack,但是如果在follower同步完成后,broker发送ack之前,leader发生故障,会导致数据重复
Log文件中的HW和LEO
HW: HIGH WATEMARK. 所有副本中最小的LEO ,指的是消费者能见到的最大的offfset,IS对列中最小的LEO ,HW之前的数据才对消费者可见(木桶短板)
LEO:每个副本最后的一个offset(LOG END OFFSET)
可以保证数据一致性,但是不能保证数据不丢失
数据一致性:
1.follower发生故障后会被淋湿踢出ISR,待follower恢复后,follower会读取本地磁盘上次记录的HW,并将log文件高于Hw的部分截取掉,从hw开始向leader进行同步,等该follower的LEO大于等于该partition的HW就可以重新加入ISR了
2.Leader发生故障后,会从ISR中选出一个新leader,为保证多个副本之间的数据一致性,其余的follower会先将自己的log文件高于hw的部分截取掉,然后重新同步数据
Exactly Once语义:
将服务起的ack级别设置为-1,可以保证producer和server之间不会丢失数据。即At Least Once ,将ack的级别设置为0,可以保证生产者每条消息只会被发送一次 即At Most Once 语义。
at Least Once可以保证数据不丢失,但不能保证数据不重复,at most once可以保证数据不重复,但是不能保证数据不丢失,但是对于一些重要的信息,下游数据要求既不丢失也不重复,这就需要幂等性 不管生产者向Server发送了多少次重复数据,Server都只会吃酒话一条 ,即 At Least Once + 幂等性 = Exactly once
要启用幂等性,只要将producer的参数中的 enable.idompotence 设置为true即可
实现其实是将原来下游要做的去重放在了上游,开启幂等性的producer在初始化的时候会被分配一盒pid,发往同一partition的消息会附带Sequence Number .而broker端会对<PID ,Partition,SeqNumber> 做缓存,当具有相同主键的消息提交时,Broker只会持久化一条
但是PId重启就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区会话的Exactly Once。
kafka消费:
消费者采用pull拉取数据,但是这样会导致kafka没有数据时会陷入循环中,一直返回空数据。针对这一情况,kafka的消费者在消费数据的 时候会传入一个时长参数timeout,如果当前没有数据可供消费,消费者会等待一段时间再返回,这段时间即为timeout
分区分配策略:
一个消费者组中多个消费者,一个topic有多个partition,所以必然会设计到partition的分配问题,即确定哪个partition由哪个cosumer消费
kafka有两种分配策略,一个是轮询(RoundRobin) ,多个消费者消费的数据不会差别过大,一个是Range
offset的维护:
由于offset在消费过程中可能会出现断电等故障,消费者恢复后,需要从之前的饿位置继续消费,所以消费者需要实时记录自己消费到了哪个offset,以便恢复后继续消费
(组+主题+partition分区)=决定唯一的offset
kafka 0.9版本之前,consumer默认将offset保存在zookeeper中,从0.9版本之后,consumer默认将offset保存在kafka一个内置的topic中,该topic为_consumer_offsets
kafka高效读写数据:
1.顺序写磁盘
生产者的文件要写到log文件中,写的过程就是一直追加到文件末端为顺序写,官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100k/s。与磁盘的机械结构有关,顺序写之所以快,是因为其省去了大量的磁头寻址的时间。
2.零复制技术
3.分布式,可以并发读写
kafka事务:
Producer事务
为了实现跨分区会话的事物,需要因为一个全局的Transaction,并将Producer或得的PId和TransactionID绑定,这样当Producer重启后就可以通过正在进行的TransactionID获得原来的PID
为了管理Transaction,kafka引入新组建Transaction Coordinator,Producer就是通过和Transaction Coordinator 交互或得 TransactionID对应的任务状态,Transaction Coordinator还负责将事务写入kafka的一个内部的Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复从而继续进行
Consumer事务:
事务的保证较弱,尤其是无法保证提交的信息被精确消费,这是由于consumer可以通过offset访问任何信息,而且不同的片段文件(segmentFile)生命周期不同,同一事务的消息可能会重启后被删除的情况。
消息发送流程:
生产者发消息采用的是异步发送,main将消息发送给RecordAccumulator
Sender 线程
发送流程:
相关参数:
batch.size :只有数据积累到batch.size后。sender才会发送数据
linger.ms : 如果数据迟迟未到batch.size,sender等待linger.time之后就会发送数据
kafka生产者API
异步发送 API 1)导入依赖 <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>0.11.0.0</version> </dependency> 2)编写代码 需要用到的类: KafkaProducer:需要创建一个生产者对象,用来发送数据 ProducerConfig:获取所需的一系列配置参数 ProducerRecord:每条数据都要封装成一个 ProducerRecord 对象 1.不带回调函数的 API public class CustomProducer { public static void main(String[] args) throws ExecutionException,InterruptedException { Properties props = new Properties(); //kafka 集群,broker-list props.put("bootstrap.servers", "hadoop102:9092"); props.put("acks", "all"); //重试次数 props.put("retries", 1); //批次大小 props.put("batch.size", 16384); //等待时间 props.put("linger.ms", 1); //RecordAccumulator 缓冲区大小 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(); } } 2.带回调函数的 API 回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是RecordMetadata 和 Exception,如果 Exception 为 null,说明消息发送成功,如果Exception 不为 null,说明消息发送失败。 注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。 public class CustomProducer { public static void main(String[] args) throws ExecutionException, InterruptedException { 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)), 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 方发即可。 public class CustomProducer { public static void main(String[] args) throws ExecutionException, InterruptedException { 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(); } }
在Spring中封装了对应的操作,可以利用配置文件来简化操作
server: port: 8081 spring: application: name: eureka-server kafka: bootstrap-servers: 1.15.114.101:9092 #https://kafka.apache.org/documentation/#producerconfigs producer: bootstrap-servers: 1.15.114.101:9092 # 可重试错误的重试次数,例如“连接错误”、“无主且未选举出新Leader” retries: 1 #生产者发送消息失败重试次数 # 多条消息放同一批次,达到多达就让Sender线程发送 batch-size: 16384 # 同一批次内存大小(默认16K) # 发送消息的速度超过发送到服务器的速度,会导致空间不足。send方法要么被阻塞,要么抛异常 # 取决于如何设置max.block.ms,表示抛出异常前可以阻塞一段时间 buffer-memory: 314572800 #生产者内存缓存区大小(300M = 300*1024*1024) #acks=0:无论成功还是失败,只发送一次。无需确认 #acks=1:即只需要确认leader收到消息 #acks=all或-1:ISR + Leader都确定收到 acks: 1 key-serializer: org.apache.kafka.common.serialization.StringSerializer #key的编解码方法 value-serializer: org.apache.kafka.common.serialization.StringSerializer #value的编解码方法 #开启事务,但是要求ack为all,否则无法保证幂等性 #transaction-id-prefix: "COLA_TX" #额外的,没有直接有properties对应的参数,将存放到下面这个Map对象中,一并初始化 properties: #自定义拦截器,注意,这里结尾时classes(先于分区器,快递先贴了标签再指定地址) #interceptor.classes: cn.com.controller.TimeInterceptor #自定义分区器 #partitioner.class: com.alibaba.cola.kafka.test.customer.inteceptor.MyPartitioner #即使达不到batch-size设定的大小,只要超过这个毫秒的时间,一样会发送消息出去 linger.ms: 1000 #最大请求大小,200M = 200*1024*1024,与服务器broker的message.max.bytes最好匹配一致 max.request.size: 209715200 #Producer.send()方法的最大阻塞时间(115秒) # 发送消息的速度超过发送到服务器的速度,会导致空间不足。send方法要么被阻塞,要么抛异常 # 取决于如何设置max.block.ms,表示抛出异常前可以阻塞一段时间 max.block.ms: 115000 #该配置控制客户端等待服务器的响应的最长时间。 #如果超时之前仍未收到响应,则客户端将在必要时重新发送请求,如果重试次数(retries)已用尽,则会使请求失败。 #此值应大于replica.lag.time.max.ms(broker配置),以减少由于不必要的生产者重试而导致消息重复的可能性。 request.timeout.ms: 115000 #等待send回调的最大时间。常用语重试,如果一定要发送,retries则配Integer.MAX #如果超过该时间:TimeoutException: Expiring 1 record(s) .. has passed since batch creation delivery.timeout.ms: 120000 # 生产者在服务器响应之前能发多少个消息,若对消息顺序有严格限制,需要配置为1 # max.in.flight.requests.per.connection: 1 eureka: instance: hostname: local client: fetch-registry: true #????????????????????????? register-with-eureka: true #??????????????????????? service-url: defaultZone: http://xushuai:123456@localhost:8083/eureka/ server: enable-self-preservation: true #??????
代码如下
@RestController public class Controller { @Autowired KafkaTemplate kafkaTemplate; @GetMapping("/send") public String sendKafka(){ for (int i = 0 ;i < 100 ;i++){ kafkaTemplate.send("first", "你好"+i); } kafkaTemplate.destroy(); return "ok"; } }
可以在kafka-map中看到消息
简单消费者:
添加消费者的配置
consumer:group-id: 1
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
利用注解监听kafka package com.example.springclouddemo.linstener; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; /** * @author xushuai * @date 2022年12月20日 16:14 */ @Service public class KafkaListener { @org.springframework.kafka.annotation.KafkaListener(topics = {"first"}) public void handlerMsg(ConsumerRecord<String,String> consumerRecord){ System.out.println("===接收到的消息:==="+consumerRecord.value()+"===消息偏移量==="+consumerRecord.offset()); } }
结果如下
想要消费者重新消费
1.改消费者组(同一个消费者组只能消费一次)
2. 改消费策略 auto.offset.reset
auto.offset.reset 参数定义了当无法获取消费分区的位移时从何处开始消费。例如:当 Broker 端没有 offset(如第一次消费或 offset 超过7天过期)时如何初始化 offset,当收到 OFFSET_OUT_OF_RANGE 错误时如何重置 Offset。
auto.offset.reset 参数设置有如下选项:
earliest:表示自动重置到 partition 的最小 offset。
latest:默认为 latest,表示自动重置到 partition 的最大 offset。
none:不自动进行 offset 重置,抛出 OffsetOutOfRangeException 异常
auto.offset.reset=none时
不希望发生 offset 自动重置的情况,因为业务不允许发生大规模的重复消费。
注意
此时消费组在第一次消费的时候就会找不到 offset 而报错,这时就需要在 catch 里手动设置 offset。
offset不提交,会导致一直重复消费
配置消费者(配置ENABLE_AUTO_COMMIT_CONFIG为 true 配置自动提交)
enable.auto.commit 的默认值是 true;就是默认采用自动提交的机制。
auto.commit.interval.ms 的默认值是 5000,单位是毫秒。
自动提交是基于时间提交的,开发人员难以把握offset的提交时机
手动提交的方法有两种commitSync(同步提交) 和commitAsync(异步提交)
两者的共同点都会将本次poll的一批数据最高的偏移量提交,
同步提交线程会阻塞直到offset提交成功并且会自动失败重试,除了不可控因素,也会提交失败,异步则没有失败重试机制,所以可能会提交失败
缺点:都会重复数据
自定义存储offset:
当有新的消费者加入消费者组,已有的消费者退出消费者组或者所订阅的主题的分区发生变化,就会触发到分区的重新分配,重新分配的过程就叫做Rebalance
消费者发生Rebalance之后,每个消费者消费的分区就会发生变化,因此消费者首先获取到自己被重新分配的分区,并且定位到每个分区最近提交的offset位置继续消费
要实现自定义存储offset,需要借助ConsumerRebalanceListener
API自定义拦截器:
定义的方法包括:
1.configure(configs)
获取配置信息和初始化数据时调用
2.onSend(ProducerRecord)
该方法封装进KafkaProducer.send方法里,即它运行在用户主线程中,Producer确保在消息被序列化以及计算分区前调用该方法,用户可在该方法中对消息做任何操作。但是最好不要修改消息所属的topic和分区,否则会影响目标分区的计算
3.onAcknowledgement(RecordMetadata,Exception)
该方法会在消息从RecordAccumulator成功发送到kafkaBroker之后,或者发送过程中失败时调用,并且通常都是在producer回调逻辑触发之前。onAcknowledgement运行在producer的IO线程中,因此不要在该方法中放入很重的逻辑,否则会拖慢producer的发送效率
4.close
关闭interceptor,主要用于执行一些资源清理的工作
interceptor可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全,另外倘若指定了多个interceptor,则producer将按照指定顺序调用他们,并仅仅是捕获每个interceptor可能抛出的异常记录到错误日志中而非在向上传递
需要去实现ProducerInterceptor
加入拦截器的配置 :
package com.example.springclouddemo.intercepter; import org.apache.kafka.clients.producer.ProducerInterceptor; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; import org.springframework.stereotype.Component; import java.util.Map; /** * @author xushuai * @date 2022年12月21日 09:59 */ @Component public class TimeInterceptor implements ProducerInterceptor<String,String> { Integer success=0; Integer error=0; @Override public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) { //取出具体数据的value String value = producerRecord.value(); //创建和新的 ProducerRecord对象并且返回 return new ProducerRecord<String,String>(producerRecord.topic(), producerRecord.partition(), producerRecord.key(), System.currentTimeMillis()+"==="+value); } @Override public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) { if (recordMetadata != null){ success++; }else { error++; } } @Override public void close() { System.out.println("success"+success); System.out.println("error"+error); } @Override public void configure(Map<String, ?> map) { } }
消息的内容带上时间戳
打印成功和失败的条数