kafka笔记

学习视频:千锋教育_索尔 https://www.bilibili.com/video/BV1Xy4y1G7zA?spm_id_from=333.337.search-card.all.click

一、为什么使用消息队列

  1. 使用同步的通信方式来解决多个服务之间的通信
    同步的通信方式会存在性能和稳定性的问题。

kafka笔记_第1张图片

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

kafka笔记_第2张图片

消息队列解决的具体是什么问题 —— 通信问题

二、消息队列的流派

目前消息队列的中间件选型有很多种:

  • rabbitMQ:内部的可玩性(功能性)是非常强的
  • rocketMQ:阿里内部一个大神,根据Kafka的内部执行原理,手写的一个消息队列中间件。性能与Kafka并肩,除此之外,封装了更多的功能(顺序消费、延迟消息 …)
  • kakfa:全球消息处理性能最快的一款MQ
  • zeroMQ

那这些消息队列中间件有什么区别?

这些流派可以分为:

  1. 有broker

这个流派通常有一台服务器作为broker,所有的消息都通过它中转。生产者把消息发送给它就结束自己的任务了,broker则把消息主动推送给消费者(或者消费者主动轮询)

  • 重topic

Kafka、RocketMQ、JMS(ActiveMQ)
整个broker,依据topic来进行消息的中转,在重topic的消息队列里必然需要topic的存在
kafka笔记_第3张图片

  • 轻topic
    RabbitMQ(AMQP)
    topic只是一种中转模式
  1. 无broker
    zeroMQ
    在生产者和消费者之间没有使用broker,例如zeroMQ,直接使用socket进行通信。

三、Kafka的基本知识

Kafka是最初由Linkedin公司开发,是一个分布式、支持分区的、多副本的、基于zookeeper协调的分布式消息系统。

它的最大特性就是可以实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延时的实时系统、storm/spark流式处理引擎、web/nginx日志、访问日志、消息服务等。

用scala语言编写,Linkedin于2010年贡献给了Apache基金会,并成为顶级开源项目。

1. Kafka的使用场景

  • 日志收集
  • 消息系统
  • 用户活动跟踪
  • 运营指标

2. Kafka的安装(linux)

  • 部署一台zookeeper服务器
  • 安装jdk
  • 下载Kafka安装包:https://archive.apache.org/dist/kafka/2.4.1/kafka_2.11-2.4.1.tgz
  • 上传到Kafka服务器上:/home/xxx/kafka
  • 解压缩安装包
  • 进入到config目录,修改server.properties文件中以下内容:
# broker.id属性在kafka集群中必须是唯一的
broker.id=0
#kafka部署的机器ip和提供服务的端口号
listeners=PLAINTEXT:192.168.0.1:9092
#kafka的消息存储文件
log.dir=/usr/local/data/kafka-logs
#kafka连接zookeeper的地址
zookeeper.connect=192.168.0.2:2181
  • 进入到bin目录中,执行以下命令,启动Kafka服务器(带配置文件)
./kafka-server-start.sh -daemon ../config/server.properties
  • 校验Kafka是否启动成功:
    进入到zk内查看是否有Kafka的节点:/brokers/ids/0

3. kafka的基本概念

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

4. 创建topic

通过Kafka命令向zk中创建一个主题

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

查看当前zk中的所有主题

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

5. 发送消息

把消息发送到broker中某个topic,打开一个Kafka发送消息的客户端,然后开始用客户端向Kafka服务器发送消息。默认情况下,每一行都会被当作一个独立的消息

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

6. 消费消息

打开一个消费消息的客户端,向Kafka服务器的某个topic消费消息。默认是消费最新的消息

方式一:从当前主题最后一条消息的偏移量+1的位置开始消费,即消费最新消息

./kafka-console-consumer.sh --bootstrap-server 192.168.0.1:9092 --topic test

方式二:从当前主题中第一条消息开始消费,即每次都从头开始消费

./kafka-console-consumer.sh --bootstrap-server.sh 192.168.0.1:9092 --from-beginning --topic test

7. 关于消息的细节

