(三)Kafka学习笔记之消费者

Kafka 开发之 Producer 实践

无论将 Kafka 作为消息队列,亦或是消息总线,或是一个数据存储平台,都需要通过生产者 Producer 向 Kafka 写入数据,通过消费者 Consumer 读取 Kafka 的数据。

Producer 概览

有很多场景需要将消息写入 Kafka:记录用户动作、用于审计或分析;存储日志消息;与其他应用程序异步通信;在写入数据库之前缓冲信息等等。

不同的场景意味着不同的需求:每条消息是否很重要?可以接受消息的丢失?可以接受偶尔收到重复的消息?对消息时延和吐吞量有严格的要求?

不同的场景,不同的需求,会影响 Producer API 的使用方式以及配置方式。虽然 Producer API 非常简单,但是在发送消息时,其底层发生了很多事情。
在这里插入图片描述
如上图展示了发送消息到 Kafka 的主要步骤。

  1. 从上图中,从创建一个 ProducerRecord 开始,ProducerRecord 包含消息要发送到哪个 Topic 和消息的内容,也可以声明一个 Key 和 Partition。一旦将 ProducerRecord 发送,Producer 要做的就是序列化 Key 和 Value 对象为二进制数组,这样才可以通过网络发送。
  2. 接着,数据发送到一个 Partitioner,此时如果在 ProducerRecord 中声明了一个 Partitioner 分区,Partitioner 仅仅将我们自定义的 Partition 返回;如果没有声明 Partition,Partitioner 将会选择一个 Partition,通常根据 Key 来选择 Partition。一旦选择了 Partition,Producer 就知道这个消息要发送到哪个 Topic 和哪个 Partition 分区了。
  3. 接着,Producer 将这个消息加入到一个消息批次(batch)中 ,这个消息批次(batch)会发送到相同的 Topic 和 Partition。此时会开辟一个独立的线程来负责发送者批消息到合适的 kafka Broker。
  4. 当 Broker 收到消息,会收到一个响应信息。如果这个消息成功写入 Kafka,Broker 会响应一个RecordMetadata 对象(包括 topic、partition、以及消息在 partition 中的 offset)。如果 Broker 没有将消息写入 Kafka,将会响应一个错误。当 Producer 收到这个错误,可以尝试多次重新发送消息,直到放弃。

创建 KafkaProducer 对象

发送消息到 Kafka,首先需要创建 KafkaProducer 对象。KafkaProducer 对象有三个必备的属性:

  • bootstrap.servers — Kafka Brokers 的 host:port 列表。此列表中不要求包含集群中的所有 Brokers,Producer 会根据连接上的 Broker 查询到其他 Broker。但是列表一般至少需要两个 Broker,因为一个 Broker 连接失败,还可以连接另外一个 Broker。
  • key.serializer — 类的完整名称,此类用于序列化发送消息的 key。Kafka Broker 期望的消息是二进制数组。Producer 接口使用了参数化类型来定义 key.serializer,以发送任何 Java 对象。这就意味着,Producer 必须知道如何将这些 Java 对象转换为二进制数组 byte arrays。key.serializery 应该设置为一个类,这个类实现了 org.apache.kafka.common.serialization.Serializer 接口,Producer 使用这个类可以将 key 对象序列化为 byte array。Kafka 客户端中包括有 ByteArraySerializer,StringSerializer 和 IntegerSerializer 三种类型的序列化器,如果发送常用类型的消息,不需要自定义序列化器。

注意:即使发送的消息只包含 value 的消息,也需要设置 key.serializer。

  • value.serializer — 类的完整名称,此类用户序列化发送的消息的 value。与 key.serializer 含义相同,其值可以与 key.serializer 相同,也可以不同。

创建一个 KafkaProducer 对象。

// 创建一个properties对象,用来封装 Producer 的属性。
private Properties kafkaProps = new Properties();

// 将Kafka Broker的 [host:name]进行封装。
kafkaProps.put("bootstrap.servers", "bigdata01:9092,bigdata02:9092");

//因为要发送字符串类型的消息,所以设置字符串类型的消息key和value序列化器
kafkaProps.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 
kafkaProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

//创建KafkaProducer对象,设置String泛型,传入properties对象
producer = new KafkaProducer<String, String>(kafkaProps); 

可以看出,通过设置 Properties 对象对参数进行封装,可以完成对 Producer 对象的控制。
Kafka 官网文档 --> 3.3 Producer Configs

