kafka基本知识(安装,介绍),主题与分区,消息存储位置,kafka集群,java消息发送和消息,spring使用kafka,kafka优化(消息丢失,重复消费,消息积压,延时队列等)

一 为什么要使用消息队列

1.使用同步的通信方法来解决多个服务之间的通信

"/>

同步的通信方式存在性能和稳定的问题

2.使用异步通信

针对于同步通信方法来说,异步可以让上游快速成功,极大提高了系统的吞吐量.而且在分布式系统中,通过下游多个服务的分布式事务的保障,也能保障业务执行之后的最终一致性.

消息队列解决的最终问题就是----通信问题!

二 消息队列的流派

三、Kafka的基本知识

1.Kafka的安装

  • 部署一台zookeeper服务器

  • 安装jdk

  • 下载kafka安装包(http://kafka.apache.org/downloads)

  • 部署kafka /usr/local/kafka

    进入config目录,修改server.properties(h)

  • 进入bin目录.执行命令启动kafka服务器

    ./kafka-server-start.sh -daemon ../config/server.properties
    
  • 校验kafka是否启动成功 :

    进入zk内查看是否有kafka的节点: /brokers/ids/0

    在zk的bin目录下
    ./zkCli.sh
    

2.kafka中的一些基本概念

kafka是重topic,而topic是一个逻辑上的概念,目的就是区分消息的类别,不同消息到不同的topic,再到相应的队列!

名称解释
Broker消息处理的中间件节点,一个kafka节点就是一个broker,一个或者多个broker就可以组成一个kafka集群
Topickafka通过topic对消息进行归类,发布到kafka的每条消息都需要指定一个topic
Producer消息生产者,向broker发送消息的客户端
Consumer消息消费者,向broker读取消息的客户端

3.创建topic

向zookeeper创建topic,replication-factor副本,partitions分区

./kafka-topics.sh --create --zookeeper 10.12.156.160:2181 --replication-factor 1 --partitions 1 --topic test

查看当前zk内有哪些topic

./kafka-topics.sh  --list --zookeeper 10.12.156.160:2181

4.发送消息

kafka自带一个producer和consumer,可以从本地文件中读取内容,或者我们也可以在命令行中直接输入内容,并将这些内容以消息的形式发送到kafka集群.默认情况下,每一行都会做成一个独立的消息.

使用kafka的发送消息的客户端,指定发送到的kafka的服务器地址和topic

./kafka-console-producer.sh --broker-list 10.12.156.247:9092 --topic test

5.消费消息

利用自带的consumer,会将获取到的内容在命令行中进行输出,默认是最新的消息.使用kafka的消费者客户端,从指定kafka服务器的指定的topic中消费消息

  • 方式一: 从最后一条消息的偏移量+1开始
./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --topic test
  • 方式二: 从头开始消费
./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --from-beginning --topic test

6.关于消息消费的细节

  • 生产者将消息发送给broker,broker会将信息保存在本地的日志文件中

    /usr/local/kafka/data/kafka-logs/主题-分区/0000000.log
    
  • 消息的保存是有序的, 通过offset偏移量来描述消息的有序性

  • 消费者消费消息时也是通过offset来描述当前要消费的那条消息的位置

7.单播消息

在一个kafka的topic中,启动两个消费者,一个生产者,问:生产者发送消息,这条信息是否同时会被两个消费者消费?

如果多个消费者在同一个消费组,那么只有一个消费组可以收到订阅的topic中的信息.换言之,同一个消费组中只能有一个消费者收到一个topic中的信息.

./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --consumer-property group.id=testGroup --topic test

8.多播消息

不同的消费者订阅同一个topic,那么不同的消费组中只有一个消费者收到消息.实际上也是多个消费组中的多个消费组收到了同一个消息

消费组1

./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --consumer-property gruop.id=testGroup1 --topic test

消费组2

./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --consumer-property gruop.id=testGroup2 --topic test

多播和单播的区别

9.查看消费组的详细信息

通过以下命令可以查看到消费组的详细信息

查看消费组列表

./kafka-consumer-groups.sh --bootstrap-server 10.12.156.247:9092 --list

消费组的详细描述

./kafka-consumer-groups.sh --bootstrap-server 10.12.156.247:9092 --describe --group testGroup

重点关注以下几个信息:

  • current-offset:最后被消费的消息的偏移量
  • log-end-offset:消息总量(最后一次消息的偏移量)
  • lag:积压的消息,未被消费的消息

四、主题和分区

1.主题topic

主题topic在kafka中是一个逻辑的概念,kafka通过topic将消息进行分类.不同的topic会被订阅该topic的消费者消费.实现逻辑上对消息的划分.

但是有一个问题,如果说某个topic中的消息非常非常多,多到需要几T来存,因为消息会被保存代log日志文件中.为了解决这这文件过大的问题,kafka提出了Partition分区的概念.

2.分区Partition

2.1分区的概念

通过partition将一个topic中的信息分区来存储.这样的好处有多个:

  • 分区存储,可以解决统一存储文件过大的问题
  • 提高了读写的吞吐量: 读和写可以并发执行

2.2创建多分区的主题

./kafka-topic.sh --create --zookeeper 10.12.156.160:2181 --replication-factor 1 --partitions 2 --topic test1

3.kafka中消息日志文件中保存的内容

  • 0000000.log: 这个文件保存的就是消息

  • _consumer_offsets_49:

    kafka内部自己创建了_consumer_offsets主题包含了50个分区(可以通过offsets.topic.num.partitions设置).这个主题用来存放消费者消费某个主题的偏移量.因为每个消费者都会维护着消费的主题的偏移量,也就是说每个消费者都会把消费的主题的偏移量自主上报给kafka中的默认主题:_ __consumer_offset. 因此kafka为了提升这个主题的并发性,默认设置了50个分区.

    • 至于提交到那个分区: 通过hash函数: hash(consumerGroupId)%__consumer_offsets计算出
    • 提交到主题的内容是: key是consumerGroupId+topic+分区号,value就是当前offset的值
  • 文件中保存的消息,默认保存7天.七天后自动删除消息.

五、kafka集群操作

1.搭建kafka集群(三个broker)

  • 创建三个server.properties文件

    # 0   1   2
    broker.id = 2
    # 9092  9093   9094
    listerners=PLAINTEXT://172.16.253.38:9092
    # kafka-logs-1 kafka-logs-2 kafka-logs-3
    log.dir=/usr/local/data/kafka-logs-1
    
  • 通过命令启动三台broker

    ./kafka-server-start.sh -daemon ../config/server0.properties
    ./kafka-server-start.sh -daemon ../config/server1.properties
    ./kafka-server-start.sh -daemon ../config/server2.properties
    
  • 校验是否启动成功

    进入到zk中查看/brokers/ids中是否有三个znode(0,1,2)

2.副本的概念

在创建主题时,除了指明主题的分区数以外,还指明了副本数,那么副本是一个什么概念呢?

副本是为了给主题中的分区创建多个备份,多个副本在kafka集群的多个broker中,会有一个副本作为leader,其他是follower.

./kafka-topics.sh --create --zookeeper 10.12.156.160:2181 --replication-factor 3 --partitions 2 --topic my-replicated-topic 
  • leader: kafka的读和写都发生在leader上.leader负责把数据同步给follower.当leader挂了,经过主从选举,从多个 follower中选举产生一个新的leader
  • follower:接收leader的同步的数据
  • isr: 可以同步和已同步的节点会被存入到isr集合中. 细节:如果isr中的节点性能较差,会被踢出isr集合

此时,broker、主题、分区、副本这些概念梳理完成

集群中有多个broker,创建主题时可以指定主题游多个分区(把消息拆分到不到的分区中存储),可以为分区创建多个副本,不同的副本存放在不同的broker中

3.kafka集群收发消息

发消息

./kafka-console-producer.sh --broker-list 10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094 --topic my-replicated-topic

收消息

./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094 --from-beginning --topic my-replicated-topic

指定消费组消费消息

./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094 --from-begining --consumer-property group.id=testGroup1 --topic my-replicated-topic

关于分区分消费组的集群消费信息

  • 一个partition只能被一个消费组的一个消费者消费(保证了局部消息消费的顺序性,也就是说多个partition的多个消费者消费的总的顺序性不能被保证,如何做到总的顺序性?)
  • partition的数量决定了消费组中消费者的数量,建议同一个消费组中消费者的数量不超过partition的数量,否则多的消费者消费不到消息
  • 如果消费者挂了,那么会出发rebalance机制,会让其他消费者来消费该分区

六、kafka的Java客户端的连接

1.引入依赖

<dependency>
     <groupId>org.apache.kafka</groupId>
     <artifactId>kafka-clients</artifactId>
     <version>2.4.1</version>
</dependency>

2.生成者发送消息的基本实现

public class MySimpleProvider {

    private final static String TOPIC_NAME = "my-replicated-topic";

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //1.设置参数
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094");

        //把发送的key从字符串序列化为字节数组
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //把发送的value从字符串序列化为字节数组
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //2.创建生产消息的客户端,传入参数
        KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);

        //3.创建消息
        //key决定当前消息发送到那个分区(hash(key)%分区数),value就是消息的内容
        ProducerRecord<String,String> producerRecord = new ProducerRecord<>(TOPIC_NAME,"MyKey","kafka java client");


        //4.发送消息,得到消息发送的数据并输出
        RecordMetadata metadata = kafkaProducer.send(producerRecord).get();
        System.out.println("同步方式发送结果:"+"topic-"+metadata.topic()+" | partition-"+metadata.partition()
        + " | offset-"+metadata.offset());
    }
}