kafka笔记_第4张图片

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

/usr/local/kafka/data/kafka-logs/主题-分区/00000000.log
  • 消息的保存是有序的,通过offset偏移量来描述消息的有序性
  • 消费者消费消息时也是通过offset来描述当前要消费的那条消息的位置

8. 单播消息

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

回答 如果多个消费者属于同一个消费组,那么只有一个消费者能订阅到topic中的消息。换言之,同一个消费组中只能有一个消费者能消费topic中的消息。

9. 多播消息

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

./kafka-console-consumer.sh --bootstrap-server 192.168.0.1:9092 --consumer-property group.id=testGroup1 --topic test // 启动一个teatGroup1下的consumer实例
./kafka-console-consumer.sh --bootstrap-server 192.168.0.1:9092 --consumer-property group.id=testGroup2 --topic test // 启动一个teatGroup2下的consumer实例

kafka笔记_第5张图片

10. 查看消费组及信息

查看消费组列表

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

kafka笔记_第6张图片
查看消费组详细信息

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

image.png

重点关注以下几个信息:

  • current-offset:最后被消费的消息的偏移量
  • log-end-offset:消息总量(最后一条消息的偏移量)
  • lag:积压了多少条消息(有多少条消息未被消费)

四、Kafka中主题和分区的概念

1. 主题topic

主题topic在Kafka中是一个逻辑的概念,Kafka通过topic将消息进行分类。发送到不同topic上的消息会被订阅该topic的消费者消费。

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

2. 分区Partition

1. 基本概念

通过_partition将一个topic中的消息分区存储_。这样有多个好处:

  • 分区存储,可以解决统一存储,文件过大的问题
  • 提高了读写的吞吐量,可以实现多个分区的并发读写

kafka笔记_第7张图片

2. 创建多分区的主题

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

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

  • 00000.log:这个文件中保存的就是消息
  • _consumer_offsets-0 ~ _consumer_offsets-49:
    • kafka内部自己创建了_consumer_offsets主题,默认包含了50个分区。
    • 这个主题用来存放消费者消费某个主题的偏移量。因为每个消费者都会自己维护自己消费的主题的偏移量,并自主上报给Kafka中默认的主题_consumer_offsets。因此_Kafka为了提升这个主题的并发性,默认设置了50个分区。
    • 至于该提交到哪个分区,通过hash函数确认:hash(consumerGroupId) % __consumer_offset主题的分区数
    • 提交到该主题中的内容:key为consumerGroupId+topic+分区号,value为当前offset的值
  • 文件中保存的消息,默认保存7天。七天后消息就会被删除。

五、Kafka集群操作

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

创建三个server.properties文件

# 主要修改以下三处配置
# 0, 1, 2 节点id
broker.id=0
# 9092 9093 9094 服务监听端口
listeners=PLAINTEXT://192.168.0.1:9092
# kafka-logs  kafka-logs-1 kafka-logs-1  消息存储路径
log.dir=/usr/local/data/kafka-logs

通过命令启动三台broker

./kafka-server-start.sh -daemon ../config/server.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笔记_第8张图片

  • leader:Kafka的读写操作,都发生在leader上。leader负责把数据同步给follower。当leader挂了,经过主从选举,从多个follower中选举产生一个新的leader。
  • follower:接收leader同步的数据。
  • ISR:可以同步和已经同步的节点会被存入到ISR集合中。这里有一个细节:如果ISR中的节点性能较差,会被踢出ISR集合。(ISR集合中的元素按照同步性能排序)

3. broker、主题、分区、副本

此时,broker、主题、分区、副本 这些概念就全部展现了:

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

4. Kafka集群消息的发送

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

5. Kafka集群消息的消费

# 不指定消费组来消费
./kafka-console-consumer.sh --bootstrap-server 192.168.0.1:9092,192.168.0.1:9093,192.168.0.1:9094 --from-beginning --consumer-property --topic my-replicated-topic

# 指定消费组来消费
./kafka-console-consumer.sh --bootstrap-server 192.168.0.1:9092,192.168.0.1:9093,192.168.0.1:9094 --from-beginning --consumer-property group.id=testGroup1 --topic my-replicated-topic

