第二章 Kafka生产者

本文详细介绍了Kafka的生产者工作流程,包括同步发送与异步发送、自定义分区器和序列化。生产者需要配置参数如bootstrap.servers、key.serializer和value.serializer。默认分区器根据消息key进行分区,也可自定义分区器以满足特定需求。此外,讨论了自定义序列化/反序列化以适应复杂内容的发送,并列举了一些关键的生产者配置参数,如acks和retries,以确保消息的可靠性和系统性能。

生产者

在kafka中,我们需要把一些数据发送到kafka服务器中,完成这一工作的角色有 Producer 来承担。

使用生产者例子
public class SimpleProducer {
    public static void main(String[] args) throws Exception {
        String key = "this is key";
        String value = "this is value";
        String topicName = "SimpleProducerTopic";
        
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        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);
        ProducerRecord<String,String> record = new ProducerRecord<>(topicName,key,value);
        
        producer.send(record);
        producer.close();  // 关闭资源,防止泄漏
    }
}

从上面的例子来看,我们可以概括发送一条消息,主要分为三个部分:

  • 创建 kafkaProducer 对象
  • 创建 ProducerRecord 消息实体对象
  • 发送消息

为了创建 kafkaProducer 对象,我们需要至少配置三个:

  • bootstrap.servers:主机列表
  • key.serializer:键序列化
  • value.serializer:值序列化

bootstrap.servers:它是Brokers的主机地址,生产者可以根据这个配置连接kafka集群。我们指定一个或多个主机地址。为了提供可用性,这里推荐是至少两个主机地址,就算其中一个挂了,还可以连接到其他的Broker上。

key.serializervalue.serializer :从kafka的角度去看,一个消息都是由字节数组构成。我们需要把keyvalue 都转成字节数组。

生产者工作流程

在这里插入图片描述

该图描述生产者工作流程。下面我们一起来看一下具体步骤。

  • 生产者配置

    配置生产者的参数,这里只配置bootstrap.servers、key.serializer、value.serializer。

    当然还可以根据实际情况,配置其他参数

  • 创建消息

    创建消息记录,一个消息记录由五个字段构成

    • topic name:主题名称
    • partition number:分区号
    • timestamp:时间戳
    • key
    • value
  • 序列化()

    生产者将根据配置序列化参数去序列化消息的key和value

    当然,我们可以自定义序列化

  • 分区()

    然后把消息发送到分区器。分区器将给每个消息分配一个分区号(partition number)。

    默认的分区器将会根据消息的key进行分区,对key进行hash取值,然后得到分区号;

    如果key没有被显式指定,分区器将均匀分配消息到每个partitions中,这里采用的轮询的算法

  • 分区缓存

    一旦消息有了分区号,分区器将准备发送消息到Broker,但是并不是立即发送。消息会被缓存到partition buffer中。每个生产者都有一个partition buffer,这样可以积累多条消息,批量发送。我们可以通过 batch.size 参数控制批量发送大小,linger.ms 参数控制滞留时间

  • 重试与成功响应

    如果Broker保存这些消息,Broker将发送 RecordMetadata 的对象响应生产者。如果发送一些错误,生产者将收到错误。如果错误属于可恢复性错误(集群中leader挂了,然后重新选举),生产者会重试发送消息。

同步发送vs异步发送

同步发送消息:生产者发送一条消息并且等待,直到接受到Broker的响应。如果成功,我们得到的响应是 RecordMetadata 对象;否则我们会得到一个异常。这样我们就可以知道消息有没有被丢失。同步发送将会阻塞直到broker响应,这将会影响到系统的吞吐量。下面给出同步发送的示例

try {
	producer.send(record).get();
} catch (Exception e) {
    System.out.println("SynchronousProducer failed with an exception");
} finally {
    producer.close();
}

异步发送:异步发送有两种方式。一种是直接发送,不考虑是否成功;另一种是直接发送,成功或失败触发回调函数。第一种方式,如果消息丢失对业务没有太大影响,可以选择这样的方式;否则选择第二种方式。s

Fire-and-forget producer

producer.send(record);

回调 producer

producer.send(record, (rm, ex) -> {
    if (ex != null) {
        System.out.println("AsynchronousProducer failed with an exception");
    }
})
自定义分区器

默认的分区器有3条规则:

  • 如果生产者发送的record指定分区号,直接使用这个分区号
  • 如果消息没有硬编码的分区号,指定key,根据key进行hash,确定分区号
  • 如果既没有指定分区号也没有指定key,分区器将采用轮询算法,均匀分配

为什么需要自定义分区器呢?

因为默认的分区器有存在两个局限性:

  • 不同的key可能有相同的hash值。
  • 分区号是由key的hash值取模主题的分区数得来。如果你给主题增加分区,原来的key的取模值会发送变化。

所以,在一些场景,我们需要自定义分区器来满足我们需求。例如,我们需要tagA的消息放在前一半的分区,tagB的消息放在后一半分区。

public class SensorProducer {
    public static void main(String[] args) throws Exception {
        String topicName = "SensorTopic";

        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092,localhost:9093");
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("partitioner.class", "SensorPartitioner");
        props.put("tag.name", "tagA");
        
        Producer<String, String> producer = new KafkaProducer(pros);
        
        for (int i = 0; i < 10; i++)
            producer.send(new ProducerRecord<>(topicName, "tagA", "tagA-"+i));
        for (int i = 0; i < 10; i++) 
            producer.send(new ProducerRecord<>(topicName, "tagB", "tagB-"+i));
        
        producer.close();
        System.out.println("SimpleProducer Completed.");
    }
}

这里的生产者配置参数和上述基本类似。仅有和上述不同的参数 partitioner.class ,我们自定义的分区器限定名。tag.name 参数不是kafka自带参数,而是我们自定义的参数。

我们自定义分区器需要实现 Partitioner 接口,并重写三个方法

  • Configure
  • Partition
  • Close
public class SensorPartitioner implements Partitioner {
    private String tagName;
    
    public void configure(Map<String, ?> configs) {
        tagName = configs.get("tag.nam").toString();
    }
    
    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();
        int sp = (int) Math.abs(numPartitions * 0.5);
        int p = 0;

        if ((keyBytes == null) || (!(key instanceof String)))
            throw new InvalidRecordException("All messages must have sensor name as key");

        if (((String) key).equals(tagName))
            p = Math.abs(key.hashCode()) % sp;
        else
            p = Math.abs(key.hashCode()) % (numPartitions - sp) + sp;

        System.out.println("Key = " + (String) key + " Partition = " + p);
        return p;
    }
}
自定义序列化/反序列化

在之前的例子中我们都是发送一些简单内容,如果我们需要发送一些复杂的内容(对象等)我们不能使用 StringSerializer 来序列化我们的内容,而且需要自定义序列化、反序列化帮助我们来完成工作。不过,在实际开发并不推荐自定义序列化、反序列化,可以使用通用的框架,Avro 等。首先,我们需要实现 org.apache.kafka.common.serialization.Serializer 接口,实现接口的三个方法:

  • configure
  • close
  • serialize

其中,configure 方法在初始化只被调用一次,close 方法用来做一些清理工作,也是只会被调用一次。serialize 方法就是用来把内容序列化字节数组或反序列化对象。

自定义序列化

public class SupplierSerializer implements Serializer<Student> {
    private String encoding = "UTF8";

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {

    }

    @Override
    public void close() {

    }

    @Override
    public byte[] serialize(String s, Student student) {
        if (student == null) {
            return null;
        }
        try {
            byte[] nameData = student.getName().getBytes(encoding);
            ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + nameData.length);
            buffer.putInt(student.getId());
            buffer.putInt(nameData.length);
            buffer.put(nameData);

            return buffer.array();
        } catch (UnsupportedEncodingException e) {
            throw new SerializationException("Error when serializing string to byte[] due to unsupported encoding " + this.encoding);
        }
    }
}

反序列化

public class SupplierDeserializer implements Deserializer<Student> {
    private String encoding = "UTF8";

    @Override
    public Student deserialize(String s, byte[] data) {
        if (data == null) {
            System.out.println("Null recieved at deserialize");
            return null;
        }
        try {
            ByteBuffer buf = ByteBuffer.wrap(data);
            int id = buf.getInt();
            int nameLength = buf.getInt();
            byte[] nameData = new byte[nameLength];
            buf.get(nameData);
            String name = new String(nameData, encoding);

            return new Student(id, name);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {

    }

    @Override
    public void close() {

    }
}
生产者常用配置参数

生产者有许多的配置参数,并有些都具有默认值。这里列举一些常用的参数。

  • bootstrap.servers
  • key.serializer
  • value.serializer
  • partitioner.class

影响kafka吞吐量和性能的几个参数:

  • acks
  • retries
  • max.in.flight.requests.per.connection

acks是broker响应的,当生产者发送一条消息到broker时,生产者将会得到一个响应,这个响应要么是 RecordMetaData 或 异常。参数 acks 的值:0,1,all。如果我们设置 acks 的值为0,生产者将不会等待响应,这样做将会有三个影响:

  • 可能出现消息丢失
  • 高吞吐量
  • 不会重试

如果我们的系统允许出现消息丢失的情况,可以配置为0。

如果我们设置 acks 的值为1,那么生产者发送消息后将会等待响应(只要leader写入成功,就返回响应),如果leader宕机和分发消息失败了,生产者将会在一段时间后重试发送。但是这样做,也会出现消息丢失的情况,因为我们只保证写入leader成功,没有保证写入复制集群成功。

如果想要保证消息百分之百的可靠性,我们可以设置 acks 的值为 all。但是这样系统将会变得慢,因为我们需要等待消息写入所有broker中。然后,我们使用异步发送的方式提高系统性能。

当消息发送失败时,生产者将会重试。那么会重试几次呢?重试的时间间隔又是多少呢?

retries 参数就是用来控制重试的次数。retry.backoff.ms. 用来控制重试的时间间隔,默认值是100毫秒。

max.in.flight.requests.per.connection 一次请求可以发送最大消息数量。我们设置它为比较高的值(值越高占用内存越大),使用异步发送提供系统性能。但是如果是异步发送将会顺序问题:假设我们发送10消息到一个分区中,第一批次发送了5条消息失败,第二批次发送5条消息成功,然后重新发送第一批次的消息成功了。它们到达分区的顺序发生了改变了。如果消息顺序对我们很重要,我们必须这样处理:

  • 使用异步发送
  • max.in.flight.requests.per.connection=1
参考
kafka生产者配置参数包括bootstrap.servers、key.serializer和value.serializer。bootstrap.servers参数用来指定生产者客户端连接Kafka集群所需的broker地址清单,具体的格式为host1:port1,host2:port2,可以设置一个或多个地址,中间以逗号隔开。建议至少设置两个地址。key.serializer和value.serializer参数用来指定key和value的序列化器,用于将数据序列化为字节数组。这两个参数没有默认值,必须填写序列化器的全限定名。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Kafka参数详解及调优--生产者](https://blog.youkuaiyun.com/weixin_35852328/article/details/90479787)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [kafka生产者-实例参数配置.md](https://download.youkuaiyun.com/download/m0_43393325/12658181)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [【深入理解Kafka系列】 第二章 生产者](https://blog.youkuaiyun.com/u013308490/article/details/127116085)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值