3.创建消息的构造方法

3.1指定发送的分区

ProducerRecord<String,String> producerRecord = new ProducerRecord<>(TOPIC_NAME,0,"MyKey","kafka java client");

4.生产者同步发送消息

如果生产者接收不到消息的确认ack,生产者就会阻塞,阻塞时间是3s.如果还没有收到消息,会进行重试,重试次数是3次.

//4.发送消息,得到消息发送的数据并输出
        RecordMetadata metadata = kafkaProducer.send(producerRecord).get();
        System.out.println("同步方式发送结果:"+"topic-"+metadata.topic()+" | partition-"+metadata.partition()
        + " | offset-"+metadata.offset());

5.生产者异步发送消息

异步发送,生成者发送完消息后就可以执行之后的业务,broker在收到消息后异步调用生产者提供的callback回调方法

//5.异步发送消息
        kafkaProducer.send(producerRecord, new Callback() {
            @Override
            public void onCompletion(RecordMetadata metadata, Exception exception) {
                if (exception != null){
                    System.out.println("消息发送失败"+exception.getMessage());
                }

                if (metadata != null){
                    System.out.println("同步方式发送结果:"+"topic-"+metadata.topic()+" | partition-"+metadata.partition()
                    + " | offset-"+metadata.offset());
                }
            }
        });

        //如果不设置,则主线程直接结束,callback方法来不及执行
        Thread.sleep(100000);