6. Kafka集群,分区,消费组中消息消费的细节

kafka笔记_第9张图片

  • 一个partition只能被同一个消费组中一个消费者消费(为了保证partition分区消息消费的顺序性)。但是多个partition的多个消费者消费的总的顺序性是得不到保证的,那怎么做到消费的总顺序性呢?
  • partition的数量决定了消费组中消费者的数量,建议同一个消费组中的消费者的数量不要超过partition的数量,否则多的消费者消费不到消息,会处于空闲状态
  • 如果消费者挂了,那么会触发rebalance机制,让其他消费者来消费该分区的消息

六、Kafka的java客户端-生产者的实现

1. 生产者的基本实现

  • pom.xml中引入依赖
<dependency>
  <groupId>org.apache.kafkagroupId>
  <artifactId>kafka-clientsartifactId>
  <version>2.4.1version>
dependency>
  • 代码实现
public class MyProducer {

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

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        // 配置要发送的broker地址
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.0.1:9092,192.168.0.1:9093,192.168.0.1:9094");
        // 把发送消息的key从字符串序列化为字节数组
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 把发送消息的value从字符串序列化为字节数组
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 加载配置,创建producer实例
        Producer<String, String> producer = new KafkaProducer<>(props);
        Order order = new Order(1, 2);
        // 创建消息
        ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, order.getOrderId().toString(), JSON.toJSONString(order));
        // 同步发送消息,得到消息发送的元数据并输出
        RecordMetadata metadata = producer.send(producerRecord).get();
        System.out.println("同步方式发送消息结果:"+"topic-"+metadata.topic()+" |partition-"+metadata.partition()+" |offset-"+metadata.offset());
    }
}

2. 生产者同步发送消息

kafka笔记_第10张图片
默认情况下,如果生产者发送消息后没有收到ack,生产者会阻塞,阻塞3s的时间,如果还没有收到消息,会进行重试,重试次数为3次。

如果重试3次后还是失败,就会抛异常。需要人工介入。

3. 生产者异步发送消息

// 限制:发送响应5次消息后,执行后续操作
CountDownLatch countDownLatch = new CountDownLatch(5);
// 异步发送消息
producer.send(producerRecord, new Callback() {
    @Override
    public void onCompletion(RecordMetadata metadata, Exception e) {
        if(e != null){
            System.err.println("发送消息失败:"+e.getStackTrace());
        }
        if(metadata != null) {
            System.out.println("同步方式发送消息结果:"+"topic-"+metadata.topic()+" |partition-"+metadata.partition()+" |offset-"+metadata.offset());
        }
        countDownLatch.countDown();
    }
});
countDownLatch.await(5, TimeUnit.SECONDS); // 如果不为0,则等待5秒
producer.close();

kafka笔记_第11张图片
异步发送,生产者发送完消息后就可以执行后续的业务,broker在收到消息后异步调用生产者提供的callback回调方法。

4. 生产者中的ack配置

对于ack来说,会有三个参数配置(默认为1,推荐配置≥2):

  • ack=0:Kafka-cluster 不需要任何broker收到消息,就立即返回ack给生产者(最容易丢消息,但效率是最高的,适合一些日志类型的消息)
  • ack=1:多副本之间的leader已经收到消息,并把消息写入到本地log中,才会返回ack给生产者(性能和安全性是最均衡的)
  • ack=-1/all:此时需要所有参与同步的分区副本(即ISR集合中的副本)收到消息,才会返回ack给生产者(最安全,但性能最差)
props.put(ProducerConfig.ACKS_CONFIG, "1");

如果没有收到ack,就开启重试:

  • 发送失败会重试,默认重试时间间隔是100ms,重试能够保证消息发送的可靠性,但是也可能造成消息重复发送,比如网络抖动。
  • 所以为防止重试导致的消息重复消费,需要在接收端做好消息的幂等处理
# 重试次数设置
props.put(ProducerConfig.RETRIES_CONFIG, 3);
# 重试间隔设置
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);