Producer 发送消息主要有三种方法,这里直接使用 Kafka 官网提供的案例代码:

 Properties props = new Properties();
 props.put("bootstrap.servers", "localhost:9092");
 props.put("acks", "all");
 props.put("retries", 0);
 props.put("batch.size", 16384);
 props.put("linger.ms", 1);
 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++)
    // 这是Kafka的Fire and forget。
     producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)));

 producer.close();
  1. Producer 对象的 send 方法接收一个 ProducerRecord 对象,所以先创建一个 ProducerRecord 对象。ProducerRecord 有多个构造方法。这里使用了两个参数的构造函数:String 类型的 Topic 和String 类型的 Value,其中 value 的类型必须与 value.serializers 一致。当然还有三个参数的构造函数:new ProducerRecord(“Topic”,“key”,“value”)。
  2. 使用 Producer 的 send 方法发送 ProducerRecord 对象,在前面的 Producer 架构图中显示,消息会先放到缓冲区,然后启动一个独立线程发送到 Broker。send 方法返回一个包含 RecordMetadata 的 Future 对象,这里忽略返回值,不关注消息发送是否成功。这类发送消息的方法一般是在允许消息丢失的场景下使用。
  3. 虽然忽略了消息发送到 Kafka 的异常,但是在消息发送到 Kafka 之前,还有可能发生异常。如序列化消息失败异常 SerializationException,缓冲区用尽异常 BufferExhaustedException(配置了 producer 信息,指定在缓冲区满时,不是阻塞,而是抛出异常的情况)、发送中断异常 InterruptException。
  • 同步发送(Synchronous Send) — 发送一个消息,send() 方法返回一个 Future 对象。使用此方法是线程安全的,该对象的 get() 会阻塞方法,直到等待结果返回。
/*
* Since the send call is asynchronous it returns a Future for the RecordMetadata that will be assigned to this record. 
Invoking get() on this future will block until the associated request completes and then return the metadata for the record or throw any exception that occurred while sending the record.
If you want to simulate a simple blocking call you can call the get() method immediately:
*/

 byte[] key = "key".getBytes();
 byte[] value = "value".getBytes();
 ProducerRecord<byte[],byte[]> record = new ProducerRecord<byte[],byte[]>("my-topic", key, value)
 // 同步发送,get()阻塞返回结果
 producer.send(record).get();

这里使用 Future.get() 方法来等待消息发送结果,直到收到 Kafka 的响应。当 Broker 返回错误时,Future 对象会抛出异常,所以我们可以在程序中捕获异常。如果没有异常,我们会得到RecordMetadata 对象,从中可以获取到消息的 offset 等消息

如果在发送数据到 Kafka 之前有任何错误,如果 Kafka Broker 返回一个非重试的异常,或者如果我们耗尽了重试次数,我们将收到一个异常。

Kafka 有两类错误:

  1. 一类是 Retriable(重试类)错误,这类错误通过再次发送消息可以解决,例如连接错误(重试可能会连接成功)和 “ no-leader “ 错误(新的leader选举成功后成功)。重试次数是可以配置的,只有在重试次数用完后,错误依然存在的情况下,客户端才会收到重试类错误。
  2. 另一类错误是非重试类错误,就是说不能通过重试来解决的异常。例如:“message size too large” 错误,此时 KafkaProducer 将不会重试,直接返回异常。
  • 异步发送(Asynchronous Send)— 以回调函数的形式调用 Send(),当收到Broker的响应时,会触发回调函数(Callback() )执行。

此时,我们可以不用知道什么时候发送消息失败,只需要将抛出的异常或者写入错误日志文件中,已备后续分析。

为了能够异步发送消息,并且能处理错误,这种场景需要为 Producer 添加一个 callback 回调函数。

/*
 * Fully non-blocking usage can make use of the Callback parameter to provide a callback that will be invoked when the request is complete.
*/

 ProducerRecord<byte[],byte[]> record = new ProducerRecord<byte[],byte[]>("the-topic", key, value);
 producer.send(myRecord,
               new Callback() {
                   public void onCompletion(RecordMetadata metadata, Exception e) {
                       if(e != null)
                           e.printStackTrace();
                       System.out.println("The offset of the record we just sent is: " + metadata.offset());
                   }
               });

要是使用回调函数,需要实现一个 org.apache.kafka.clients.producer.Callback 接口。回调接口有一个方法 — oncompletion().
如果 Kafka 返回一个错误,oncompletion() 将有一个非空异常。

 Properties props = new Properties();
 props.put("bootstrap.servers", "localhost:9092");
 props.put("transactional.id", "my-transactional-id");
 Producer<String, String> producer = new KafkaProducer<>(props, new StringSerializer(), new StringSerializer());

 producer.initTransactions();

 try {
     producer.beginTransaction();
     for (int i = 0; i < 100; i++)
         producer.send(new ProducerRecord<>("my-topic", Integer.toString(i), Integer.toString(i)));
         producer.commitTransaction();
 } catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
     // We can't recover from these exceptions, so our only option is to close the producer and exit.
     producer.close();
 } catch (KafkaException e) {
     // For all other exceptions, just abort the transaction and try again.
     producer.abortTransaction();
 }
 producer.close();

