文章目录

      • [一、前言]
    • [二、调试常用命令行总结]
    • [三、Producer消息发送流程详解]
      • [3.1、总体流程]
      • [3.2、分步骤细化流程]
    • [四、异步发送消息实战]
      • [4.1、引入依赖]
      • [4.2、简单异步发送Demo]
      • [4.3、带回调函数的异步发送Demo]
        • [4.3.1、失败重试机制]
        • [4.3.2、解析回调函数]
        • [4.3.3、完成带回调函数的异步发送Demo]
    • [五、同步发送消息实战]
      • [5.1、从源码找到端倪]
      • [5.2、同步发送Demo]
    • [六、总结]

一、前言

前些天和大家一起深入分析了kafka架构方法的知识,这部分内容偏向于理论,不过也是大数据开发工程师必须要掌握的知识点。

kafka架构篇系列文章:
[深入分析Kafka架构(一):工作流程、存储机制、分区策略]
[深入分析Kafka架构(二):数据可靠性、故障处理]
[深入分析Kafka架构(三):消费者消费方式、三种分区分配策略、offset维护]

因此我觉得非常有必要再辅以代码实现,来加深理解,融会贯通。因此本系列kafka实战篇以实战为主,目的是使用kafka提供的JAVA API来完成消息的发送,消费,拦截等操作,用来加深对kafka架构的认知,并会把相关完整demo共享到github和咱们csdn上,感兴趣的可以拿来使用。

本文为kafka实战系列第一篇,主要进行kafka的消息发送部分的流程解析及实战开发。

注意:我所使用的kafka版本为2.4.1,java版本为1.8,本文会对一些新老版本的改动地方加以说明。

二、调试常用命令行总结

其实在日常使用kafka的过程中,很少使用命令行操作,一般命令行操作只是用来调试的时候使用。不过作为回顾,下面列出一些常用的命令行操作,并对其进行详细解释。

  1. 查看当前服务器中的所有topic

    bin/kafka-topics.sh --zookeeper zookeeper主机名或ip:2181 --list` 
    
    
  2. 创建topic

    如下命令可以创建了一个3分区,2副本的topic first:

    
     bin/kafka-topics.sh --zookeeper zookeeper主机名或ip:2181 
     --create --replication-factor 2 --partitions 3 --topic first
     
     选项说明:
     
     --topic 定义topic名
     --replication-factor 定义副本数
     --partitions 定义分区数
  3. 删除topic

    注意:删除topic的时候需要在server.properties中设置delete.topic.enable=true否则只是标记删除,并没有真正删除。

    bin/kafka-topics.sh --zookeeper zookeeper主机名或ip:2181 
    --delete --topic first 
    
    
    
  4. 查看某个Topic的详情

    bin/kafka-topics.sh --zookeeper zookeeper主机名或ip:2181 
    --describe --topic first 
    
    
    
  5. 发送消息

    注意:–broker-list kafka集群里的broker主机名或ip:9092,如果要发送给多个broker,用逗号分割就可以了。

    bin/kafka-console-producer.sh 
    --broker-list kafka集群里的broker主机名或ip:9092 --topic first
    输入完上面的命令成功后会有">"标志,就可以输入数据了
    输入数据,需要删除的时候按住ctrl 在点击backspace就可以删除了。
    >hello world 
    
    
    
    
  6. 消费消息

    注意:在kafka0.9.x版本之前,消费者指定的是zookeeper,但是新版本都是指定的kafka集群。

     bin/kafka-console-consumer.sh 
    --bootstrap-server kafka集群里的broker主机名或ip:9092 --from-beginning --topic first
    
    --from-beginning:会把主题中以往所有的数据都读取出来。` 
    
    
    
  7. 修改分区数

    注意:分区数只能增多不能减少(由于分区数减少后,把删掉的分区的数据分配到剩余的分区这个过程过于复杂,所以kafka没有设计分区减少的逻辑。)

    
     bin/kafka-topics.sh --zookeeper zookeeper主机名或ip:2181 --alter --topic first --partitions 6 
      
    
    
    
    

三、Producer消息发送流程详解

3.1、总体流程

我们谈到消息队列就会想到:异步,解耦,消峰。kafka自然也不例外,在新版本里,它的Producer发送消息采用的也是异步发送的方式(_之前老版本有同步发送的api,新版本取消了,但是我们可以通过骚操作实现同步发送,后面会详细解释_)。在消息发送的过程中,涉及到了两个线程,分别是main线程(又叫主线程)和sender线程,以及一个线程共享变量(可以理解为缓存)RecordAccumulator

总体来说,sender线程是main线程的守护线程,在工作时,main线程负责创建消息对象并将消息放在缓存RecordAccumulator,sender线程从缓存RecordAccumulator中拉取消息然后发送到kafka broker。

