kafka生产者

一、消息发送流程

1、创建ProducerRecord对象,ProducerRecord对象需要包含目标主题topic和要发送的消息内容value。还可以指定键key或分区partition。

2、在发送ProducerRecord对象时,会先经过拦截器,在拦截器里面可以对ProducerRecord对象做一些处理。

3、在doSend方法中通过序列化器将key、value序列化成字节数组,方便数据在网络上传输。

4、key、value序列化完毕之后,会确认分区编号。如果ProducerRecord对象中指定了partition,则使用指定的partition;如果没有指定,则将ProducerRecord对象传给分区器。

如果ProducerRecord对象中指定了key,则根据key的hash对该topic的partition个数取余来决定发送到哪个partition;如果ProducerRecord对象没有指定key,则会以轮训的方式分配partition。

注意:上述拦截器、键值序列化器、分区器都可以自定义,只需实现相应的接口即可。

二、创建kafka生产者

1、kafka生产者的创建有3个必要的属性

(1)、bootstrap.servers

  broker的地址清单,格式为host:port。清单里不需要包含所有broker地址,生产者会从给定的broker里找到集群中其他broker的信息。kafka集群环境下建议至少提供两个broker的信息,一旦其中一个宕机,生产者仍然能够连接上集群。多个broker地址用逗号隔开。

(2)、key.serializer

  broker希望接收到的消息的键key和值value都是字节数组。生产者接口允许使用参数化类型,因此需要把java对象作为key和value发送给broker,这样代码可读性会比较高一点,不过生产者需要知道如何把java对象转换成字节数组。key.serializer必须被设置成一个实现了org.apache.kafka.common.serialization.Serializer接口的类,生产者会使用该方式把key序列化成字节数组。注意:key.serializer是必须要设置的,即便生产者在发送ProducerRecord对象时不指定key属性,key.serializer也要设置。

(3)、value.serializer

  与key.serializer一样,value.serializer指定的是value的序列化方式

 1 Properties properties = new Properties();
 2 // 设置key序列化器
 3 // properties.put("key.serializer",
 4 // "org.apache.kafka.common.serialization.StringSerializer");
 5 properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
 6 // 设置value序列化器
 7 // properties.put("value.serializer",
 8 // "org.apache.kafka.common.serialization.StringSerializer");
 9 properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
10 // 设置集群地址
11 // properties.put("bootstrap.servers", brokerList);
12 properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092, localhost:9192, localhost:9292");
13 KafkaProducer<String, String> producer = new KafkaProducer<>(properties);

2、可选属性

(1)、acks

  acks参数表示生产者认为消息发送成功的标识,该参数的设置对消息丢失的可能性有很大影响。

  ①、acks=0:生产者在成功写入消息之前不会等待任何来自服务器的响应(只管发,不管服务器收没收到)。弊端是如果发送过程中出现异常情况,导致服务器没有收到消息,生产者是不知道的,此时就造成消息丢失。优点在于生产者不需要等待服务器的响应,所以可以以网络能够支持的最大速度发送消息,从而达到很高的吞吐量。

  ②、acks=1:只要集群中对应partition的leader节点收到消息,生产者就会收到来自服务器的成功响应。如果消息无法到达leader节点(可能原来的leader节点挂了,新的leader节点还没有选举出来),生产者会收到一个错误的响应,为了避免消息丢失,生产者会重新发送消息。如果leader节点收到消息后还没来得及同步给所有follower节点,此时leader节点挂掉了,恰巧没有收到消息的follower节点经过选举后成为新的leader节点,那么消息还是会丢失。

  ③、acks=all:只有当所有参与复制的节点都收到消息时,生产者才会收到来自服务器的成功响应。这种模式可以避免消息丢失。但是延迟会比前两种模式高,因为它需要等待所有节点都收消息。

(2)、buffer.memory

  该参数表示生产者内存缓冲区的大小,默认是32MB。kafka的客户端发送消息给到服务器,不是来一条就发一条,而是把消息先存入内存缓冲区中,然后把消息收集成一个个的batch(批次)发送给服务器。如果设置的太小,向内存缓冲区写消息的速度又大于从内存缓冲区读消息的速度就会导致内存缓冲区被写满,send()方法就会被阻塞或者抛出异常。

(3)、compression.type

  默认情况下消息是不会被压缩的。该参数的值可以有snappy、gzip、lz4,它指定了消息被发送到broker之前使用哪一种压缩算法对消息进行压缩。

(4)、retries

  当acks=1或者all时,生产者收到来自服务器的错误响应时重新发送消息的次数,如果达到这个次数,生产者会放弃重试并返回错误。默认情况下生产者在每次重试之间会等待100ms,这个等待时间间隔可以通过retry.backoff.ms参数来设置。

(5)、batch.size

  当有多个消息需要被发送到同一个partition时,生产者会把它们放在同一个batch(批次)里。该参数指定一个batch的大小,单位是字节。当batch被填满时,batch里面的所有消息会被发送到服务器。生产者也不一定非得等到batch被填满时才发送,半满甚至只包含一条消息也可能会被发出去,所以batch的大小设置的很大也不会造成延迟,只是会多占用一些内存空间。但是如果设置的太小,生产者就需要频繁的发送消息到服务器,会增加一些额外开销。

(6)、linger.ms

  该参数指定了生产者在发送batch之前等待更多消息加入batch的时间。生产者会在批次填满或者等待达到linger.ms时间时把batch发出去。默认情况下只要有可用线程,就算batch里只有一条消息也会发出去。

(7)、client.id

  该参数可以是任意的字符串,服务器会用它来标识消息的来源。不同的生产者如果设置了不同的client.id,服务器就可以通过client.id区分哪条消息时哪个生产者发的。

(8)、max.in.flight.requests.per.connection

  该参数指定了生产者在收到服务器响应之前可以发送多少个消息。参数值设置成1可以保证消息是按照发送的顺序写入服务器的,即使发生了重试。

(9)、timeout.ms、request.timeout.ms、metadata.fetch.timeout.ms

  timeout.ms指定了leader节点向follower节点同步消息时等待follower节点返回消息确认的时间。与acks=all相匹配,如果在指定时间没有收到follower节点的同步确认响应,leader节点就会返回错误,生产者根据retries次数判断是否要重发。

  request.timeout.ms指定了生产者在发送消息时等待服务器响应的时间。当acks=all时,request.timeout.ms的值要大于timeout.ms的值。

  metadata.fetch.timeout.ms指定了生产者在获取元数据(目标分区的leader节点信息等)时等待服务器响应的时间。如果等待响应超时,生产者要么重试,要么返回错误,要么执行回调。request.timeout.ms的值要大于timeout.ms + metadata.fetch.timeout.ms的值。

(10)、max.block.ms

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

(11)、max.request.size

  该参数表示生产者在一个请求里能发送消息的最大值。假如该参数值设置为2MB,那么生产者可以发送的单个消息最大为2MB,或者生产者在一个请求里发送的batch最大为2MB。broker对自己可以接收的消息最大值也有限制(message.max.bytes),所以两边配置最好相匹配,以避免因生产者发送的消息大小比broker能接收的最大值大而导致消息被broker拒绝。

(12)、receive.buffer.bytes、send.buffer.bytes

  这两个参数分别指定了TCP socket接收和发送数据包的缓冲区大小。如果他们被设置成-1,就使用操作系统的默认值。如果生产者或消费者与broker处于不同数据中心,那么可以适当调大一点,因为跨数据中心的网络一般都有比较高的延迟和比较低的带宽。

顺序保证:

  如果把retries设置成大于0的整数,同时把max.in.flight.requests.per.connection设置成比1大的数,那么如果第一个批次消息写入失败,而第二个批次写入成功,生产者会重试发送第一个批次,此时如果第一个批次也写入成功,那么两个批次的顺序就反过来了。如果某些场景要求消息是有序的,那么消息是否写入成功也很关键,所以不建议把retries设置成0。可以把max.in.flight.requests.per.connection设置成1,这样在生产者尝试发送第一个批次的消息时,就不会有其他的消息发送给broker。这种操作会影响生产者的吞吐量,所以建议只有在需要对消息的顺序有严格要求的情况下使用。

三、消息发送

通过KafkaProducer的send方法发送消息。

1、同步发送

同步发送调用send(ProducerRecord<K, V> record)方法会先返回一个Future对象,然后调用Future对象(Future是接口,具体的实现类是org.apache.kafka.clients.producer.internals.FutureRecordMetadata)的get()方法等待kafka服务器响应。

如果服务器返回错误,get()方法会抛异常。如果没有发生错误,会返回RecordMetadata对象,里面包含消息偏移量等信息。

2、异步发送

异步发送调用send(ProducerRecord<K, V> record, Callback callback)方法,返参与同步发送相同,只是入参比同步发送多了一个回调参数callback,callback是一个实现了org.apache.kafka.clients.producer.Callback接口的类对象,里面只有一个方法

void onCompletion(RecordMetadata metadata, Exception exception) 

如果exception参数不为空,则表示kafka服务器返回了错误。

注:不管应用程序调用生产者的异步还是同步send,生产者都是异步将消息发送给broker,主线程将消息写入缓冲区组成batch,sender线程将batch发送给broker。

四、自定义拦截器、分区器、序列化器

1、自定义拦截器

需要实现org.apache.kafka.clients.producer.ProducerInterceptor<K, V>接口

(1)、onSend方法

允许此方法修改记录,在这种情况下,将返回新记录。 修改键/值的含义是分区分配(如果在ProducerRecord中未指定)将基于修改后的键/值而不是来自客户端的键/值来完成。

(2)、onAcknowledgement方法

当记录已确认发送到服务器,或者在记录发送到服务器失败时,将调用此方法。总之就是生产者发送之后不管成功与否都会调用此方法。

2、自定义分区器

需要实现org.apache.kafka.clients.producer.Partitioner接口,partition方法用来确定发送到哪个具体的分区。

 1 public class MyPartitioner implements Partitioner {
 2 
 3     private static final Map<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();
 4 
 5     /**
 6      * 获取partition编号
 7      *
 8      * @param topic      The topic name
 9      * @param key        The key to partition on (or null if no key)
10      * @param keyBytes   The serialized key to partition on( or null if no key)
11      * @param value      The value to partition on or null
12      * @param valueBytes The serialized value to partition on or null
13      * @param cluster    The current cluster metadata
14      */
15     @Override
16     public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
17         List<PartitionInfo> partitionInfoList = cluster.availablePartitionsForTopic(topic);
18         int availableCounter = partitionInfoList.size();
19         int index;
20         // 如果key是空的
21         if (Objects.isNull(keyBytes) || keyBytes.length == 0) {
22             int nextValue = this.nextValue(topic);
23             index = nextValue % availableCounter;
24         } else {
25             index = Utils.toPositive(Utils.murmur2(keyBytes)) % availableCounter;
26         }
27         int partitionNumber = partitionInfoList.get(index).partition();
28         System.out.println("partitioner分区器=========================partitionNumber=" + partitionNumber + "=========================");
29         return partitionNumber;
30     }
31 
32     /**
33      * This is called when partitioner is closed.
34      */
35     @Override
36     public void close() {
37 
38     }
39 
40     /**
41      * Configure this class with the given key-value pairs
42      *
43      * @param configs
44      */
45     @Override
46     public void configure(Map<String, ?> configs) {
47 
48     }
49 
50     /**
51      * 当没有指定key时,对partition进行轮训
52      * @param topic The topic name
53      * @return next number
54      */
55     private int nextValue(String topic) {
56         AtomicInteger counter = topicCounterMap.get(topic);
57         if (Objects.isNull(counter)) {
58             counter = new AtomicInteger(0);
59             AtomicInteger value = topicCounterMap.putIfAbsent(topic, counter);
60             if (Objects.nonNull(value)) {
61                 counter = value;
62             }
63         }
64         return counter.getAndUpdate((operand) -> operand + 1);
65     }
66 }

3、自定义key、value序列化器

实现org.apache.kafka.common.serialization.Serializer<T>接口并重写 byte[] serialize(String topic, T data) 方法。

posted @ 2021-02-24 16:06  西北-孤狼  阅读(146)  评论(0)    收藏  举报