Kafka-2-集群&基础使用

1 Kafka集群

1.1 分区与备份

搭建三个节点的Kafka集群,创建1个Topic,两个Partition,三个副本

副本:是对Partition的备份,集群中不同的副本会被部署在不同的Broker上

在三个Broker的集群中创建名为my-replicated-topic的Topic,有两个分区,三个副本,Topic的详细信息:
请添加图片描述
集群中的三台机器分别为0,1,2 上述的信息可以看到
Topic的第一个分区Partition0的Leader是2号机器 这个分区的三个副本在2,0,1上
Topic的第二个分区Partition1的Leader是0号机器 这个分区的三个副本在0,1,2上

上述的Isr代表可以同步的Broker节点和已同步的Broker节点集合,Leader宕机后从Isr集合中选取新Leader
如果某台Follower机器同步效率低,其中数据滞后于Leader且在Leader宕机后被选做新的Leader
这是不合理的,为此可以将其从Isr集合中摘除 不将它作为待上位成为Leader的Follower
请添加图片描述

Partition0有3个副本分别位于0,1,2三台机器上 且有Leader和Followers的关系
Leader负责读写并和Followers同步数据,一旦Leader所处的机器宕机,其他的Follower可以成为该分区的Leader

Producer生产的消息存放到Leader分区,Consumer从Leader分区消费消息
Follower只做备份或在Leader宕机后成为Leader,Leader不断向Follower同步数据以保证一致性

1.2 集群的消费问题

1,一个Consumer来消费
请添加图片描述
2,一个ConsumerGroup来消费(单播)
请添加图片描述
来看这样的一个场景:
集群中两个Broker,一个Topic,四个Partition,两个ConsumerGroup
这样的场景消费的规则是什么?
请添加图片描述
一个Partition只能被一个ConsumerGroup里的一个Consumer消费,从而保证消费顺序
Kafka只在Partition的范围内保消费信息的局部顺序性,不能在一个Topic中多个Partition中保证消费顺序性
一个Consumer是可以消费多个Partition的

用这样一个例子来说明,下图并不完整只是举例
但牢记单播的原则,一个Partition只能被一个ConsumerGroup中的一个Consumer消费
生产者依次生产了1-9 九条消息被分发到不同的Partition中 不同ConsumerGroup中的Consumer来消费这些消息
Consumer1可以消费到五条消息,但是这五条消息不能保证有序,不能保证是1,2,4,7,8
但能保证来自Partition0的消息顺序是1,2,8 来自Partition2的消息顺序是4,7,这就是局部顺序性
请添加图片描述
一个Partition只能被一个ConsumerGroup中的一个Consumer消费就是为了保证局部顺序性
一旦Partition可以被一个ConsumerGroup中多个Consumer消费,假设Partition2可以被Consumer3和Consumer4消费
那么Partition2发来的消息4,7 可能把4发给了Consumer3,7发给了Consumer4 这里的数字消息并无顺序意义
此时就无法保证局部顺序性,不知道是7应该是先被消费还是4该先被消费,某些业务场景无法接受

再考虑这样一个问题假设Consumer3异常宕机,此时Partition0和Partition2在 ConsumerGroup2中此时没有消费者
此时根据Kafka集群的rebalance机制,会自动选择其他的Consumer来消费Partition0和Partition2
不违反单播/多播原则即可

2 基础使用

2.1 生产者生产消息

2.1.1 Java客户端的生产者

首先引入依赖:

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

Java代码实现一个生产者简单发送一条消息:

public class MyProducer {

    // 发送的目的Topic
    private final static String TOPIC_NAME = "my-topic";

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 配置生产者需要的KV
        Properties properties = new Properties();
        
        // 发送的目的集群 及集群中机器通信的端口
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9002,127.0.0.1:9003,127.0.0.1:9004");
        
        // 把发送的消息key从字符串序列化为字符数组 
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        
        // 把发送的消息value从字符串序列化为字符数组
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 创建Producer对象 即发送消息的客户端 使用之前的properties的配置
        Producer<String, String> producer = new KafkaProducer<String, String>(properties);
        
            
        // ProducerRecord即被发送的消息记录 将消息包装到该对象中 由Producer来send
        // 这里未指定发送分区 具体发到Topic的哪个分区 根据hash(key)%partitionCount计算决定
        // 指明Topic和被发送消息的KV K在未指定分区时可以根据公式计算出发到哪个partition V就是具体消息内容
        ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>
                (TOPIC_NAME, "hello", "helloKafka");

        // 通过Producer发送消息 这里采用同步方式 返回的RecordMetadata是本次发送后该消息的元数据
        RecordMetadata metadata = producer.send(producerRecord).get();

        // 打印该消息被发向的Topic partition 和该消息的offset
        System.out.println("同步方式发送消息结束-> " + "topic:" + metadata.topic() +
                " partition:" + metadata.partition() + " offset:" + metadata.offset());
        
    }
    

}

想要将消息发送到指定分区上只需更改:

ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>
                (TOPIC_NAME, 0, "hello", "helloKafka");
此时消息被发送到partition0上