6.生产中的ack配置

在同步发送的前提下,生产者在获得集群返回的ack之前会一致阻塞.那么集群什么时候返回ack呢?此时根据_ack的值来确定:

  • -ack = 0 kafka-cluster不需要任何的broker收到消息,就立即返回ack给生产者,最容易丢消息的,效率是最高的

  • -ack = 1 多副本之间的leader已经收到消息,并把消息写入到本地的log中,才会返回ack给生产者,性能和安全性是最均衡的

  • -ack = -1/all 里面有默认的配置min.insync.replicas=2(默认为1,推荐配置大于等于2),此时就需要leader和一个follower同步完成后,才会返回ack给生产者(此时集群中有两个broker已经完成数据的接受),这种方式最安全,但性能最差.

6.1关于ack和重试(如果没有收到ack,就开始重试)的配置

		//ack的类别
        properties.put(ProducerConfig.ACKS_CONFIG,"1");

        //如果ack失败,则进行重试,重试能保证消息发送的可靠性,但是也可能造成消息的重复发送
        //比如网络抖动,所以需要在接受者那边做好消息接收的幂等性
        properties.put(ProducerConfig.RETRIES_CONFIG,3);
        //重试间隔设置,默认100ms
        properties.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG,300);

7.关于消息发送的缓冲区

  • kafka默认会创建一个消息缓冲区, 用来存放要发送的消息,缓冲区默认是32m
  • kafka本地线程回去缓冲区中 一次拉取16kb的数据,发送到broker
  • 如果线程拉不到16kb的数据,间隔10ms也会将已拉到的数据发送到broker
        //设置发送消息的本地缓冲区,如果设置了该缓冲区,消息会先将消息发送到本地缓存
        //可以提高发送消息的性能,默认是33554432,即32MB
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,3355432);
        //kafka本地线程会从缓冲区拉取数据,批量发送到broker
        //设置批量发送消息的大小,默认是16384,即16KB,就是说一个batch满了16KB就发出去
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
        //是否立即发送
        //默认是0,即立即发送,但这样会影响性能
        //一般设置10ms左右,就是说这个消息发送完后会进入本地的一个batch,如果10ms内,这个batch满了16kb,就会随batch一起发送出去
        //如果10ms内,batch没满,那么也必须把消息发送出去,不能让消息的发送延迟时间太长
        properties.put(ProducerConfig.LINGER_MS_CONFIG,10);

