kafka生产者是一个向kafka集群发布记录的客户端。下图为kafka生产者组件图,图中展示了kafka发送消息的主要步骤:
- 发送消息之前,首先创建一个ProducerRecord对象,该对象包含目标主题、指定键和分区以及要发送的内容;
- 在发送ProducerRecord对象前,生产者要把键和值对象序列化成字节数组,才能在网络上传输;
- 然后数据传送到分区器;如果在ProducerRecord对象中指定了分区,那么分区器将什么也不做;如果没有指定分区,那么分区器将会根据ProducerRecord对象中的键来选择一个分区;分区好后,分区器就知道该往哪个主题和分区发送这条记录;
- 服务器在接收到消息时会返回一个响应;如果消息成功写入kafka,就返回一个RecordMetaData对象(对象包含主题和分区信息、偏移量);如果过没有成功写入,将会继续尝试重新发送消息,如果几次之后还是失败则返回错误信息。
要想往kafka中写入消息,就需要创建一个生产者对象并设置一些属性,这其中有三个属性是必须设置的:
bootstrap.servers
:指定broker的地址清单,格式:host:port
;不需要列出所有的broker地址,但推荐至少提供两个;key.serializer
:指定一个实现了org.apache.kafka.common.serialization.Serializer
接口的类,使得生产者可以使用该类将java对象转换为字节数组;这是因为broker接收的键和值都是字节数组;value.serializer
:与key.serializer
一样,也需要将值序列化为字节数组;
创建kafka生产者
创建一个kafka生产者,只需要实例化KafkaProducer
对象,然后配置对象的属性即可。
/* 创建一个properties对象,设置一些必要的属性,这里只指定了必要属性 */
Properties props = new Properties();
props.put("bootstrap.servers", "node01:9092,node02:9092,node03:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
/* 创建kafkaProducer对象 */
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(props);
实例化生产者对象后,就可以开始发送消息了,发送消息有两种方式,一种是同步发送消息,一种是异步发送消息。
同步发送消息
kafkaProducer.send()方法先返回一个Futurn对象,该对象调用get()方法等待kafka响应,如果服务器没有返回错误则会得到一个RecordMetadata
对象,该对象可以获取消息的偏移量;如果返回错误,则get()方法会抛出异常。
ProducerRecord<String, String> record = new ProducerRecord<>("test", "Precision Products", "France");
try {
kafkaProducer.send(record).get();
} catch (Exception e) {
e.printStackTrace();
}
异步发送消息
当我们发送消息的时候并不需要等待响应,尽管kafka会将目标主题、分区信息和偏移量发送回来。但当消息发送不成功时,我们就需要抛出异常、记录错误日志以便于分析;为了在异步发送消息的同时能够对异常情况进行处理,生产者提供了回调支持。
接下来用一个完整示例来演示如何创建一个新的生产者,采用异步发送数据的方式,将数据发送到名叫“test”的topic中去:
public class MyProducer {
public static void main(String[] args) {
/* 创建一个properties对象,设置一些必要的属性,这里只指定了必要属性 */
Properties props = new Properties();
props.put("bootstrap.servers", "node01:9092,node02:9092,node03:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
/* 创建kafkaProducer对象 */
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(props);
kafkaProducer.send(new ProducerRecord<>("CustomerCountry","Biomedical Materials","USA"),new DemoProducerCallback());
/* 关闭生产者 */
kafkaProducer.close();
}
private static class DemoProducerCallback implements Callback{
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if ( exception != null){
exception.printStackTrace();
}
}
}
}
生产者其它配置
除了上述三个必要的配置参数外,下面介绍几个在内存使用、性能和可靠性方面对生产者影响较大的参数。
-
acks
指定必须有几个分区副本收到消息,生产者才会认为消息写入是成功的。
- acks=0:不等待服务器响应,消息丢失无法得知;能够支持最大速度发送消息,达到高吞吐量
- acks=1:集群leader收到消息,则会返回成功响应;leader崩溃或新的leader还未选出,则生产者会收到错误响应;吞吐量取决于同步发送还是异步发送
- acks=all:只有当所有参与复制的节点收到消息时,生产者才会收到成功响应;该模式最安全,但延迟高
-
buffer.memory
用来设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。
-
compression.type
指定消息发送到broker之前使用哪一种压缩算法进行压缩,参数可设置为:
snappy
、gzip
、lz4
。 -
retries
设置生产者可以重发消息的次数,超过该次数生产者会放弃重试并返回错误。建议在设置重试次数和重试时间间隔之前,测试一下恢复崩溃节点需要的时间,让总的重试时间大于kafka集群从崩溃节点中恢复的时间,防止生产者过早的放弃重试。
-
batch.size
当有多个消息需要被发送到同一个分区时,生产者会将它们放在同一个批次里。该参数指定一个批次可以使用的内存大小。
-
linger.ms
指定生产者在发送批次之前等待更多消息加入批次的时间。
-
client.id
任意字符串,用于服务器识别消息来源。
-
max.in.flight.requests.per.connection
指定生产者在收到服务器响应之前可以发送多少个消息。设为1可以保证消息是按照发送的顺序写入服务器的。
-
timeout.ms、request.timeout.ms 和 metadata.fetch.timeout.ms
- timeout.ms:指定broker等待同步副本返回消息确认的时间
- request.timeout.ms:指定了生产者在发送数据时等待服务器返回响应的时间
- metadata.fetch.timeout.ms:指定了生产者在获取元数据时等待服务器返回响应的时间
-
max.block.ms
指定了在调用send()方法或使用partitionsFor()方法获取元数据时生产者的阻塞时间。达到设置时间时,会抛出超时异常。
-
max.request.size
用于控制生产者发送的请求大小。
-
receive.buffer.bytes 和 send.buffer.bytes
分别指定了TCP socket接收和发送数据包的缓冲区大小,设为-1,则使用操作系统默认值。
序列化器
在创建一个生产者对象时,必须指定序列化器。kafka默认提供了ByteArraySerializer、StringSerializer、IntegerSerializer三个序列化器,但是这并不能满足大部分的应用场景,所以需要开发自定义的序列化器;但是不推荐使用自定义序列化器,推荐使用已有的序列化器和反序列化器,如:JSON、Avro、Thrift或Protobuf。
接下来写一个自定义序列化器来了解序列化器工作原理:
- 首先创建一个类来表示一个客户:
public class Customer {
private int customerID;
private String customerName;
public Customer(int customerID, String customerName) {
this.customerID = customerID;
this.customerName = customerName;
}
public int getCustomerID() {
return customerID;
}
public String getCustomerName() {
return customerName;
}
}
- 为这个类创建一个序列化器:
import org.apache.kafka.common.serialization.Serializer;
import java.nio.ByteBuffer;
import java.util.Map;
public class CustomerSerializer implements Serializer<Customer> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
// 不做配置
}
/**
* Customer对象被序列化成:
* 表示customerID的4字节整数
* 表示customerName长度的4字节整数(customerName为null,则为0)
* 表示customerName的N个字节
* */
@Override
public byte[] serialize(String topic, Customer data) {
try{
byte[] serializerName;
int stringSize;
if (data == null) {
return null;
} else {
if (data.getCustomerName() != null){
serializerName = data.getCustomerName().getBytes("UTF-8");
stringSize = serializerName.length;
} else {
serializerName = new byte[0];
stringSize = 0;
}
}
ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + stringSize);
buffer.putInt(data.getCustomerID());
buffer.putInt(stringSize);
buffer.put(serializerName);
return buffer.array();
} catch (Exception e){
e.printStackTrace();
}
return new byte[0];
}
@Override
public void close() {
// 不需要关闭任何东西
}
}
分区
kafka消息是一个个键值对,ProducerRecord对象可以只包含目标主题和值,键可以设置为默认null;键有两个用途:
- 作为消息的附加信息
- 用来决定消息该被写到主题的哪个分区
kafka有两种默认的分区机制:
- 当键值为null,且使用了默认分区器;那么分区器将使用轮询算法将消息均衡的发布到各个分区上;
- 当键值不为null,且使用了默认分区器;那么kafka会对键进行散列,然后根据散列值把消息映射到特定的分区上;只有在不改变主题分区数量的情况下,键与分区之间的映射才能保持不变;
有时候我们也需要对数据进行不一样的分区,这时候就需要通过自定义分区策略来实现:
public class BananaPartitioner implements Partitioner {
@Override
public void configure(Map<String, ?> configs) {
}
@Override
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) || (!(key instanceof String)))
throw new InvalidRecordException("We expect all messages to hava customer name as key");
if (key.equals("Banana"))
return numPartitions; // Banana总是被分配到最后一个分区
// 其他记录被散列到其他分区
return (Math.abs(Utils.murmur2(keyBytes)) % (numPartitions - 1));
}
@Override
public void close() {
}
}
参考资料
Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2018.1
https://dy.163.com/article/EVG4T30T0511FQO9.html
https://blog.csdn.net/m0_37809146/article/details/91126212