目录
拦截器
生产者拦截器
自定义生产者拦截器
序列化器
反序列化器
分区器
消息累加器
前提了解:
整个kafka生产者客户端由两条线程协调运行。
这两条线程分别为主线程和sender线程(发送线程)
主线程的作用就是:由KafkaProducer创建消息,然后通过可能的拦截器,序列化器,分区器的作用之后缓存到消息累加器
send线程的作用就是:负责将消息累加器中的消息发送到kafka中。
拦截器
拦截器是在kafka0.10.0.0版本中就已经引入的一个功能,kafka一共有两种拦截器。生产者拦截器和消费者拦截器。
生产者拦截器
生产者拦截器可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息,修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求。
当然生产者拦截器的使用也很方便,主要是自定义实现org.apache.kafka.clients.producer.ProducerInterceptor接口。
ProducerInterceptor接口中包含3个方法:
//producer会在消息序列化器,分区器之前调用拦截器的onSend()方法来对消息进行定制化操作。
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
//producer会在消息被应答之前或者发送失败时调用生产者的 onAcknowledgement()方法
//不过该方法运行在Producerde I/O线程中,所以方法中的实现代码逻辑越简单越好,否则会影响消息的发送速度
public void onAcknowledgement(RecordMetadata metadata, Exception exception);
//用于在关闭拦截器时执行一些资源的清理工作
public void close();
自定义生产者拦截器
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
/**
* 生产者拦截器
*/
public class ProducerInterceptorPrefix implements ProducerInterceptor<String, String> {
private volatile long sendSuccess = 0;
private volatile long sendFailure = 0;
//定制化数据
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
final String mpdofoeddValue = "preFix" + producerRecord.value();
return new ProducerRecord<String, String>(
producerRecord.topic(),
producerRecord.partition(),
producerRecord.timestamp(),
producerRecord.key(),
mpdofoeddValue,
producerRecord.headers());
}
//消息被应答前发送失败时会调用
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
if(e == null ){
sendSuccess ++;
}else{
sendFailure ++;
}
}
@Override
public void close() {
final double successRatio = (double)sendSuccess / (sendSuccess + sendFailure);
System.out.println("发送成功率 = " + String.format("%f",successRatio* 100) + "%");
}
@Override
public void configure(Map<String, ?> map) { }
}
实现自定义的ProducerInterceptorPrefix之后,需要在producer的配置参数中指定拦截器。示例:
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
/**
* kafka生产
*/
public class KafkaProducerFastStart {
public static final String brokerList = "127.0.0.1:9092";
public static final String topic = "topic-demo";
public static void main(String args[]) {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList); //集群地址
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); // 16K batch.size
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); // 32M buffer.memory
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//KEY序列化方式
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//value序列化方式
properties.put(ProducerConfig.ACKS_CONFIG, "1");
properties.put(ProducerConfig.RETRIES_CONFIG, 3);//retries
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);//linger.ms
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, ProducerInterceptorPrefix.class.getName());// interceptor.classes 拦截器
//配置生产者客户端参数并创建KafkaProducer实例
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//构建需要发送得消息
ProducerRecord<String, String> data = new ProducerRecord<String, String>(topic, "key", "value");
try {
producer.send(data);
} catch (Exception e) {
e.printStackTrace();
} finally {
producer.close();
}
}
}
当然,拦截器不仅仅可以指定一个。还可以指定多个形成拦截链,拦截链会按照interceptor.classes 参数配置的拦截器的顺序来一一执行。每个拦截器之间用逗号隔开。
properties.put(
ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
ProducerInterceptorPrefix.class.getName() + "," + ProducerInterceptorPrefix2.class.getName());// interceptor.classes 拦截器
序列化器
生产者需要用序列化器将对象转换成字节数组才能通过网络发送给kafka.当然消费者需要用对应的反序列化器将kafka的字节数组转换为相应的对象。
kafka客户端自带的序列化器有:
DoubleSerializer ,ByteArraySerializer ,ByteBufferSerializer,
BytesSerializer , IntegerSerializer , LongSerializer ,StringSerializer
当然,如果这几种序列化器都无法满足应用需求,我们可以选择如 Avro,JSON,Thrift,Protobuf等,或者使用自定义类型的序列化器来实现。
图中 Company 为自定义的对象。
import org.apache.kafka.common.serialization.Serializer;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Map;
/**
生产端序列化器
*/
public class CustomizeSerializer implements Serializer<Company> {
@Override
public void configure(Map<String, ?> map, boolean b) { }
@Override
public byte[] serialize(String topic, Company company) {
if (company == null) {
return null;
}
byte[] name, address;
try {
if (company.getName() != null) {
name = company.getName().getBytes("UTF-8");
} else {
name = new byte[0];
}
if (company.getAddress() != null) {
address = company.getAddress().getBytes("UTF-8");
} else {
address = new byte[0];
}
ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + name.length + address.length);
buffer.putInt(name.length);
buffer.put(name);
buffer.putInt(address.length);
buffer.put(address);
return buffer.array();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new byte[0];
}
@Override
public void close() {
}
}
kafkaProducer配置
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,CustomizeSerializer.class.getName());
反序列化器
import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Deserializer;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Map;
/**
* 反序列化器
*/
public class CustomizeDeserializer implements Deserializer<Company> {
@Override
public void configure(Map<String, ?> map, boolean b) { }
@Override
public Company deserialize(String topic, byte[] data) {
if(data == null ){
return null;
}
if(data.length < 8 ){
throw new SerializationException("Size of data received by DemoDeserializer is shorter than expected !");
}
ByteBuffer buffer = ByteBuffer.wrap(data);
int nameLen,addressLen;
String name, address;
nameLen = buffer.getInt();
final byte[] nameBytes = new byte[nameLen];
buffer.get(nameBytes);
addressLen = buffer.getInt();
byte[] addressBytes = new byte[addressLen];
buffer.get(addressBytes);
try{
name = new String(nameBytes,"UTF-8");
address = new String(addressBytes,"UTF-8");
}catch (UnsupportedEncodingException e){
throw new SerializationException("Error occur when deserializing");
}
return new Company(name,address);
}
@Override
public void close() { }
}
消费者指定反序列化器
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,CustomizeDeserializer.class.getName());
分区器
消息通过send()方法发送broker的过程中,有可能会经过拦截器,序列化器,之后,就会需要确定消息要发往的分区。如果ProducerRecord中指定了partition字段,那么就不需要分区器的作用。因为partition代表的就是索要发往的分区号。
kafka提供的默认分区器是org.apache.kafka.clients.producer.internals.DefaultPartitioner.它实现了org.apache.kafka.clients.producer.Partitioner接口,这个接口定义了2个方法:
//用来计算分区号。
@params topic 分区
@params key 消息的key
@params keyBytes 序列化后的消息key
@params value 消息值
@params valueBytes 序列化后的值
@params cluster 集群的元数据信息
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
public void close();
当然Pratitioner接口还有一个父接口:org.apache.kafka.common.Configurable。该接口中只有一个方法:
//用来获取配置信息及初始化数据
void configure(Map<String, ?> configs);
接下来我们看下默认分区器中的实现
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;
}
}
可以看到:如果key不为null,那么默认分区器会对key进行哈希(采用MurmurHash2算法)得到的哈希值来计算分区号。
接下来我们可以自定义我们自己的分区器
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.utils.Utils;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 分区器
*/
public class Dempartitioner implements Partitioner {
private final AtomicInteger counter = new AtomicInteger(0); //原子操作类AtomicInteger
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object valye, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
int numPartitions = partitionInfos.size();
if(null == keyBytes){
return counter.getAndIncrement() % numPartitions;
}else{
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
@Override
public void close() {}
@Override
public void configure(Map<String, ?> map) { }
}
指定分区器的配置
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,Dempartitioner.class.getName());//partitioner.class
消息累加器
RecordAccumulator也叫消息累加器,主要用来缓存消息以便Sender线程可以批量发送,进而减少网络传输的资源消耗来提升性能。 是在客户端开辟出的一块内存区域。
this.accumulator = new RecordAccumulator(logContext,
config.getInt("batch.size"),
this.totalMemorySize,
this.compressionType,
config.getLong("linger.ms"),
retryBackoffMs,
this.metrics,
this.time,
this.apiVersions,
this.transactionManager);
参数buffer.memory,默认为33554432B,及32MB. 指定RecordAccumulator缓存的大小
如果生产者发送消息的速度超过发送到服务器的速度,则会导致生产者空间不足。这个时候producer的send方法要么被阻塞,要么抛出异常。这个取决于参数max.block.ms的配置,此参数默认值为60000,及60秒。
RecordAccumulator的内部为每个分区都维护了一个双端队列。队列中的具体内容就是ProducerBatch(消息批次).即Deque<ProducerBatch>.
通俗来讲,ProducerBatch为一个消息批次,可以将较小的ProducerRecord拼凑成一个较大的ProducerBatch.来减少网络请求次数提高吞吐量。
当然ProducerRecord即为我们的消息。
每次有消息要写入到累加器中的时候,会先去寻找对应的双端队列,从队列的尾部获取一个ProducerBatch, 事先会判断消息的大小,如果大小在被写入的Producerbatch批次范围内,则写入。如果没找到ProducerBatch或者消息大小大于被写入的批次范围无法写入的时候,就会新建一个ProducerBatch,根据参数batch.size来创建batch大小。如果消息大于batch.size的大小,就以评估的大小创建ProducerBatch.
消息写入缓存时,追加到双端队列的尾部Sender线程读取消息时,从双端队列的头部读取。注意ProducerBatch中可以包含很多个ProducerRecord(消息)。
在RecordAccumulator的内部还有一个BufferPool,主要来实现ButeBuffer的复用。不过BufferPool只针对特定大小的ByteBuffer进行管理。超过大小是不会进入BufferPool的。可以通过参数batch.size来指定。默认为16384B.
当每条消息流入消息累加器,会先寻找每个分区中的ProducerBatch双端队列。如果找不到,会判断消息的大小,如果小于batch.size。则用batch.size的大小来创建ProducerBatch.
sender线程会从RecordAccumulator中获取缓存的消息之后,进一步将消息封装成<Node,Request>的形式,通过KafkaClient进行IO操作。