七、Java消费者客户端

1.消费者的基本实现

public class MyConsumer {

    private final static String TOPIC_NAME = "my-replicated-topic";
    private final static String CONSUMER_GROUP_NAME = "testGroup";

    public static void main(String[] args) {
        Properties prop = new Properties();
        prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094");
        //消费分组名
        prop.put(ConsumerConfig.GROUP_ID_CONFIG,CONSUMER_GROUP_NAME);
        prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        KafkaConsumer<String,String> kafkaConsumer = new KafkaConsumer(prop);
        //消费者订阅主题列表
        kafkaConsumer.subscribe(Arrays.asList(TOPIC_NAME));

        while (true){
            /**
             * poll() 拉取消息的长轮询
             */
            ConsumerRecords<String, String> poll = kafkaConsumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord<String, String> record : poll) {
                System.out.printf("收到消息:partition = %d,offset = %d,key = %s,value = %s%n",record.partition(),record.offset(),record.key(),record.value());
            }
        }
    }
}

2.关于消费者的自动提交和手动提交offset

2.1提交的内容

消费者无论是自动提交还是手动提交,都需要把所属的消费者+消费的主题+消费的某个分区及消费的偏移量,提交到集群的_consumer_offset主题中.

2.2自动提交

消费者poll消息下来之后就会自动提交offset

        //手动提交与自动提交,默认自动提交,true
        prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
		//自动提交的时间间隔
        prop.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");

值得注意的是,自动提交会丢消息.因为消费者在消费前提交offset,有可能提交完后还没消费消费者就挂了.

2.3手动提交

        //手动提交与自动提交,默认自动提交,true
        prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");

手动提交又分为两种:

  • 同步提交

    在消息消费完后调用同步提交的方法,当集群返回ack前一致阻塞,返回ack后表示提交成功,执行之后的逻辑

    while (true){
                /**
                 * poll() 拉取消息的长轮询
                 */
        		//Duration.ofMillis(1000) 当前长轮询像是一个通道,只维持1s的时间,在这个时间内
                //不管是否拉取到500条消息,当时间后都断开通道,开始消费消息
                ConsumerRecords<String, String> poll = kafkaConsumer.poll(Duration.ofMillis(1000));
                for (ConsumerRecord<String, String> record : poll) {
                    System.out.printf("收到消息:partition = %d,offset = %d,key = %s,value = %s%n",record.partition(),record.offset(),record.key(),record.value());
                }
    
                if (poll.count() > 0){
                    //手动同步提交offset,当前线程会阻塞知道offset提交成功
                    //一般使用同步提交,因为提交之后也没有什么逻辑代码了
                    kafkaConsumer.commitSync();====阻塞==== 提交成功
    
           
                }
    
  • 异步提交

    在消息消费完之后提交,不需要等待集群ack,直接执行之后的逻辑,可以设置一个回调方法,供集群调用

    while (true){
                /**
                 * poll() 拉取消息的长轮询
                 */
                ConsumerRecords<String, String> poll = kafkaConsumer.poll(Duration.ofMillis(1000));
                for (ConsumerRecord<String, String> record : poll) {
                    System.out.printf("收到消息:partition = %d,offset = %d,key = %s,value = %s%n",record.partition(),record.offset(),record.key(),record.value());
                }
    
                if (poll.count() > 0){
                    //手动同步提交offset,当前线程会阻塞知道offset提交成功
    
                    //手动异步提交offset,当前线程不会阻塞,可以继续执行后面的代码逻辑
                    kafkaConsumer.commitAsync(new OffsetCommitCallback() {
                        @Override
                        public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                            if (exception != null){
                                System.err.println("commit failed for "+offsets);
                                System.err.println("commit failed exception "+exception.getStackTrace());
                            }
                        }
                    });
                }
    

3.长轮询poll消息

  • 默认情况下爱,消费者一次会poll500条消息

    //一次poll最大拉取消息的条数,可以根据消费消息的快慢来设置
    prop.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,500);
    
  • 代码中设置了长轮询的时间是1s

    ConsumerRecords<String, String> poll = kafkaConsumer.poll(Duration.ofMillis(1000));
    

    意味着:

    • 如果一次poll到500条,就直接执行for循环
    • 如果这一次没有poll到500条,且时间在1s内,那么长轮询继续poll,要么到500条,要么到1s
    • 如果多次poll都没有达到500条,且1s时间到了,那么直接执行for循环
  • 如果两次poll的间隔超过30s,集群会认为该消费者的消费能力过弱,该消费者会被踢出消费组,触发rebaalance机制,rebalance机制会造成性能开销.可以通过设置这个参数,让一次poll的消息条数少一点

