kafka

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) {

    }
}

消息的内容带上时间戳

打印成功和失败的条数 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值