上述案例都是采用单线程来演示的,但是在一个生产者对象 Producer 可以被多个线程用于发送消息。如果需要更好的吞吐量,可以添加更多使用相同的 Producer 线程,也可以向应用程序添加更多的 Producer 来实现更高的吞吐量。

Producer 相关配置

Producer 有大量的配置参数,都记录在 Apache Kafka 文档中,许多都有合理的默认值,因此没有理由修改每一个参数。

然而,一些参数对内存使用、性能和 Producer 的可靠性有重要的影响。

acks

acks 参数控制了必须有多少个 partition 副本接收到消息,Producer 才认为写入成功。这个参数消息丢失的可能性有重大影响。允许设置的参数值有三个:

  • acks=0:Producer 不会等待 Broker 的回复,Producer 会假定消息已经发送成功,立即返回。
    这意味着如果发生了错误,导致 Broker 没有收到发送的消息,Producer 将不知道这个情况,消息会被丢失。但是,由于 Producer 没有等待服务器的任何响应,它可以以网络支持的速度发送消息,因此可以实现非常高的吞吐量。
  • acks=1:当 Leader Broker 收到消息时,Producer 将从 Broker 接收到消息成功的响应。如果消息没有写入到 Leader 分区,比如 Leader down 掉了,新的Leader还尚未选举出来,此时 Producer 将收到一个 error 响应,并尝试重发此消息,以避免数据丢失。如果消息写入 Leader 分区,而恰好此时 Leader 突然 down 掉了,消息还未同步到其它 Follower 分区,如果有一个 Follower 分区被选举出来为 Leader(unclean leader election)。那么此时消息将会丢失。acks=1 时的吞吐量取决于消息是同步发送还是异步发送。
  • acks=-1 / all:一旦所有处于同步状态的副本都收到消息,Producer 将收到来自于 Broker 的成功的响应。这是最安全的模式,因此这可以确保多个 Broker 接收到了这个消息,并且即使有部分 Broker 崩溃,这个消息也是继续存在。但是这种情况下,消息延时更高,因为这个模式下不止一个 Broker 在接收消息。

注:
acks=0:忽视消息是否发送成功。
acks=1:Leader 接收到消息就返回成功消息响应。
akcs=-1:所有 Broker 都收到消息就返回成功消息响应。

自定义分区器

创建的 ProducerRecord 对象包含一个 topic,key,value。在发送消息时,如果没有设置 key,那么key=null。在真实项目中,key 的值一般都不会为 null。key 的作用有两个:

  1. 可以用于存储消息的额外信息
  2. 用来决定消息将被写到 topic 的哪个分区。

所有具有相同 key 的消息将进入到相同的分区。这意味着,如果一个 Consumer 进程只读取 topic 中分区的一个子集。那么此 Consumer 进程将读取单个 Key 的所有记录。

创建一个 Key-value 消息的方式:

//为消息设置了key值
ProducerRecord<Integer, String> record =new ProducerRecord<>("testTopic", "key", "value"); 

//此时消息的key值为null
ProducerRecord<Integer, String> record =new ProducerRecord<>("testTopic", "value");

当 key 为 null 时且使用默认分区器时,消息将被发送到 topic 的一个可用分区。这是采用轮询算法来选择分区而平衡分区之间的消息。如果消息存在一个 key,并且使用了默认分区器,Kafka 将对 key 求哈希值(也就是说 Kafka 模式使用哈希分区器),并将消息映射到特定的分区。因为相同的 key 总是映射到同一个分区是十分重要的,Kafka 会使用 topic 中的所有分区来计算映射 — 而不仅仅是可用的分区。

key 到分区的映射是一致的,只要 topic 中分区数量不变。因此只要分区的数量是常用,就可以确定。但是,在向 topic 添加新分区时,就不再保证了 — 旧记录将保留在分区中,而新纪录被写入到另外的其它分区(这是由于前后的分区数量发生变化,相应同一个 key 的 hash 值也发生了变化)。如果 key 相当重要,那么就需要在创建足够分区的 topic,放置在后期再更改分区数量。

当然 Kafka 除了拥有 Hash分区器,还可以自定义分区器。

自定义分区器步骤:

  1. 继承 Partitioner 接口;
  2. 实现 partition() 方法;
  3. 使用 props.put(“partitioner.class”, “com.XXX.XXXPartitione”); // 包名
public class BananaPartitioner implements Partitioner{
   
    @Override
    public void configure(Map<String, ?> configs) {
        // TODO nothing
    }

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 得到 topic 的 partitions 信息
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        
        // 指定key=5时,partition为1。
        if(key.toString(),equal("5")){
              return 1;
        }
    
        String num = key.toString();
        return num.substring(0, 5).hashCode() % (num - 1);
    }

    @Override
    public void close() {
        // TODO nothing
    }
}

使用:

props.put("partitioner.class", "包名.BananaPartitioner ");

猜你喜欢

转载自blog.csdn.net/dec_sun/article/details/89389090