Kafka生产者-概念速览|配置参数|序列化|分区——《Kafka权威指南》笔记

Kafka生产者——向Kafka写入数据

除了内置的客户端外,Kafka还提供了二进制连接协议,也就是说,我们直接向Kafka网络端口发送适当的字节序列,就可以从Kafka读取消息或往Kafka写入消息。因此还有好多语言实现的Kafka客户端,比如C++,Python,Go,不仅限于Java。

生产者概览

一个应用程序在很多情况下需要向Kafka写入消息:记录用户活动、保存日志信息、记录只能家电信息、与其他应用程序进行异步通信缓冲即将写入数据库的数据,等等。

消息发送过程

尽管生产者API使用起来很简单,但消息的发送过程还是挺复杂的。

在这里插入图片描述

1)我们首先创建一个 ProducerRecord 对象开始,ProducerRecord 对象需要包含目标主题和要发送的内容,还可以指定键和分区。

2)在发送 PR 时,生产者需要先把键值对象序列化成字节数组,这样才能在网络上传输。

3)接下来,数据被传给分区器。如果 PR 中已经指定了分区,那么分区器就不会再做任何事情了,直接把指定的分区返回。如果没有,分区器则会根据 PR 中的键来选择一个分区

4)紧接着,这条记录就会添加到一个记录批次里,这个批次的所有消息都会被发送到相同的主题和分区里

5)之后,有一个独立的线程将这些记录批次发送给相应的broker上。

6)服务器在收到这些消息时会返回一个响应,如果消息发送成功写入Kafka中,就会返回一个 RecordMetaData 对象,它包含着主题和分区信息,以及记录在分区里的偏移量。

创建 Kafka 生产者

想要写入Kafka,首先要创建一个生产者对象,并设置一些属性。Kafka生产者有3个必选属性。

bootstrap.servers

​ 该属性指定 broker 的地址清单,地址格式为:host:port。建议提供两个broker信息,其中一个宕机,生产者仍然能够连接到集群上。

key.serializer

​ broker希望接收到的消息的键值都是字节数组,不过生产者需要将键值所包装的Java对象转成字节数组。Kafka客户端默认提供 ByteArraySerializer、StringSerializer 和 IntegerSerializer 的序列化器。

value.serializer

如果键是整数类型而值是字符串,那么需要使用不同的序列化器。

Properties kafkaProps = new Properties();
        
        kafkaProps.put("bootstrap.servers","hadoop102:9092,hadoop103:9092");
        kafkaProps.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        kafkaProps.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");

        KafkaProducer<String,String> producer = new KafkaProducer<String, String>(kafkaProps);

此时实例化生产者后,接下来就可以发送开始发送消息了。

发送消息有以下3种方式。

  • 发送并忘记(fire-and-forget)
    • 我们把消息发送给服务器,但并不关心它是否到达。
  • 同步发送
    • 我们使用send()方法发送消息,它会返回一个 Future 对象,调用 get() 方法进行等待,就可以知道消息是否发送成功了。
  • 异步发送
    • 我们调用 send() 发送,并指定一个回调函数,服务器在返回响应时会调用该函数。
		//发送并忘记
        producer.send(record);

        //同步发送
        try {
            RecordMetadata metadata = producer.send(record).get();
            Long offset = metadata.offset();
        } catch (Exception e) {
            e.printStackTrace();
        }

        //异步发送
        producer.send(record, new Callback() {
            @Override
            public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                if (e != null){
                    e.printStackTrace();
                }
            }
        });

生产者的配置

  • acks
    • acks参数表明了必须多少个副本收到了消息,生产者才会认为消息写入成功的。这个参数对于消息丢失的可能性有着重要影响
      • 设置 acks = 0 的话,生产者在成功写入消息之前不会等待任何服务器响应。但如果当中出现了消息丢失,生产者也无从得知,好处就是最大速度发送消息,从而达到很高的吞吐量
      • 设置 acks = 1 的话,只要集群首领节点收到消息,生产者就会收到一个来自服务器的成功响应。但如果消息无法到达首领节点(leader节点崩溃,新的leader没有被选举出来),生产者就会收到错误响应并重发。不过一个没有收到消息的新leader,消息还是会丢失。
      • 设置 acks = all 的话**,只有当所有参与复制的节点全部收到消息时**,生产者才会收到一个来自服务器的成功响应。这种模式是最安全的,不过延迟比acks=1会更高
  • buffer.memory
    • 用来设置生产者内存缓冲区大小,生产者用它缓冲要发送到服务器的消息。如果应用程序发送消息的速度超过发送服务器的速度,就会导致生产者空间不足。这个时候,send()方法调用要么阻塞,要么抛出异常,取决于如何设置 block.on.buffer.full 参数
  • compression.type
    • 默认情况下,消息发送时不会被压缩。该参数可以设置为 snappy,gzip,它指定了消息被发送给broker之前使用哪种压缩算法进行压缩。
      • snappy压缩算法 是由Google发明,它占用较少的CPU,能够提供更好的性能和可观的压缩比,用于考虑性能和网络带宽。
      • gzip压缩算法 一般会占用较多CPU,但会提供更高的压缩比,因此如果网络带宽有限,可以使用该算法。
    • 使用压缩可以降低网络传输和存储开销,而这往往是向 Kafka 发送消息的瓶颈所在
  • retries
    • 因为生产者向服务器发送消息可能收到的时临时性的错误(比如分区找不到leader)。在这种情况下,reties参数决定着生产者可以重发消息的次数,默认每次重试次数等待100ms,不过可以通过 retry.backoff.ms 参数来改变这个时间间隔。
      • 我们可以先测试以下恢复一个崩溃节点需要多少时间
  • batch.size
    • 该参数指定了一个批次可以使用的内存大小,按照字节计算。当批次被填满,批次中所有的信息会被发送出去。当然也不一定填满会发送(还要考虑linger.ms参数),过大会占用内存,过小会频繁发送消息,增加开销。
  • linger.ms
    • 该参数指定了生产者在发送批次之前等待更多消息加入批次的时间。KafkaProducer会在批次填满或者linger.ms达到上限时,就把批次发送出去
      • 如果设置比0大的数,虽然会增加延迟,但会提升吞吐量。(因为一次性发送更多的消息,每条消息开销就变小了。
  • max.in.flight.requests.per.connection
    • 该参数指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高,占用的内存就会越高,不过会提升吞吐量。
  • timeout.ms、request.timeout.ms、meta.fetch.timeout.ms
    • req.ms 指定了生产者在发送消息时等待服务器返回响应的时间
    • meta.ms 指定了生产者获取元数据时服务器返回响应的时间
    • time.ms 指定了broker等待同步副本返回消息确认的时间,与acks的配置向匹配——如果指定时间里没有收到同步副本的确认,那么broker就会返回一个错误。
  • max.request.size
    • 用于控制生产者发送的请求大小,能发送单个消息的最大值。另外,broker对可接收的消息最大值也有自己的限制 message.max.bytes ,所以两边的配置最好可以匹配,避免生产者发送消息被broker拒绝。
顺序保证

Kafka 可以保证在同一个分区内的顺序是一致的。

有些场景对于顺序是非常重要的,比如银行。

因此如果 retries 参数设置成非零整数,同时把 max.in.flight.requeset.per.connection 设为比1大的数,那么如果第一个批次的消息写入失败,而第二次的消息写入成功,而broker将重新写入第一批次。那么此时两个批次的顺序就会反过来。

因此,如果某些场景要求消息是有序的,消息写入成功也是很重要的,所以不建议把 reties 设为0,但可以把 request.per.connection 设为 1,这样生产者发送第一批消息时,就不会有其他消息发送给broker。

序列化器

之前我们讲到实例化一个生产者,就必须要指定一个序列化器,虽然Kafka提供了默认的字符串序列化器,整型和字节数组序列化器,不过不足以满足大部分场景的需求,因为我们要序列化的记录类型越来越多,越来越复杂。

自定义序列化器

如果我们发送到 Kafka 的对象如果不是简单字符串和整型,那么可以使用序列化框架,如 Arvo、Thrift、Protobuf,或者使用自定义序列化器。当然使用现成的序列化框架最好。

如果我们需要发送的消息是一个Customer对象的话

/**
 * @Author Juniors Lee
 * @Date 2021/11/16
 */
public class Customer {

    private int customID;

    private String customerName;

    public int getCustomID() {
        return customID;
    }

    public void setCustomID(int customID) {
        this.customID = customID;
    }

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }
}

因此我们要自定义的序列化器。

/**
 * @Author Juniors Lee
 * @Date 2021/11/16
 */
public class CustomerSerializer implements Serializer<Customer> {

    @Override
    public void configure(Map<String, ?> map, boolean b) {
        
        //不做任何配置
    }

    @Override
    /**
     * Customer 对象被序列化成:
     * 表示 customerID 的4字节整数
     * 表示 customerName 长度的4字节整数
     * 表示 customerName 的N个字节
     */
    public byte[] serialize(String s, Customer customer) {
        
        byte[] serializedName;
        int stringSize;
        
        if (customer == null)
            return null;
        else {
            if (customer.getCustomerName() != null){
                serializedName = customer.getCustomerName().getBytes(StandardCharsets.UTF_8);
                stringSize = serializedName.length;
            }else {
                serializedName = new byte[0];
                stringSize = 0;
            }
        }
        ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + stringSize);
        buffer.putInt(customer.getCustomID());
        buffer.putInt(stringSize);
        buffer.put(serializedName);
        
        return buffer.array();
    }

    @Override
    public void close() {

        //不需要关闭任何东西
    }
}
使用 Avro 序列化

Apache Avro 是一种与编程语言无关的序列化格式。Doug Cutting创建的这个给项目,目的是为了一种共享数据文件的形式。

Avro 数据是通过与语言无关的 schema 来定义的。而 schema 是通过 JSON 来描述,数据被序列化成二进制或JSON文件。

Avro 有一个很有意思的特性是,当负责写消息的应用程序使用了新 schema,负责读的应用程序可以继续处理消息而无需做出任何改动,这点很适合Kafka上使用。

在这里插入图片描述

分区

ProducerRecord对象包括了目标主题、键和值。Kafka的消息是一个个键值对,当然键可以为null。不过大部分的应用程序都要用到键,一是作为消息的附带信息,二是能够用来决定消息该写到主题的哪个分区,相同的键分配到相同的分区中。一个进程只会从一个主题中读取数据。

如果键值为 null,而且使用了默认的分区器,那么记录将被随机发送到主题内各个可用的分区上。分区器使用的是轮询(Round Robin)算法将消息均衡地发布在各个分区上

当然如果键不为null,而且使用的默认分区器,那么Kafka会对键进行散列,然后根据散列值将消息映射到特定的分区上,而这样的映射,我们将用到主题所有的分区,而不仅仅是可用的分区。这也意味着,如果写入数据的分区也可能是不可用的,当然这个情况很少发生。

只有在不改变主题分区的数量的情况下,键与分区之间的映射才能保持不变,因此最好在创建主题的时候就把分区规划好。

自定义分区器Demo
/**
 * @Author Juniors Lee
 * @Date 2021/11/16
 */
public class BananaPartitioner implements Partitioner {

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster){

        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        
        if ((keyBytes == null) || (!(key instanceof  String)))
            throw new InvalidRecordException("We expect all messages to have customer name as key")
        if (((String)key).equals("Banana"))
            return numPartitions; //Banana总是分配到最后一个分区
        
        //其他记录被散列在其他分区
        return (Math.abs(Utils.murmur2(keyBytes)) % (numPartitions - 1));

    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值