本文根据原创整理而来
原创出处 https://blog.csdn.net/u013256816/article/details/78851989
Kafka Producer Inteceptor
使用
KafkaProducer
端的拦截器非常简单,主要是实现ProducerInterceptor
接口,此接口包含4个方法:
ProducerRecord<K, V> onSend(ProducerRecord<K, V>record)
:
Producer在将消息序列化和分配分区之前会调用拦截器的这个方法来对消息进行相应的操作。一般来说最好不要修改消息ProducerRecord
的topic、key
以及partition
等信息,如果要修改,也需确保对其有准确的判断,否则会与预想的效果出现偏差。比如修改key不仅会影响分区的计算,同样也会影响Broker
端日志压缩(LogCompaction)
的功能。
void onAcknowledgement(RecordMetadata metadata,Exceptionexception):
在消息被应答(Acknowledgement)
之前或者消息发送失败时调用,优先于用户设定的Callback之前执行。这个方法运行在Producer
的IO
线程中,所以这个方法里实现的代码逻辑越简单越好,否则会影响消息的发送速率。
void close():
关闭当前的拦截器,此方法主要用于执行一些资源的清理工作。
configure(Map<String, ?>configs):
用来初始化此类的方法,这个是ProducerInterceptor
接口的父接口Configurable
中的方法。
一般情况下只需要关注并实现onSend
或者onAcknowledgement
方法即可。下面我们来举个案例,通过onSend
方法来过滤消息体为空的消息以及通过onAcknowledgement
方法来计算发送消息的成功率。
public class ProducerInterceptorDemo implements ProducerInterceptor<String,String> {
private volatile long sendSuccess = 0;
private volatile long sendFailure = 0;
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
if(record.value().length()<=0)
return null;
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
if (exception == null) {
sendSuccess++;
} else {
sendFailure ++;
}
}
@Override
public void close() {
double successRatio = (double)sendSuccess / (sendFailure + sendSuccess);
System.out.println("[INFO] 发送成功率="+String.format("%f", successRatio * 100)+"%");
}
@Override
public void configure(Map<String, ?> configs) {}
}
自定义的
ProducerInterceptorDemo
类实现之后就可以在Kafka Producer
的主程序中指定,示例代码如下:
public class ProducerMain {
public static final String brokerList = "localhost:9092";
public static final String topic = "hidden-topic";
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties properties = new Properties();
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("bootstrap.servers", brokerList);
// 可以有多个inteceptor,如"com.ProducerInterceptorDemo,com.ProducerInterceptorDemoPlus"
properties.put("interceptor.classes", "com.hidden.producer.ProducerInterceptorDemo");
Producer<String, String> producer = new KafkaProducer<String, String>(properties);
while (true) {
String message = "kafka_message-" + new Date().getTime() + "-edited by hidden.zhu";
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(topic,message);
try {
Future<RecordMetadata> future = producer.send(producerRecord, new Callback() {
public void onCompletion(RecordMetadata metadata, Exception exception) {
System.out.print(metadata.offset()+" ");
System.out.print(metadata.topic()+" ");
System.out.println(metadata.partition());
}
});
} catch (Exception e) {
e.printStackTrace();
}
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Kafka 分区分配计算(分区器 Partitions )
KafkaProducer
在调用send方法发送消息至broker
的过程中,首先是经过拦截器Inteceptors
处理,然后是经过序列化Serializer处理,之后就到了Partitions
阶段,即分区分配计算阶段。在某些应用场景下,业务逻辑需要控制每条消息落到合适的分区中,有些情形下则只要根据默认的分配规则即可。
看下
ProducerRecord
:
private final String topic;//所要发送的topic
private final Integer partition;//指定的partition序号
private final Headers headers;//一组键值对,与RabbitMQ中的headers类似,kafka0.11.x版本才引入的一个属性
private final K key;//消息的key
private final V value;//消息的value,即消息体
private final Long timestamp;//消息的时间戳,可以分为Create_Time和LogAppend_Time之分,这个以后的文章中再表。123456
在
KafkaProducer
的源码(1.0.0)中,计算分区时调用的是下面的partition()
方法:
/**
* computes partition for given record.
* if the record has partition returns the value otherwise
* calls configured partitioner class to compute the partition.
*/
private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
Integer partition = record.partition();
return partition != null ?
partition :
partitioner.partition(record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
}
可以看出的确是先判断有无指明
ProducerRecord
的partition
字段,如果没有指明,则再进一步计算分区。上面这段代码中的partitioner
在默认情况下是指Kafka默认实现的org.apache.kafka.clients.producer.DefaultPartitioner
,其partition()
方法实现如下:
/**
* Compute the partition for the given record.
*
* @param topic The topic name
* @param key The key to partition on (or null if no key)
* @param keyBytes serialized key to partition on (or null if no key)
* @param value The value to partition on or null
* @param valueBytes serialized value to partition on or null
* @param cluster The current cluster metadata
*/
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) {
int nextValue = nextValue(topic);
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// no partitions are available, give a non-available partition
return Utils.toPositive(nextValue) % numPartitions;
}
} else {
// hash the keyBytes to choose a partition
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
private int nextValue(String topic) {
AtomicInteger counter = topicCounterMap.get(topic);
if (null == counter) {
counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
if (currentCounter != null) {
counter = currentCounter;
}
}
return counter.getAndIncrement();
}
由上源码可以看出
partition
的计算方式:如果
key
为null
,则按照一种轮询的方式来计算分区分配
如果key
不为null
则使用称之为murmur
的Hash
算法(非加密型Hash
函数,具备高运算性能及低碰撞率)来计算分区分配。
KafkaProducer
中还支持自定义分区分配方式,与org.apache.kafka.clients.producer.internals.DefaultPartitioner
一样首先实现org.apache.kafka.clients.producer.Partitioner
接口,然后在KafkaProducer
的配置中指定partitioner.class
为对应的自定义分区器(Partitioners)
即可:
properties.put("partitioner.class","com.hidden.partitioner.DemoPartitioner");