关于RecordAccumulator,咱们还需要知道:

  1. 在消息追加到RecordAccumulator时会对消息进行分类,发往同一分区的消息会被装在同一个Deque中,Deque存放的是ProducerBatch表示一组消息。换句话说就是RecordAccumulator会按照分区进行队列维护;
  2. 队列中存放的是发往该分区的消息组,追加消息时候从队列的尾部追加;
  3. RecordAccumulator的大小默认32M,可以通过buffer.memory配置指定;
  4. 如果内存空间用完了,追加消息将发生阻塞直到有空间可用为止,默认最大阻塞60s,可以通过数max.block.ms配置。

整体流程图可以看成下面这样:
消息发送流程
其实知道了上面这些,就可以根据api写一些简单的kafka发送消息的代码了。因为在api里,main线程和sender线程都被封装的很好,很多事情是我们不需要去关心的。

不过如果你和我一样,好奇main线程和sender线程具体都做了什么?那就接着看下面这部分细化的的流程总结,不感兴趣的话就可以直接跳到下一大节(异步发送Demo)开始撸代码了。

3.2、分步骤细化流程

首先main线程的流程:

  1. 封装消息对象为ProducerRecord并调用send方法;
  2. 进入producer拦截器(拦截器可以自定义);
  3. 更新kafka集群数据;
  4. 进行序列化,将消息对象序列化成byte数组;
  5. 使用分区器计算分区;
  6. 将消息追加到线程共享变量RecordAccumulator。

sender线程在KafkaProduer实例化结束开启,后面就是sender线程干的活了:

  1. sender线程将消息从RecordAccumulator中取出处理消息格式;
  2. 构建发送的请求对象Request;
  3. 将请求交给Selector,并将请求存放在请求队列;
  4. 收到响应就移除请求队列的请求,调用每个消息上的回调函数。

四、异步发送消息实战

其实只要掌握了Producer消息发送的总体流程,就可以根据api写基本demo了。下面我们层层递进来完成异步发送demo。

4.1、引入依赖

这里以maven依赖为例,大家可以根据自己的kafka版本在mvn上找到适合自己的依赖,由于只是做简单的消息发送,所以只需要引入kafka-clients依赖即可。我的kafka版本为2.4.1,所以我需要引入的依赖为:

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


建议:这里建议大家引入依赖后,花点时间研究一下KafkaProducer源码,至少看看里面构造函数和方法的注释也好,对kafka的学习很有帮助!

4.2、简单异步发送Demo

这里我们需要用到如下三个类:

  • KafkaProducer:需要创建一个生产者对象,用来发送数据;
  • ProducerConfig:获取所需的一系列配置参数;
  • ProducerRecord:每条数据都要封装成一个ProducerRecord对象才可以发送。

下面就开始撸代码,一共分为4步:

  1. KafkaProducer有5个构造方法来初始化,这里我们采用第3种传递properties的方式来初始化,所以第一步需要创建properties。

    properties是k,v结构的,种类非常多,不用刻意去记它,平时记住几个常用的,然后额外的用到的时候在官网或者ProducerConfig源码里面去查就可以了。
    producerconfigs官网查询地址: http://kafka.apache.org/documentation/#producerconfigs

    这里我们使用到的参数设置如下:

    Properties props = new Properties();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-1:9092");//kafka集群,broker-list
    props.put(ProducerConfig.ACKS_CONFIG, "all");//all相当于-1
    props.put(ProducerConfig.RETRIES_CONFIG, 1);//重试次数
    props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);//批次大小
    props.put(ProducerConfig.LINGER_MS_CONFIG, 1);//等待时间
    props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);//RecordAccumulator缓冲区大小
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
    
    
    

    在使用的时候,把上面kafka-1改为你自己的kafka主机名或者ip就可以了。

  2. 有了properties之后就可以创建一个生产者对象了,把前面的props传入KafkaProducer就可以了;

    Producer<String, String> producer = new KafkaProducer<>(props);
    
  3. 使用前面创建的producer对象的send方法来发送数据即可,这里作为demo,咱们用for循环发送100条0-99的数据;

    for (int i = 0; i < 100; i++) {
               producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i)));
            } 
    
    
    
    
  4. 发送完记得关闭生产者。

    producer.close(); 
    
    
    
    

到这里一个简单的异步producer Demo就写完了,完整代码如下:

import org.apache.kafka.clients.producer.*;