5. 发送消息的缓冲区机制

kafka笔记_第12张图片

  • Kafka默认会创建一个消息缓冲区,用来存放要发送的消息,缓冲区默认大小为 32MB
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
  • Kafka本地线程会去缓冲区中一次拉取16kb的数据,发送到broker
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
  • 如果线程拉取不到16kb的数据,那么间隔10ms后也会将已拉取到的数据发送到broker
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);

七、Kafka的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 props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.0.1:9092,192.168.0.1:9093,192.168.0.1:9094");
        // 配置消费组名
        props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 创建消费者实例
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        // 消费者订阅主题列表
        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        while(true){
            // poll() 拉取消息的长轮询
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
            for(ConsumerRecord<String, String> record: records){
                System.out.printf("收到消息:partition = %d, offset = %d, key = %s, value = %s%n", record.partition(), record.offset(), record.key(), record.value());
            }
        }
    }
}

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

1. 消费者提交的内容

无论是自动提交还是手动提交,都需要把所属的消费组+消费的主题+消费的分区+消费的偏移量,这样的信息提交到集群的_consumer_offsets主题中。

2. 自动提交

消费者poll到消息后,就会自动提交offset(此时还未进行业务处理,即消费端未真正开始消费消息)

// 是否自动提交offset,默认是true
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
// 自动提交offset的间隔时间
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

注意:自动提交会导致消息丢失。因为消费者在消费前提交offset,有可能提交后还未消费,消费者就挂了。

3. 手动提交

为防止消息丢失,需要把自动提交的配置改成false

// 是否自动提交offset
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");

手动提交又分为两种:

  • 手动同步提交
    在消费完消息后,调用同步提交的方法,当集群返回ack前一直阻塞,返回ack后表示提交成功,执行之后的逻辑
while(true){
    // poll() 拉取消息的长轮询
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    for(ConsumerRecord<String, String> record: records){
        System.out.printf("收到消息:partition = %d, offset = %d, key = %s, value = %s%n", record.partition(), record.offset(), record.key(), record.value());
    }
    // 所有消息已消费完
    if(records.count() > 0){
        // 手动同步提交offset,当前线程会阻塞直到offset提交成功
        // 一般使用同步提交,因为提交之后一般也没有什么逻辑代码了
        consumer.commitSync();
    }
}
  • 手动异步提交
    在消息消费完后提交,不需要等到集群ack,直接执行后续逻辑,可以设置一个回调方法,供集群调用
while(true){
    // poll() 拉取消息的长轮询
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    for(ConsumerRecord<String, String> record: records){
        System.out.printf("收到消息:partition = %d, offset = %d, key = %s, value = %s%n", record.partition(), record.offset(), record.key(), record.value());
    }
    // 所有消息已消费完
    if(records.count() > 0){
        // 手动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后面的程序逻辑
        consumer.commitAsync(new OffsetCommitCallback() {
            @Override
            public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                if(e != null){
                    System.err.println("Commit failed for " + map);
                    System.err.println("Commit failed exception: " + e.getStackTrace());
                }
            }
        });
    }
}

3. 长轮询poll消息

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

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

代码中设置了长轮询的时间是1000ms

while(true){
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    for(ConsumerRecord<String, String> record: records){
        System.out.printf("收到消息:partition = %d, offset = %d, key = %s, value = %s%n", record.partition(), record.offset(), record.key(), record.value());
    }
}

意味着:

  • 如果一次poll到500条,就直接执行for循环
  • 如果这一次没有poll到500条,且时间在1s内,那么长轮询继续poll,要么到500条,要么到1s
  • 如果多次poll都没有达到500条,且时间到了1s,那么直接执行for循环

如果两次poll的时间间隔超过30s,集群会认为该消费者的消费能力过弱,该消费者会被踢出消费组,触发rebalance机制,rebalance机制会造成性能开销。

// 如果两次poll的时间超过了30s的时间间隔,Kafka会认为其消费能力过弱,将其踢出消费组。将分区分配给其他消费者 - rebalance
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30*1000);

