kafka生产端拦截器、序列化器、分区器的详细介绍

2,095 阅读6分钟

前言

kafka生产者客户端由两个线程协调运行,这两个线程分别为主线程和sender线程。主线程中由kafkaProducer创建消息,然后通过拦截器、序列化器和分区器的不同模块处理,之后缓存到消息累加器中。那么拦截器、序列化器和分区器到底是怎样工作,下面我们一起来一探究竟~

拦截器

用过Spring Interceptor的同学,一定对拦截器这个概念不陌生。kafka一共有两种拦截器:生产者拦截器消费者拦截器。生产者拦截器有两个方面的作用,既可以用来在消息发送前做一些预处理工作,比如给消息添加前缀内容等;也可以用来在发送回调逻辑前做一些定制化的工作,比如统计成功率等。

要怎样使用生产者拦截器呢?首先我们要实现org.apache.kafka.clients.producer Interface ProducerInterceptor接口,然后通过参数interceptor.classes来配置,告诉Producer客户端我们增加了拦截器即可。
Interface ProducerInterceptor接口的3个方法:

public interface ProducerInterceptor<K, V> extends Configurable {
    // 该方法会在消息发送之前被调用
    ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
    // 该方法会在消息成功提交或发送失败之后被调用
    void onAcknowledgement(RecordMetadata metadata, Exception exception);
    // 关闭interceptor,主要用于执行一些资源清理工作
    void close();
}
onSend()

该方法会在KafaProducer将消息序列化和计算分区之前就调用到。你可以修改消息内容,不过一般不建议修改ProducerRecord的topic、key、partition等信息。另外如果在这里修改了key/value之后,到了分区器拿到的key/value就是修改后的值,而不是原始在客户端发出的key/value。修改key不仅会影响分区的计算,同样会影响到broker服务端日志压缩(Log Compaction)的功能异常。

实现自定义的ProducerInterceptorImpl之后,需要在KafkaProducer的配置参数ProducerConfig.INTERCEPTOR_CLASSES_CONFIG指定这个拦截器。如果没指定,默认值为空,即没有拦截器功能。

interceptor.classes
A list of classes to use as interceptors. Implementing the org.apache.kafka.clients.producer.ProducerInterceptor interface allows you to intercept (and possibly mutate) the records received by the producer before they are published to the Kafka cluster. By default, there are no interceptors.

properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
    ProducerInterceptorPrefix.class.getName());

KafkaProducer是支持配置多个拦截器以形成拦截链。如果是配置多个拦截器,那么拦截链会按照interceptor.classes参数配置的拦截器的顺序逐一执行。并且下一个拦截器会以上一个拦截器的输入内容作为输入内容继续处理,但是上一个拦截器的异常错误输出不会导致下一个拦截器的异常或阻塞。如果上一个拦截器执行失败,那么下一个拦截器会接着从上一个执行成功的拦截器的结果继续执行。

onAcknowledgement()

该方法是KafkaProducer在消息被成功应答或者发送失败时被调用,还要优于用户设定的Callback方法之前执行。
这个方法通常运行在Producer的I/O线程中,所以这个方法的代码实现效率要足够快,否则会影响到消息的发送速度。

close()

该方法主要用于拦截器关闭时执行一些资源的清理工作。
这3个方法中抛出的异常都会被捕获并记录到日志中,但并不会再向上传递。

序列化器

生产者需要将消息对象进行序列化成为字节数,才能通过网络发送给kafka服务端,而在消费者端也需要使用反序列化器将收到的字节数转换为相应的消息对象。
生产者使用的序列化器和消费者使用的反序列化器是需要一一对应的,如果生产者使用了某种序列化器,比如StringSerializer,而消费者使用了另一种序列化器,比如IntegerSerializer,那么是无法解析出想要的数据的。

kafka客户端自带了好几种类型的序列化器,包括:Byte、Double、Integer、Long、String等,他们都实现了org.apache.kafka.common.serialization Interface Serializer接口。此接口有3个方法:

public interface Serializer<Textends java.io.Closeable {
    //用来配置当前类编码类型
    public void configure(Map<String, ?> configs, boolean isKey);
    //用来执行序列化
    public byte[] serialize(String topic, T data);
    //用来关闭当前序列化器
    public void close();
}

如果kafka客户端提供的几种序列化器都无法满足应用需求的话,也可以选择使用如JSON、ProtoBuf和Protostuff等序列化工具来实现,或者使用自定义类型的(不太建议自己自定义序列化器)。

分区器

KafkaProducer在调用send()方法发送消息到broker服务端过程中,首先经过拦截器Interceptor(不是必须)、序列化器Serializer(必须),然后就到分区器Partitioner。如果消息ProducerRecord中指定了partition字段,那么就不需要分区器来处理,因为partition代表的就是所要发往的分区号;如果没有指定,则需要分区器进行分配。

kafka提供的默认分区器是org.apache.kafka.clients.producer.internals.DefaultPartitioner,它实现了org.apache.kafka.clients.producer Interface Partitioner接口。此接口有3个方法:

public interface Partitioner extends Configurable, Closeable {
    //计算分区号
    int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
    //表示通知分区程序用来创建新的批次。
    void close();
    //表示通知分区程序用来创建新的批次
    default void onNewBatch(String topic, Cluster cluster, int prevPartition) {
    }
}
partition()

该方法是用来计算分区号的,返回值为int类型。参数分别是主题、键、序列化后的键、值、序列化后的值和集群的元数据信息。
其中,当key不为空,那么默认的分区器会对key进行哈希(采用MurmurHash2算法,具备高运算性能及低碰撞率),最终根据得到的哈希值来计算分区号,拥有相同key的消息会被写入同一个分区。如果key为空,那么消息将会以轮询的方式发往主题内的各个可用分区。

注意:如果key不为空,那么计算分区号是以所有分区为基数,去选择其中一个;如果key为空,那么计算分区号是以所有可用分区为基数,去选择任意一个。注意这两者的区别。

在不改变主题分区数量的情况下,key与分区之间的映射关系是保持不变的。不过,一旦主题中增加了分区,那么就难以保证key与分区之间的映射关系了。
当然,除了使用kafka提供的默认分区器进行分区分配,还可以使用自定义的分区器,只需实现Partitioner接口即可。

总结

至此,我们了解了主线程下拦截器、序列化器和分区器的定义和作用,也了解它们都支持自定义功能实现。接下来,我们可以动手实践一下了~

码字不易,一起努力!!!你的点赞是我前行的动力~