import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class AsyncProducerDemo {

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

        // 0.配置一系列参数
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-1:9092");//kafka集群,broker-list
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        props.put(ProducerConfig.RETRIES_CONFIG, 1);//重试次数
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);//批次大小
        props.put(ProducerConfig.LINGER_MS_CONFIG, 1);//等待时间
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);//RecordAccumulator缓冲区大小
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

        // 1.创建一个生产者对象
        Producer<String, String> producer = new KafkaProducer<>(props);

        // 2.调用send方法
        for (int i = 0; i < 100; i++) {
            producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i)));
        }
        // 3.关闭生产者
        producer.close();
    }
}

然后在kafka集群某个broker下,输入消费者命令行来验证代码:

bin/kafka-console-consumer.sh --bootstrap-server kafka-1:9092 --topic testKafka1 --from-beginning

执行代码,然后会发现命令行成功输出数据,证明demo完成:
demo截图
反思:如果发送失败了怎么办?生产者对象调用send方法会返回什么?支持回调函数吗?

首先回答第一个问题,如果发送失败了,kafka内部有自己的一套机制,这是我们代码不可控的,它会自动进行重新发送,我们只能控制失败后的重试次数。
究竟这套失败重新发送的机制是怎样的,以及后两个问题,我们下一节带回调函数的异步发送Demo来进行详细介绍。

4.3、带回调函数的异步发送Demo

4.3.1、失败重试机制

在这里插入图片描述
其实kafka的producer发送消息的流程如上图所示,它内部维护了一套失败重试机制,可以看到具体的消息失败重试是kafka内部自动完成的,我们只能控制失败重试的次数

回调函数会在producer收到ack时异步调用,该方法有两个参数,分别是RecordMetadata和Exception,如果Exception为null,说明消息发送成功,如果Exception不为null,说明消息发送失败。

4.3.2、解析回调函数

通过查看KafkaProducer源码,可以发现有两个方法:
send方法
其中有一个支持回调函数Callback,这里的回调函数会在producer收到ack时异步调用,该方法有两个参数,分别是RecordMetadata和Exception,如果Exception为null,说明消息发送成功,如果Exception不为null,说明消息发送失败。

注意:除了发现callback,还可以发现send方法返回的是Future对象,这里先有个印象,在后面讲同步demo的时候会详细解释。

4.3.3、完成带回调函数的异步发送Demo

我们只需要修改send方法并重写onCompletion就可以了,别的都不用更改,代码如下:

for (int i = 0; i < 100; i++) {
      producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i)), new Callback() {

          //回调函数在Producer收到ack时异步调用
          @Override
          public void onCompletion(RecordMetadata metadata, Exception exception) {
              if (exception == null) {
                  System.out.println("消息发送成功->" + metadata.offset());
              } else {
                  exception.printStackTrace();
              }
          }
      });

  }

在java1.8的情况下,还可以使用lambda表达式来优化这段代码:

for (int i = 0; i < 100; i++) {
     //回调函数在Producer收到ack时异步调用
     producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i)), (metadata, exception) -> {
         if (exception == null) {
             System.out.println("消息发送成功->" + metadata.offset());
         } else {
             exception.printStackTrace();
         }
     });

 }

执行代码,会发现符合代码预期结果,带回调函数的异步发送Demo完成:
带回调函数的异步发送消息demo

五、同步发送消息实战

5.1、从源码找到端倪

通过前面查看KafkaProducer源码,我们可以发现两个send方法都是返回Future对象,而Future有一个get方法,并且这个get方法是阻塞的。虽然send是异步的,但是只要我们每次send后都调用它的返回对象的get方法,那就可以实现同步发送消息的目的了,不得不说,kafka的源码设计非常棒,能学到很多东西。

5.2、同步发送Demo

我们只需要修改异步发送demo的调用send方法这里就可以实现同步发送了,具体来说,就是调用send方法返回的get()方法就可以了。不过为了调试方便,我们拿到get()返回的原数据RecordMetadata对象,然后输出这条数据的offset来验证结果。

其实可以通过get()返回的RecordMetadata对象拿到很多原数据的信息:
RecordMetadata提供的方法
修改的代码如下,为了便于调试,我们输出每条信息的offset:

for (int i = 0; i < 100; i++) {
    RecordMetadata metadata = producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i))).get();
    System.out.println("offset = "+metadata.offset());
}

运行代码,查看结果,符合咱们的预期,同步发送消息demo完成:
验证同步发送demo

六、总结

本文对kafka生产者发送消息的流程进行了详细的解释和实战,其中包含了新版本的kafka对于同步发送消息和异步发送消息的api实现,以及kafka源码里的回调函数和架构内部的失败重试机制等都给出了底层的详细解释及实战demo。
完整的代码已上传,感兴趣的可以下载查看。

github:https://github.com/ropleData/kafkaProducerDemo


萧渊之
41 声望20 粉丝