生产者是负责向Kafka发送消息的应用程序,通常应用于:记录用户的活动、物联网硬件数据的写入、保存日志消息和缓存即将写入到数据库的数据等。
生产者向Kafka发送消息的主要步骤如下:
步骤如下:
生成者配置参数:
public class KafkaProducerAns {
public static final String brokerList="node01:9092";
public static final String topic="topic-demo";
public static Properties initConfig(){
Properties prop = new Properties();
// broker 的地址清单,地址的格式为 host:port
prop.put("bootstrap.servers",brokerList);
//broker 希望接收到的消息的键和值都是字节数组。
prop.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
prop.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//重试次数
prop.put("retries", 1);
//批次大小
prop.put("batch.size", 16384);
//等待时间
prop.put("linger.ms", 1);
//RecordAccumulator缓冲区大小
prop.put("buffer.memory", 33554432);
return prop;
}
public static void main(String[] args) {
Properties properties = initConfig();
KafkaProducer<String,String> producer = new KafkaProducer<>(properties);
ProducerRecord<String, String> record = new ProducerRecord<>(topic, "Hello Kafka");
try {
producer.send(record);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:ProducerRecord不仅仅只有消息,只是与业务相关的消息体只有一个value属性,以下为ProducerRecord类定义,包含主题、分区号、键、值、消息的时间戳
发送消息的方式有三种:
try {
producer.send(record);
} catch (Exception e) {
e.printStackTrace();
}
try {
producer.send(record).get();
} catch (Exception e) {
e.printStackTrace();
}
注意:onCompletion方法两个参数是互斥的,消息发送成功时,metadata不为null而exception为null;
消息发送异常时,metadata为null而exception不为null;
try {
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception!=null){
exception.printStackTrace();
}else {
System.out.println(metadata.topic()+"-"+
metadata.partition()+":"+metadata.offset());
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
另外,如果如下发送多条消息,对同一个分区而言,如果record1于record2之前发送,KafkaProducer可以保证对应的callback1在callback2之前调用,也就是说回调函数也可以保证分区有序。
producer.send(record1,callback1);
producer.send(record2,callback1);
/**
* Asynchronously send a record to a topic. Equivalent to send(record, null)
.
* See {@link #send(ProducerRecord, Callback)} for details.
*/
@Override
public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
return send(record, null);
}
/**
* Asynchronously send a record to a topic and invoke the provided callback when the send has been acknowledged.
*
* The send is asynchronous and this method will return immediately once the record has been stored in the buffer of
* records waiting to be sent. This allows sending many records in parallel without blocking to wait for the
* response after each one.
* @param record The record to send
* @param callback A user-supplied callback to execute when the record has been acknowledged by the server (null
* indicates no callback)
*/
@Override
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
// intercept the record, which can be potentially modified; this method does not throw exceptions
ProducerRecord<K, V> interceptedRecord = this.interceptors == null ? record : this.interceptors.onSend(record);
return doSend(interceptedRecord, callback);
}
注意: send()方法包含两个重载方法,返回类型为Future,send方法本身是异步的,通过返回的Future对象可以是调用放稍后获得发送结果
在消息发送完以后,需要调用KafkaProducer的close()方法来回收资源。
producer.close();
生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给Kafka。
上述有提到,常见的序列化器有ByteArraySerializer、StringSerializer 和 IntegerSerializer等等。他们都实现了org.apache.kafka.common.serialization.Serializer接口。该接口包含三个方法:
**configure()**方法用来配置当前类,**serialize()**方法用来执行序列化操作,**close()**方法用来关闭当前序列化器(一般情况下,close()是一个空方法,如果实现了此方法,则必须保证此方法的幂等性).
public interface Serializer<T> extends Closeable {
public void configure(Map<String, ?> configs, boolean isKey);
public byte[] serialize(String topic, T data);
public void close();
StringSerializer的实现:
configure方法:在创建KafkaProducer实例时调用的,主要用来确认编码格式;
serialize方法:将String类型转成byte[]类型
public class StringSerializer implements Serializer<String> {
private String encoding = "UTF8";
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
String propertyName = isKey ? "key.serializer.encoding" : "value.serializer.encoding";
Object encodingValue = configs.get(propertyName);
if (encodingValue == null)
encodingValue = configs.get("serializer.encoding");
if (encodingValue != null && encodingValue instanceof String)
encoding = (String) encodingValue;
}
@Override
public byte[] serialize(String topic, String data) {
try {
if (data == null)
return null;
else
return data.getBytes(encoding);
} catch (UnsupportedEncodingException e) {
throw new SerializationException("Error when serializing string to byte[] due to unsupported encoding " + encoding);
}
}
@Override
public void close() {
// nothing to do
}
}
如果Kafka客户端提供的几种序列化器都无法满足需求,则可以选择Avro、Thrift等序列化工具实现,或者使用自定义序列化器。
假设发送一个Company对象如下,自定义一个CompanySerializer
public class Company {
private String Name;
private String address;
public Company() {
}
public String getName() {
return Name;
}
public void setName(String name) {
Name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
CompanySerializer:
public class CompanySerializer implements Serializer<Company> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
}
@Override
public byte[] serialize(String topic, Company data) {
if (data==null){
return null;
}
byte[] name,address;
try {
if (data.getName()!=null){
name=data.getName().getBytes("UTF-8");
}else {
name=new byte[0];
}
if (data.getAddress()!=null){
address=data.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() {
}
}
CompanySerializer的使用,直接将value.serializer的参数设置为CompanySerializer全限定名:
prop.put("value.serializer", CompanySerializer.class.getName());
注意:自定义序列化器很简单,但是不建议使用,因为在不同版本的序列化器和反序列化器之间调试兼容性问题着实是个挑战,同时如果序列化器发生改动,所有消费数据的程序都要改动。建议使用Avro等序列化工具。
分区的原因:
分区的原则
生产者通过封装一个ProducerRecord对象发送数据给Kafka,ProducerRecord 对象包含了目标主题、键和值。ProducerRecord 对象可以只包含目标主题和值,键可以设置为默认的 null,不过大多数应用程序会用到键。
键有两个用途:可以作为消息的附加信息,也可以用来决定消息该被写到主题的哪个分区。
拥有相同键的消息将通过散列算法被发送到同一个分区;
如果键值为 null,并且使用了默认的分区器,那么记录将被随机地发送到主题内各个可用的分区上。分区器使用轮询(Round Robin)算法将消息均衡地分布到各个分区上。
自定义分区策略
Kafka中默认通过的分区器是org.apache.kafka.clients.producer.internals.DefaultPartitioner。源码如下,它继承了Partitioner 接口实现了partition()方法和close方法。
public class DefaultPartitioner implements Partitioner {
private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();
public void configure(Map<String, ?> configs) {}
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//获取topic分区列表及其个数
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
//如果键为null
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 {
return Utils.toPositive(nextValue) % numPartitions;
}
} else {
// 键值不为空,通过散列算法寻找对应分区号
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
private int nextValue(String topic) {
AtomicInteger counter = topicCounterMap.get(topic);
if (null == counter) {
counter = new AtomicInteger(new Random().nextInt());
AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
if (currentCounter != null) {
counter = currentCounter;
}
}
return counter.getAndIncrement();
}
public void close() {}
}
**注意:**只有在不改变主题分区数量的情况下,键与分区之间的映射才能保持不变。如果增加了分区,就很难保证key与分区之间的映射关系。
我们也可以自定义分区器,继承Partitioner 接口即可。如下,将默认分区器中,改变key为null时不会选择不可用分区。
public class DemoPartitioner implements Partitioner {
private final AtomicInteger counter=new AtomicInteger(0);
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, 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, ?> configs) {
}
}
使用自定义分区器:
prop.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,DemoPartitioner.class.getName());
拦截器是Kafka0.10.0中引入 一个功能,生产者拦截器可以用来在发送消息前做一些准备工作,比如过滤消息、修改消息内容等,也可以用来发送回调逻辑前做一些定制化的需求,比如统计类工作。
生产者拦截器通过继承org.apache.kafka.clients.producer.ProducerInterceptor接口实现,ProducerInterceptor包含三个方法:
public interface ProducerInterceptor<K, V> extends Configurable {
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
public void onAcknowledgement(RecordMetadata metadata, Exception exception);
public void close();
}
案例:自定义一个拦截器,在发送消息前给每条消息前缀加上"prefix-",并计算发送消息的成功率:
public class ProducerInterceptotPrefix implements ProducerInterceptor<String,String> {
private volatile long sendSuccess=0;
private volatile long sendFailure=0;
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
String newValue = "prefix1-" + record.value();
return new ProducerRecord<>(record.topic(),record.partition(),
record.timestamp(),record.key(),newValue);
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
if (exception==null){
sendSuccess++;
}else {
sendFailure++;
}
}
@Override
public void close() {
double successRatio = sendSuccess / (sendSuccess + sendFailure);
System.out.println("[INFO] 发送成功率"+String.format("%f",successRatio*100)+"%");
}
@Override
public void configure(Map<String, ?> configs) {
}
}
使用:
prop.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,ProducerInterceptotPrefix.class.getName());
Kafka可以保证一个分区里的消息是有序的,也就是说,如果生产者按照一定的顺序发送消息, broker 就会按照这个顺序把它们写入分区,消费者也会按照同样的顺序读取它们。
1. 为什么只保证单个分区内数据有序?
如果Kafka要保证多个partition有序,不仅broker保存的数据要保持顺序,消费时也要按序消费。假设partition1堵了,为了有序,那partition2以及后续的分区也不能被消费,这种情况下,Kafka 就退化成了单一队列,毫无并发性可言,极大降低系统性能。因此Kafka使用多partition的概念,并且只保证单partition有序。这样不同partiiton之间不会干扰对方。
2. 调参与优化
通过调整retries(失败重试次数)和max.in.flight.requests.per.connection(生产者收到响应前可以发送多少个消息)可以影响消息发送顺序;如果对生产者吞吐量要求比较大,对消息顺序要求不高时候,设置retries大于0,同时把max.in.flight.requests.per.connection设为比1大的整数,那么,如果第一个批次消息写入失败,而第二个批次写入成功, broker 会重试写入第一个批次。如果此时第一个批次也写入成功,那么两个批次的顺序就反过来了;如果要求消息必须是有序的,那么建议将max.in.flight.requests.per.connection设为1,且retries不为0,这样当第一批次消息发送失败时,也不会有第二个批次发送消息给broker,不过这样也严重影响生产者的吞吐量。
将服务器的ACK级别设置为-1(all),可以保证Producer到Server之间不会丢失数据,即At Least Once语义。相对的,将服务器ACK级别设置为0,可以保证生产者每条消息只会被发送一次,即At Most Once语义。At Least Once可以保证数据不丢失,但是不能保证数据不重复;相对的,At Least Once可以保证数据不重复,但是不能保证数据不丢失。但是,对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据既不重复也不丢失,即Exactly Once语义。在0.11版本以前的Kafka,对此是无能为力的,只能保证数据不丢失,再在下游消费者对数据做全局去重。对于多个下游应用的情况,每个都需要单独做全局去重,这就对性能造成了很大影响。
0.11版本的Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指Producer不论向Server发送多少次重复数据,Server端都只会持久化一条。幂等性结合At Least Once语义,就构成了Kafka的Exactly Once语义。即:
At Least Once + 幂等性 = Exactly Once
要启用幂等性,只需要将Producer的参数中enable.idompotence设置为true即可。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。
开启幂等性的Producer在初始化的时候会被分配一个PID,发往同一Partition的消息会附带Sequence Number。而Broker端会对
Kafka生产者通过ack机制保证数据的可靠性。
ack机制:为保证producer发送的数据,能可靠的发送到指定的topic,topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
方案 | 优点 | 缺点 |
---|---|---|
半数以上完成同步,就发送ack | 延迟低 | 选举新的leader时,容忍n台节点的故障,需要2n+1个副本 |
全部完成同步,才发送ack | 选举新的leader时,容忍n台节点的故障,需要n+1个副本 | 延迟高 |
Kafka选择了第二种方案,原因如下: