Kafka生产者详解

一、生产者发送消息的过程

   Kafka 生产者发送消息的过程:

  • Kafka 会将发送消息包装为 ProducerRecord 对象, ProducerRecord 对象包含目标主题和要发送的内容,同时还可以指定键和分区。在发送 ProducerRecord 对象前,生产者会先把键和值对象序列化成字节数组,这样才能够在网络上传输。
  • 接下来,数据被传给分区器。如果之前已经在 ProducerRecord 对象里指定了分区,那么分区器就不会再做任何事情。如果没有指定分区 ,那么分区器会根据 ProducerRecord 对象的键来选择一个分区,紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的 broker 上。
  • 服务器在收到这些消息时会返回一个响应。如果消息成功写入 Kafka,就返回一个 RecordMetaData 对象,包含了主题和分区信息,以及记录在分区里的偏移量。如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,如果达到指定的重试次数后还没有成功,则直接抛出异常,不再重试。

  

  项目依赖

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

  创建生产者

  创建 Kafka 生产者时,以下三个属性是必须指定的:

  • bootstrap.servers :指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错;
  • key.serializer :指定键的序列化器;
  • value.serializer :指定值的序列化器。
public class SimpleProducer {

    public static void main(String[] args) {
        String topic = "Partitioner-Test";
        Properties prop = new Properties();
        prop.put(ProducerConfig.ACKS_CONFIG, "1");

        // 发送失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,
        // 但是也可能造成消息重复发送,比如网络抖动,所以需要在接收者那边做好消息接收的幂等性处理
        prop.put(ProducerConfig.RETRIES_CONFIG, 3);
        // 重试间隔设置
        prop.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
        //如果设置了该缓冲区,消息会先发送到本地缓冲区,可以提高消息发送性能,默认值是33554432,即32MB
        prop.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);

        // kafka本地线程会从缓冲区取数据,批量发送到broker,
        // 设置批量发送消息的大小,默认值是16384,即16kb,就是说一个batch满了16kb就发送出去
        prop.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);

        //默认值是0,意思就是消息必须立即被发送,但这样会影响性能
        //一般设置100毫秒左右,就是说这个消息发送完后会进入本地的一个batch,
        // 如果100毫秒内,这个batch满了16kb就会随batch一起被发送出去
        //如果100毫秒内,batch没满,那么也必须把消息发送出去,不能让消息的发送延迟时间太长
        prop.put(ProducerConfig.LINGER_MS_CONFIG, 100);

        prop.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.21.120:9092");
        prop.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        prop.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        prop.put("partitioner.class", "com.smile.producer.CustomerPartitioner");
        prop.put("pass.line", 6);

        KafkaProducer<Integer, String> producer = new KafkaProducer<>(prop);
        asyncSend(topic, producer);
        producer.close();
    }public static void asyncSend(String topic, KafkaProducer<Integer, String> producer) {
        for (int i = 0; i < 10; i++) {
            ProducerRecord<Integer, String> record = new ProducerRecord(topic, "hai "+i, "wonder- " + i);
            if (i % 5 == 0) {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            producer.send(record, (recordMetadata, e) -> {
                if (e != null) {
                    System.out.printf("发送消息异常:%s", e.getMessage());
                } else {
                    System.out.printf("topic=%s,partition=%s,offset=%s \n",
                            recordMetadata.topic(), recordMetadata.partition(), recordMetadata.offset());
                }
            });
        }
    }
}

     启动消费者

# bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic --from-beginning

  同步发送消息

  在调用 send 方法后可以接着调用 get() 方法,send 方法的返回值是一个 Future<RecordMetadata>对象,RecordMetadata 里面包含了发送消息的主题、分区、偏移量等信息。

    public static void syncSend(String topic, KafkaProducer<String, String> producer) {
        for (int i = 0; i < 10; i++) {
            try {
                ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, "hello " + i, "word " + i);
                RecordMetadata metadata = producer.send(producerRecord).get();
                System.out.printf("topic=%s, partition=%d, offset=%s \n", metadata.topic(), metadata.partition(), metadata.offset());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

  异步发送消息

for (int i = 0; i < 10; i++) {
    ProducerRecord<String, String> record = new ProducerRecord<>(topicName, "k" + i, "world" + i);
    /*异步发送消息,并监听回调*/
    producer.send(record, new Callback() {
        @Override
        public void onCompletion(RecordMetadata metadata, Exception exception) {
            if (exception != null) {
                System.out.println("进行异常处理");
            } else {
                System.out.printf("topic=%s, partition=%d, offset=%s \n",
                        metadata.topic(), metadata.partition(), metadata.offset());
            }
        }
    });
}

  自定义分区器

  Kafka 有着默认的分区机制:

  • 如果键值为 null, 则使用轮询 (Round Robin) 算法将消息均衡地分布到各个分区上;
  • 如果键值不为 null,那么 Kafka 会使用内置的散列算法对键进行散列,然后分布到各个分区上。

  某些情况下,可能有着自己的分区需求,这时候可以采用自定义分区器实现。这里给出一个自定义分区器的示例:

public class CustomerPartitioner implements Partitioner {
    private int passLine;

    @Override
    public void configure(Map<String, ?> configs) {
        passLine = (Integer) configs.get("pass.line");
    }

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        return (Integer) key > passLine ? 1 : 0;
    }

    @Override
    public void close() {
        System.out.printf("分区器关闭...");
    }

}

         需要创建一个至少有两个分区的主题:

 bin/kafka-topics.sh --create \
                    --bootstrap-server 192.168.21.120:9092 \
                     --replication-factor 1 --partitions 2 \
                     --topic Partitioner-Test

`  其他配置属性

1. acks

acks 参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的:
acks=0 : 消息发送出去就认为已经成功了,不会等待任何来自服务器的响应;
acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应;
acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。

2. buffer.memory

  设置生产者内存缓冲区的大小。

3. compression.type

  默认情况下,发送的消息不会被压缩。如果想要进行压缩,可以配置此参数,可选值有 snappy,gzip,lz4。

4. retries

  发生错误后,消息重发的次数。如果达到设定值,生产者就会放弃重试并返回错误。

5. batch.size

  当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。

6. linger.ms

  该参数制定了生产者在发送批次之前等待更多消息加入批次的时间。

7. clent.id

  客户端 id,服务器用来识别消息的来源。

8. max.in.flight.requests.per.connection

  指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量,把它设置为 1 可以保证消息是按照发送的顺序写入服务器,即使发生了重试。

9. timeout.ms, request.timeout.ms & metadata.fetch.timeout.ms

  • timeout.ms 指定了 borker 等待同步副本返回消息的确认时间;
  • request.timeout.ms 指定了生产者在发送数据时等待服务器返回响应的时间;
  • metadata.fetch.timeout.ms 指定了生产者在获取元数据(比如分区首领是谁)时等待服务器返回响应的时间。

10. max.block.ms

  指定了在调用 send() 方法或使用 partitionsFor() 方法获取元数据时生产者的阻塞时间。当生产者的发送缓冲区已满,或者没有可用的元数据时,这些方法会阻塞。在阻塞时间达到 max.block.ms 时,生产者会抛出超时异常。

11. max.request.size

  该参数用于控制生产者发送的请求大小。它可以指发送的单个消息的最大值,也可以指单个请求里所有消息总的大小。例如,假设这个值为 1000K ,那么可以发送的单个最大消息为 1000K ,或者生产者可以在单个请求里发送一个批次,该批次包含了 1000 个消息,每个消息大小为 1K。

12. receive.buffer.bytes & send.buffer.byte

  这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。

posted on 2021-09-20 11:40  溪水静幽  阅读(303)  评论(0)    收藏  举报