//一次poll最大拉取消息的条数,可以根据消费消息的快慢来设置
        prop.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,500);
        //如果两次poll的间隔超过30s,集群会认为该消费者的消费能力过弱,该消费者会被踢出消费组,
        // 触发rebaalance机制,rebalance机制会造成性能开销.可以通过设置这个参数,让一次poll的消息条数少一点
        prop.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG,30 * 1000);

4.消费者的健康状态检查

消费者每个1s向kafka集群发送心跳,集群发现如果有超过10s没有续约的消费者,将被踢出消费组,触发该消费组的 rebalance机制,将该分区交给消费组里的其他消费者进行消费.

//consumer向broker发送心跳的间隔时间
prop.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG,1000);
//kafka如果超过10s还没有收到消费者的心跳,则会把消费者踢出消费者,进行rebalance,把分区分配给其他消费者
prop.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG,10*1000);

5.指定分区消费

//指定分区消费
kafkaConsumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));

6.消息的回溯消费

就是从当前主题的当前分区,从第一个偏移量开始消费

//消息的回溯消费
kafkaConsumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
kafkaConsumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));

7.指定offset偏移量进行消费

//指定偏移量进行消费
kafkaConsumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
kafkaConsumer.seek(new TopicPartition(TOPIC_NAME,0),10);

8.指定时间消费

根据时间,去所有的partition中确定该时间对应的offset,然后去所有的partition中找到对应的该offset之后的消息开始消费

		//指定时间消息
        //根据时间,去所有的partition中确定该时间对应的offset,
        // 然后去所有的partition中找到对应的该offset之后的消息开始消费
        List<PartitionInfo> partitionInfos = kafkaConsumer.partitionsFor(TOPIC_NAME);
        //从一小时前开始消费
        long fetchDataTime = new Date().getTime() - 1000 * 60 * 60;
        //获取一小时前的所有offset
        Map<TopicPartition,Long> map = new HashMap<>();
        for (PartitionInfo partitionInfo : partitionInfos) {
            map.put(new TopicPartition(TOPIC_NAME,partitionInfo.partition()),fetchDataTime);
        }
        //根据时间获得offset后,在根据offset获取到具体的消息
        Map<TopicPartition, OffsetAndTimestamp> parMap = kafkaConsumer.offsetsForTimes(map);
        for (Map.Entry<TopicPartition, OffsetAndTimestamp> parMapEntry : parMap.entrySet()) {

            //消息的键
            TopicPartition key = parMapEntry.getKey();
            //消息的值
            OffsetAndTimestamp value = parMapEntry.getValue();
            if (key != null || value != null) continue;
            long offset = value.offset();
            System.out.println("partition-"+key.partition()+"| offset " + offset);

            //根据消息里的Timestamp确定offset
            if (value != null){
                kafkaConsumer.assign(Arrays.asList(key));
                kafkaConsumer.seek(key,offset);
            }
        }

9.新消费组的消费offset规则

//当消费主题的是一个新的消费组,或者指定offset的消费方式,而offset不存在,那么应该如何消费
        //latest(默认): 只消费自己启动后发送到主题的消息
        //earliest: 第一次从头开始消费,以后按照消费offset记录继续消费,注意区别seekToBeginning(每次从头开始)
        prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");

八、Springboot使用kafka

1.引入依赖

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

2.配置文件

server:
  port: 8080