2.1.2 生产者的同步&异步发送

同步发送:

生产者发送消息后没有收到ACK,就会导致生产者阻塞
阻塞X秒后如果还没有收到ACK,会重发消息,重发的次数最多为Ytry {
            RecordMetadata data = producer.send(producerRecord).get();
            
            // 在收到ACK前 这里被阻塞
            System.out.println("同步方式发送消息结束-> " + "topic:" + metadata.topic() +
                    " partition:" + metadata.partition() + " offset:" + metadata.offset());
        } catch (InterruptedException e) {
        	// 发送失败 记录日志
            e.printStackTrace();
            
            // 设置间隔3s后 再次尝试同步发送 再失败人工介入
            Thread.sleep(3000);
            try {
                RecordMetadata data = producer.send(producerRecord).get();

            } catch (Exception e1) {
                // 手动处理
            }
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

请添加图片描述

异步发送:

异步发送时生产者发完后就可以执行之后的业务,
borker收到消息后异步调用生产者提供的callback()告知结果

		// 创建一个计数器 发几条消息初始化几 这里只发1条
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        producer.send(producerRecord, new Callback() {
            @Override
            public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                if(e != null) {
                    System.err.println("发送消息失败: " + e.getStackTrace());
                }

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

                // 每成功1次 计数器-1
                countDownLatch.countDown();
            }
        });

        // 判断countDownLatch是不是0 不是则等待5s
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.close();

请添加图片描述
这两种方式,生产中更多的使用同步而非异步的方式
因为异步存在消息丢失的可能,异步虽然可以提升发送性能,但丢失消息是部分业务不能容忍的

2.1.3 生产者端ACK的相关配置

以同步方式发送消息时,每发送一条都要等待Broker回传相应的ACK,未收到前都会被阻塞

ACK可以配置三个参数:
ACK回传策略
-ack=0 该参数代表消息到达Broker后,就回传ACK 最容易丢失消息,效率最高

-ack=1 该参数代表当Leader收到消息后并把消息写入本地日志,再返回ACK 性能均衡

-ack=-1 该参数代表当Leader收到消息后并把消息同步到X-1台机器后,再返回ACK 最安全,但效率最低
里面的X通过 min.insync.replicas=X(X默认为1,推荐X设置>=2)

// 配置ACK回传策略 此种方式当Leader收到消息并将消息写入本地日志后 即可回传ACK
properties.put(ProducerConfig.ACKS_CONFIG, "1");

请添加图片描述
且当消息发送失败后(没有收到ACK),生产者会重发消息,重发的间隔时间和重发最大次数也可以配置:

// 生产者发送消息失败,重发消息的时间间隔
properties.put(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG, 300);
        
// 生产者发送失败后,重发的最大次数
properties.put(ProducerConfig.RETRIES_CONFIG, "3");

注意,重发机制可以保证消息的可靠性,但也可能由于网络延迟等因素,造成消息的重复发送
因此需要在接收方做好接收消息的幂等性处理

2.1.4 发送消息的缓冲区机制

当生产者生产好数据后,并不会立刻发送
Kafka会默认创建一个缓冲区,并开启一个本地线程每隔一段时间从缓冲区中取数据并发给对应Partition
请添加图片描述

// 设置发送消息的本地缓冲区 如果设置了缓冲区 消息先写到缓冲区中 缓冲区大小默认32M
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);

// 设置批量发送消息的大小,默认16K 即一个batch满了16K就发送 
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);

// 10ms后batch中的数据无论满没满 都要将batch中的消息发送 不能导致太高时延
properties.put(ProducerConfig.LINGER_MS_CONFIG, 10);

2.2 消费者消费消息

2.2.1 Java客户端的消费者

Java代码实现一个消费者,该消费者订阅了1个Topic,每隔1s拉取一次消息

public class MyConsumer {

    // 消费的目的Topic
    private final static String TOPIC_NAME = "my-topic";

    // Consumer隶属的消费者组
    private final static String CONSUMER_GROUP_NAME = "testGroup";

    public static void main(String[] args) {

        // 配置消费者端需要的KV
        Properties properties = new Properties();

        // 消费的目的集群 及集群中机器通信的端口
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9002,127.0.0.1:9003,127.0.0.1:9004");

        // 把消费的消息key从字符串序列化为字符数组
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 把消费的消息value从字符串序列化为字符数组
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 消费者所属ConsumerGroup名称
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);

        // 创建一个消费者的客户端
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);

        // 消费者订阅主题列表
        consumer.subscribe(Arrays.asList(TOPIC_NAME));

        while (true) {
            // poll()是拉取消息的长轮询 Consumer可以在1s内拉取n次 1s结束后开始消费拉取n次取到的消息
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));

            // 每条ConsumerRecord就是一条具体的消息 可以得到该消息被发到的Partition 在Partition中的偏移量 以及消息的核心生产者设置的KV
            for(ConsumerRecord<String, String> record : consumerRecords) {
                System.out.printf("收到消息: partition = %d offset = %d key = %s value = %s%n",
                                        record.partition(), record.offset(), record.key(), record.value());
            }
        }


    }


}