4. 消费者健康状态检查

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

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

5. 指定分区、偏移量、时间进行消费

指定分区消费

consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));

从头消费

consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));

指定offset消费

consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seek(new TopicPartition(TOPIC_NAME, 0), 10);

指定时间消费

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

List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
// 从1小时前开始消费
long fetchDataTime = new Date().getTime() - 1000*60*60;
Map<TopicPartition, Long> map = new HashMap<>();
for(PartitionInfo par: topicPartitions){
    map.put(new TopicPartition(TOPIC_NAME, par.partition()), fetchDataTime);
}
Map<TopicPartition, OffsetAndTimestamp> parMap = consumer.offsetsForTimes(map);
for(Map.Entry<TopicPartition, OffsetAndTimestamp> entry: parMap.entrySet()){
    TopicPartition key = entry.getKey();
    OffsetAndTimestamp value = entry.getValue();
    if(key == null || value == null) continue;
    Long offset = value.offset();
    System.out.println("partition-"+key.partition()+"|offset-"+offset);
    // 根据消息里的timestamp确定offset
    if(value != null){
        consumer.assign(Arrays.asList(key));
        consumer.seek(key, offset);
    }
}

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

新消费组中的消费者在启动以后,默认会从当前分区的最后一条消息的offset+1位置开始消费。可以通过以下设置,让新的消费者第一次从头开始消费,之后再消费新消息

  • latest:默认的,消费新消息
  • _earliest:第一次从头开始消费,之后消费新消息(最后消费的位置offset+1)
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

八、Springboot中使用Kafka

1. 创建一个springboot项目,pom.xml中引入依赖

<dependency>
  <groupId>org.springframework.kafkagroupId>
  <artifactId>spring-kafkaartifactId>
dependency>

2. 编写配置文件 application.yml

server:
  port: 8080

spring:
  kafka:
    bootstrap-servers: 192.168.0.131:9092 # Kafka集群地址 (broker节点地址)
    producer: # 生产者
      retries: 3 # 重试次数
      batch-size: 16384 # 每次拉取缓冲区中16kb数据发送到broker
      buffer-memory: 33554432 # 缓冲区大小32MB
      acks: 1 # 默认值为1 (多副本之间的leader收到消息,并写入到本地log中,返回ack给生产者)
      # 指定消息key和消息体的编解码方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer: # 消费者
      group-id: default-group # 消费组id
      enable-auto-commit: false # 手动提交offset(即先消费,再提交offset)
      auto-offset-reset: earliest # 第一次消费会从最开始的位置消费,之后消费新消息
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      max-poll-records: 500 # 一次最多拉取500条消息
    listener:
      # RECORD -- 每一条记录被消费者监听器处理后提交
      # BATCH -- 每一批poll()的数据被消费者监听器处理后提交
      # TIME -- 每一批poll()的数据被消费者监听器处理之后,距离上次提交的时间大于TIME时提交
      # COUNT -- 每一批poll()的数据被消费者监听器处理之后,被处理的record数量大于等于COUNT时提交
      # COUNT_TIME -- COUNT | TIME 有一个条件满足时提交
      # MANUAL -- 每一批poll()的数据被消费者监听器处理之后,手动调用Acknowledgment.acknowledge()后提交
      # MANUAL_IMMEDIATE -- 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种
      ack-mode: MANUAL_IMMEDIATE
  redis:
    host: 127.0.0.1

3. 编写生产者

@RestController
@RequestMapping("/msg")
public class KafkaController {

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

    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

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

4. 编写消费者

@Component
public class MyConsumer {
    @KafkaListener(topics = "my-replicated-topic", groupId = "MyGroup1")
    public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack){
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        ack.acknowledge(); // 手动提交offset(如果没有设置手动提交offset,消息会被重复消费)
    }
}

5. 先去启动zookeeper

# 在zookeeper的bin目录下执行
./zkServer.sh start

# 执行后,验证是否启动成功
ps -ef | grep zookeeper 

6. 再去启动kafka

# 在kafka的bin目录下执行
./kafka-server-start.sh -daemon ../config/server.properties