spring:
  kafka:
    bootstrap-servers: 10.12.156.247:9093
    producer: # 生产者
      retries: 3 #重发次数
      batch-size: 16384
      buffer-memory: 33554432
      acks: 1
      # 指定消息key和value的编码解码格式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer: #消费者
      group-id: default-group
      enable-auto-commit: false
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringSerializer
      value-deserializer: org.apache.kafka.common.serialization.StringSerializer
      max-poll-records: 500
    listener:
      # 当每一条记录被消费者监听器(listenerConsumer)处理之后提交
      # RECORD
      # 当每一批poll()的数据被消费者监听器(listenerConsumer)处理之后提交
      # BATCH
      # 当每一批poll()的数据被消费者监听器(listenerConsumer)处理之后提交,距离上次提交时间大于TIME提交
      # TIME
      # 当每一批poll()的数据被消费者监听器(listenerConsumer)处理之后提交,被处理record数量大于等于COUNT时提交
      # COUNT
      # TIME | COUNT 有一个条件满足时提交
      # COUNT_TIME
      # 当每一批poll()的数据被消费者监听器(listenerConsumer)处理之后提交,手动调用Acknowledgment.acknowledge()后提交
      # MANUAL
      # 手动调用Acknowledgment.acknowledge()后提交
      # MANUAL_IMMEDIATE
      ack-mode: MANUAL_IMMEDIATE
  redis:
    host: 124.70.180.216

3.编写消息生产者

@Controller
@RequestMapping("/msg")
public class MyKafkaProducer {

    private final static String TOPIC_NAME = "my-replicated-topic";

    @Autowired
    private KafkaTemplate<String,String> kafkaTemplate;

    @RequestMapping("/send")
    public String sendMsg(){
        kafkaTemplate.send(TOPIC_NAME,0,"key","this is a message");
        return "send success";
    }
}

4.编写消息消费者

@Component
public class MyKafkaConsumer {

    @KafkaListener(topics = "my-replicated-topic",groupId = "MyGroup1")
    public void listenerGroup(ConsumerRecord<String,String> record, Acknowledgment ack){
        String value = record.value();
        System.out.println(value);
        System.out.println(record);

        //手动提交offset
        ack.acknowledge();
    }

//    @KafkaListener(topics = "my-replicated-topic",groupId = "MyGroup2")
//    public void listenerGroup2(ConsumerRecords<String,String> records, Acknowledgment ack){
//        for (ConsumerRecord<String, String> record : records) {
//            String value = record.value();
//            System.out.println(value);
//            System.out.println(record);
//            //手动提交offset
//            ack.acknowledge();
//        }
//
//    }
}

5.消费者中配置主题,分区和偏移量

    @KafkaListener(groupId = "testGroup",topicPartitions = {
            @TopicPartition(topic = "topic1",partitions = {"0","1"}),
            @TopicPartition(topic = "topic2",partitions = "0",
                    partitionOffsets = @PartitionOffset(partition = "1",initialOffset = "100"))
    },concurrency = "3")    //concurrency就是同组消费者的个数,就是并发消费数,建议小于等于分区总数
    public void listenerGroupPro(ConsumerRecord<String,String> record, Acknowledgment ack){
        String value = record.value();
        System.out.println(value);
        System.out.println(record);

        //手动提交offset
        ack.acknowledge();
    }

九、kafka集群中的controller、rebalance、HW

1.controller

  • 集群中谁来充当controller

    每个broker启动时会向zk创建一个临时序号节点,获得的序号最小的那个broker将会作为集群中的controller,负责管理整个集群中分区和副本的状态

  • 当某个分区的**leader副本出现故障时,**由控制器负责为该分区选举新的leader副本.(ISR的顺序)

  • 当检测到某个分区的ISR集合发生变化时(新增或减少broker),由控制器负责通知所有broker更新其元数据信息

  • 当使用kafka-topics.sh脚本为某个topic**(增加或减少)分区数量时**,同样还是由控制器负责让新分区被其他节点感知到

2.Rebalance机制

前提是: 消费者没有指定消费的分区. 当消费组消费者和分区的情况发生变化的时候,就会触发rebalance机制

这和机制会重新调整消费者消费哪个分区

在触发rebalance机制之前,消费者消费哪个分区有三种策略:

  • range: 通过公式来计算某个分区消费哪个分区 前面的消费者是分区总数/消费者数量 +1,之后的消费者是分区总数/消费者数量
  • 轮询: 大家轮流消费
  • sticky: 在触发rebalance后,在消费者消费的原分区不变的基础是上进行调整.(在rebalance之前用那种,就继续用那种,如果这个策略没有开,则就要进行全部的重新分配.建议开启)