2.2.2 offset的自动提交和手动提交

Consumer收到消息后,需要向Broker的_consumer_offset主题提交当前主题-分区-消息的偏移量
以记录Topic中哪些消息已经被消费,哪些尚没有被消费

提交的方式有两种,自动提交和手动提交

自动提交
// 配置消费者是否自动提交offset 默认为true
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        
// 配置自动提交offset的间隔时间
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

自动提交是默认开启的,消费者poll到消息后默认自动向Broker0的_consumer_offset提交偏移量
poll消息时,定位当前应该拉取哪些消息,也是根据offset来决定的
请添加图片描述
但是自动提交可能会丢失消息 因为如果Consumer还没消费完poll下来的消息就提交了偏移量
那么此时如果Consumer宕机,poll下来的有些消息就没有被消费成功
即丢失了一部分消息,但系统却认为这些消息已被消费

手动提交

手动提交即消费完消息后再提交该消息的offset

// 关闭默认开启的自动提交 采用手动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");

请添加图片描述

手动提交又可以用同步或异步的方式完成

手动同步提交:
 	while (true) {
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));

            for(ConsumerRecord<String, String> record : consumerRecords) {
                System.out.printf("收到消息: partition = %d offset = %d key = %s value = %s%n",
                                        record.partition(), record.offset(), record.key(), record.value());
            }
            
            // 手动同步提交offset 提交成功前阻塞后面逻辑 但一般消费完也不做什么事
            if(consumerRecords.count() > 0) {
                consumer.commitAsync();
            }
        }


手动异步提交:
	while (true) {
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));

            for(ConsumerRecord<String, String> record : consumerRecords) {
                System.out.printf("收到消息: partition = %d offset = %d key = %s value = %s%n",
                                        record.partition(), record.offset(), record.key(), record.value());
            }
            
            // 手动异步提交
            consumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception e) {
                    if(e != null) {
                        System.err.println("commit failed for " + offsets);
                        System.err.println("commit failed exception " + e.getStackTrace());
                    }
                }
            });
           
        }

生产中一般都会使用手动提交,且使用同步的方式来完成
因为消费完一般消费者端就没有逻辑了,采用异步的方式意义并不大

2.2.3 消费者poll消息的细节

关于poll消息的过程,有如下几个配置:

// 一次poll最多拉取的消息条数500 可以根据消费消息的快慢来设置
properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);

// 如果两次poll的间隔超过了30s Kafka会任务当前的Consumer消费能力过弱 将其踢出消费者组
// 通过rebalance机制选择消费者组中新的Consumer来消费当前的Partition reblance机制又会造成额外开销
properties.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);

再看拉取消息并消费的代码:
	 while (true) {
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));

            for(ConsumerRecord<String, String> record : consumerRecords) {
                System.out.printf("收到消息: partition = %d offset = %d key = %s value = %s%n",
                                        record.partition(), record.offset(), record.key(), record.value());
            }

            // 手动同步提交
			  if(consumerRecords.count() > 0) {
                consumer.commitAsync();
            }           

        }

其实拉取消息的过程是这样的:
循环中每次拉取消息的总时间只有1s,但可以再这1s内拉取n次
如果一次poll到了500条,结束poll直接开始for循环
如果这次poll拉不到500条,且总时间在1s内,继续poll
1s结束后,开始消费这n次拉取的消息,消费结束后 提交offset

2.2.4 消费者的健康状态检查

先看这样两个配置:

// 配置Consumer给Broker发送心跳的间隔时间
properties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);

// Broker如果超过10s没有收到Consumer的心跳 会将Consumer踢出ConsumerGroup 
// 并通过rebalance机制选择其他Consumer消费Partition
properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);

Consumer每隔1s向Kafka集群发送心跳,集群如果发现某个Consumer已经超过10s没有回传心跳
可以将失联的Consumer踢出消费者组,并触发该消费者组的rebalance机制
将分区交给别的Consumer消费

2.2.5 消费者指定分区偏移量和时间消费

1,为Consumer指定分区消费 
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));

2,消息回溯消费 即让消费者从某个分区的第一条消息开始消费 
即从某个Partition的offset=0的消息开始消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));

3,指定offset消费 即让消费者从某个分区的第n个消息开始消费 此处n=10
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seek(new TopicPartition(TOPIC_NAEM, 0), 10);

4,从指定时间点开始消费 如从1小时前开始消费 即消费1小时前到现在的消息
根据时间去所有的Partition中确认该时间对应的offset 然后在所有Partition中找到该offset之后的消息开始消费

2.2.6 新消费组消费offset的规则

考虑如下一个场景,原先只有一个ConsumerGroup消费某个Topic中的消息
现在增加一个ConsumerGroup到系统中并消费Topic
那么新加的ConsumerGroup能消费到哪些消息?

// 当新加了ConsumerGroup消费某个主题时 新ConsumerGroup中的Consumer有两种消费方式
// 1, latest 默认值 只消费自己启动后发到Topic的消息
// 2, earliest 一开始从头开始消费 以后按照offset正常消费
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值