# 执行后,验证是否启动成功
ps -ef | grep kafka

7. 启动springboot项目,浏览器通过请求send接口发送消息

kafka笔记_第13张图片
image.png

注意:消费者中配置消息主题、分区、偏移量

@KafkaListener(groupId = "testGroup", topicPartitions = {
    @TopicPartition(topic = "topic1", partitions = {"0", "1"}), // 指定消费topic1下的0,1分区
    // 指定消费topic2下的0分区,从offset+1开始消费;消费topic2下的1分区,指定从offset为100的位置开始消费
    @TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))
})
public void listenGroup2(ConsumerRecord<String, String> record, Acknowledgment ack){
    String value = record.value();
    System.out.println(value);
    System.out.println(record);
    ack.acknowledge(); // 手动提交offset(如果没有设置手动提交offset,消息会被重复消费)
}

九、Kafka集群中controller、rebalance、HW

1. controller

由于kafka集群是一个分布式系统,需要有一个协调者,controller就是Kafka集群中的协调者。它本身也是一个broker,只不过负责了一个额外的工作。

集群中谁来充当controller?

每个broker启动时会向zk创建一个临时序号节点,最先在zookeeper上创建临时节点成功的broker将会作为集群中的controller。

controller重度依赖zk,依赖zk保存元数据、进行服务发现。controller大量使用watch功能实现对集群的协调管理,主要负责这么几件事:

  • 当集群中有一个副本的leader挂掉,需要在集群中选举出一个新的leader,选举的规则是从isr集合中最左边获得
  • 当集群中有broker新增或减少,controller会同步信息给其他broker
  • 当集群中有分区新增或减少,controller会同步信息给其他broker

如果controller所在的broker发生了故障,kafka集群就必须选举出一个新的controller。
这里存在一个问题:很难确定broker是宕机了还是暂时故障?但是为了集群的正常运行,必须选举出一个新的controller,如果之前故障的controller又恢复正常了,不知道自己已经被更换,那么集群中就出现了两个controller,这就是脑裂问题。

脑裂现象:简单描述,就是在分布式系统环境下,由于网络或者其他原因导致master出现假死,这时会触发系统进行新的master选举,此时系统中就会出现两个master,产生一系列问题。

Kafka是通过使用epoch number来处理脑裂问题的
epoch number只是一个单调递增的数。第一次选择controller时,epoch number值为1。如果再次选择新的controller,epoch number为2,依次单调递增。

每个新选举出的controller通过zookeeper的条件递增操作获得一个新的更大的epoch number,当其他broker知道当前controller的epoch number时,如果他们从controller收到包含旧(较小)epoch number的消息,则会忽略这些消息。即broker根据最大的epoch number来区分谁是最新的Controller。
kafka笔记_第14张图片

2. rebalance机制

  • 前提:消费组中的消费者没有指明分区来消费
  • 触发条件:当消费组中的消费者和分区的关系发生变化的时候
  • 分区分配策略:在rebalance之前,分区怎么分配会有三种策略:
    • range:根据公式计算得到每个消费者消费哪几个分区(前面的消费者消费分区数=分区总数/消费者数量+1;后面的消费者消费分区数=分区总数/消费者数量)
    • 轮询:大家轮着来(如果有消费者挂掉,重新轮询,之前的分配关系会被打乱)
    • sticky:粘合策略,如果需要rebalance,会在之前已分配的基础上调整,不会改变之前的分配情况,如果这个策略没有开,那么就要进行全部的重新分配。建议开启

3. HW和LEO

LEO是某个副本最后的消息位置(log-end-offset)
HW是已完成同步的位置。消息在写入broker,且每个broker完成这条消息的同步后,HM才会变化。在这之前消费者是消费不到这条消息的。在同步完成之后,HW更新之后,消费者才能消费到这条消息,这样的目的是为防止消息的丢失。

LEO(Log End Offset):标识当前日志文件中下一条待写入的消息的 offset
HW(High Watermark):高水位,它标识了一个特定的消息偏移量 offset(此水位之上的消息已被所有副本同步,可以安全消费),消费者只能拉取到这个水位 offset 之前的消息
kafka笔记_第15张图片