3.HW和LEO

LEO是某个副本最后消息的消息位置(log-end-offset)

HW是已经完成同步的位置.消息在写入broker时.且每个broker完成这条消息的同步后,HW才会变化.在这之前消费者是消费不到这条消息的.在消息同步完成之后,HW更新之后,消费者才能消费到这条消息,这样的目的是防止消息的丢失(5被消费之后,leader在未完成备份之前,挂掉了,后面发现消费不到消息.因为消费之后offset发生了变化)和重复消费(如 broker1完成备份之后,leader挂了,且新的leader是broker1,所以这样就让消息重复消费了).

十、kafka中的优化问题

1.如何防止消息丢失

  • 生产者: 1.使用同步发送. 2.把ack设为1或者all ,并且设置同步分区数 >=2
  • 消费者: 把自动提交改为手动提交

2.如何防止消息的重复消费

在防止消息丢失的方案中,如果生产发送完消息后,因为网络抖动,没有收到ack,但实际上broker已经收到了.

此时生产者会进行重试,于是broker就会收到多条相同的消息,而造成消息的重复消费

  • 生产者关闭重试: 会造成丢消息(不建议)

  • 消费者解决非幂等性消费问题

    所谓的幂等性: 多次访问的结果是一样的.对于rest的请求(get(幂等)、post(非幂等)新增、put(幂等)修改、delete(幂等)删除)

    • 在数据库中创建联合主键,防止相同的主键 创建出多条记录
    • 使用分布式锁,以业务id为锁.确保只有一条记录能够创建成功

3.如何做到消息的顺序消息

  • 生产者: 保证消息按顺序发送,且消息不丢失—使用同步发送,ack非0
  • 消费者: 主题只能设置一个分区,消费组中只能有一个消费者

kafka的顺序消费使用场景不多,因为牺牲掉了性能,但是比如rocketmq在这一块有专门的功能已经设计好了

4.kafka的消息积压问题

4.1消息积压问题的出现

消息的消费者的消费速度远赶不上生产者的生成消息的速度,导致kafka中有大量的消息没有被消息.随着没有被消费的数据堆积越来越多,消费者寻址的性能会越来越差,最后导致整个kafka对外提供的服务性能很差,从而造成其他服务也访问很慢,造成服务雪崩.

4.2消息积压的解决方案

  • 在消费者中,使用多线程,充分利用机器的性能进行消费消息.
  • 通过业务的架构设计,提升业务层面消费的性能.
  • 创建多个消费组,多个消费者,部署到其他机器上,一起消费,提高消费者的消费速度
  • 创建一个消费者,该消费者在kafka另建一个主题,配上多个分区,多个分区再配上多个消费者,该消费者将poll下来的消息,不进行消费,直接转发到新建的主题上.此时,新的主题的多个分区的多个消费者就开始一起消费了.(不常用)

5.延迟队列

5.1应用场景

订单创建后,超过30分钟没有支付,则需要取消订单,这种场景可以通过延时队列来实现

5.2具体方案

  • kafka中创建相应的主题
  • 消费者消费该主题的消息(轮询)
  • 消费者消费消息时判断消息的创建时间和当前时间是否超过30分钟(前提是订单没支付)
    • 如果是: 去数据库中修改订单状态为已取消
    • 如果否: 记录当前消息的offset,并不在继续消费之后的消息.等待一分钟后,再次向kafka拉取该offset及之后的消息,进行判断,以此往复

十一、kafka-eagle监控平台

1.搭建

  • 去kafka eagle官网下载

    http://kafka.apache.org

  • 分配一台虚拟机

  • 虚拟机中安装jdk

  • 接压缩kafka-eagle的压缩包

  • 给kafka-eagle配置环境变量

    vi /etc/profile

    export KE_HOME=/usr/local/tools/kafka-eagle/
    export PATH=$PATH:$KE_HOME/bin
    
  • 修改system-config.properties配置文件

    配置zk

    注掉原来的轻量级数据库

    配置自己的数据库

  • bin目录下启动

    ./ke.sh start
    

kafka.eagle.driver=org.sqlite.JDBC
kafka.eagle.url=jdbc:sqlite:/hadoop/kafka-eagle/db/ke.db
kafka.eagle.username=root
kafka.eagle.password=www.kafka-eagle.org

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值