十、Kafka中的优化问题

1. 如何防止消息丢失

发送方:使用同步发送,把ack设置成1或者-1或者all;并设置同步分区数≥2,多副本,数据备份
消费方:把自动提交改成手动提交

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

在防止消息丢失的方案中,如果生产者发送完消息后,因为网络抖动,没有收到ack,但实际上broker已经收到了。此时生产者就会进行重试,于是broker就会收到多条相同的消息,从而造成消费者对消息的重复消费。

怎么解决:

  • 生产者关闭重试:会造成消息丢失(不建议)
  • 在消费者端解决幂等性消费问题:
    • 所谓的幂等性:指的是多次访问结果是一样的。对于rest请求(get(幂等)、post(非幂等)、put(幂等)、delete(幂等))
    • 具体解决方案:
      • 1. 在数据库中创建联合主键,防止相同的主键创建出多条记录
      • 2. 使用分布式锁,以业务id为锁,保证只有一条记录能够创建成功

kafka笔记_第16张图片

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

发送方:保证消息按顺序消费,且消息不丢失 —— 使用同步的发送,ack设置成非0的值
接收方:主题只能设置一个分区,消费组中只能有一个消费者。(分区数过少,消费者较少,会极大影响Kafka的性能)
Kafka的顺序消费使用场景不多,因为会牺牲性能

4. 如何解决消息积压问题

消息积压现象:

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

解决方案:

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

5. 延迟队列

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

6. Kafka为何如此之快

主要从三个角度进行分析:

1. 生产端

批量消息发送

生产端在调用send()方法时,不会立即把消息发送出去,而是放入缓存池中。把缓存池中的消息划分成一批批的数据,按批次将消息发送给mq服务端。
消息的批量发送减少了生产端与服务端的请求处理次数,从而提高了整体的消息处理能力。

自定义协议格式

序列化方式和压缩格式都能减小消息的体积,从而节省网络资源开销。

2. 服务端Broker

服务端的高性能主要体现在三个方面:

Page Cache

服务端会将消息持久化到磁盘文件中。写入文件的时候,操作系统会先将消息数据写入内存中的PageCache中,然后再一批批写入磁盘中,减少了磁盘IO的开销。
读取文件时,也是先从PageCache中读取,加快读取速率。

Kafka的文件布局及磁盘文件顺序写入

kafka服务端的结构为一个topic对应多个partition,每个partition对应一个单独的文件夹。Kafka在分区中实现文件的顺序写,即可支持多个分区文件同时写入,更能发挥磁盘IO的性能
弊端:Kafka在写入消息时,IO性能会随着topic,分区数量的增长而先升后降。所以需要合理的设置topic和分区数量。

sendFile零拷贝技术

使用零拷贝技术读取数据时,可以减少一次数据的拷贝(从内核空间拷贝到用户空间),直接通过DMA拷贝完成数据的复制,不需要cpu参与,加快数据的读取速度。

3. 消费端

并行消费

消费端通过设置多个消费组,实现消息的并行消费。从而提高消费速度

参考链接:https://mp.weixin.qq.com/s/IL9Yqu_NlN7sKmz9FBCKog

十一、Kafka-eagle监控平台

1. 搭建

  • 官网下载kafka-eagle安装包:https://www.kafka-eagle.org/
  • 分配一台虚拟机
  • 虚拟机中安装jdk
  • 解压缩

2. 配置

给kafka-eagle配置环境变量 (/etc/profile)

export KE_HOME=/usr/local/kafka-eagle
export PATH=$PATH:$KE_HOME/bin

修改kafka-eagle配置文件(/conf/system-config.properties)

  • 修改里面的zk地址和mysql地址

3. 启动

进入到bin中,通过命令启动

./ke.sh start

4. 访问

默认端口8048,默认的登录用户名和密码分别是admin,123456
kafka笔记_第17张图片

你可能感兴趣的:(中间件,kafka,java)