生产者
在kafka中,我们需要把一些数据发送到kafka服务器中,完成这一工作的角色有 Producer 来承担。
使用生产者例子
public class SimpleProducer {
public static void main(String[] args) throws Exception {
String key = "this is key";
String value = "this is value";
String topicName = "SimpleProducerTopic";
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String,String> producer = new KafkaProducer<>(props);
ProducerRecord<String,String> record = new ProducerRecord<>(topicName,key,value);
producer.send(record);
producer.close(); // 关闭资源,防止泄漏
}
}
从上面的例子来看,我们可以概括发送一条消息,主要分为三个部分:
- 创建
kafkaProducer对象 - 创建
ProducerRecord消息实体对象 - 发送消息
为了创建 kafkaProducer 对象,我们需要至少配置三个:
bootstrap.servers:主机列表key.serializer:键序列化value.serializer:值序列化
bootstrap.servers:它是Brokers的主机地址,生产者可以根据这个配置连接kafka集群。我们指定一个或多个主机地址。为了提供可用性,这里推荐是至少两个主机地址,就算其中一个挂了,还可以连接到其他的Broker上。
key.serializer 或 value.serializer :从kafka的角度去看,一个消息都是由字节数组构成。我们需要把key 和 value 都转成字节数组。
生产者工作流程

该图描述生产者工作流程。下面我们一起来看一下具体步骤。
-
生产者配置
配置生产者的参数,这里只配置bootstrap.servers、key.serializer、value.serializer。
当然还可以根据实际情况,配置其他参数
-
创建消息
创建消息记录,一个消息记录由五个字段构成
- topic name:主题名称
- partition number:分区号
- timestamp:时间戳
- key
- value
-
序列化()
生产者将根据配置序列化参数去序列化消息的key和value
当然,我们可以自定义序列化
-
分区()
然后把消息发送到分区器。分区器将给每个消息分配一个分区号(partition number)。
默认的分区器将会根据消息的key进行分区,对key进行hash取值,然后得到分区号;
如果key没有被显式指定,分区器将均匀分配消息到每个partitions中,这里采用的轮询的算法
-
分区缓存
一旦消息有了分区号,分区器将准备发送消息到Broker,但是并不是立即发送。消息会被缓存到partition buffer中。每个生产者都有一个partition buffer,这样可以积累多条消息,批量发送。我们可以通过
batch.size参数控制批量发送大小,linger.ms参数控制滞留时间 -
重试与成功响应
如果Broker保存这些消息,Broker将发送
RecordMetadata的对象响应生产者。如果发送一些错误,生产者将收到错误。如果错误属于可恢复性错误(集群中leader挂了,然后重新选举),生产者会重试发送消息。
同步发送vs异步发送
同步发送消息:生产者发送一条消息并且等待,直到接受到Broker的响应。如果成功,我们得到的响应是 RecordMetadata 对象;否则我们会得到一个异常。这样我们就可以知道消息有没有被丢失。同步发送将会阻塞直到broker响应,这将会影响到系统的吞吐量。下面给出同步发送的示例
try {
producer.send(record).get();
} catch (Exception e) {
System.out.println("SynchronousProducer failed with an exception");
} finally {
producer.close();
}
异步发送:异步发送有两种方式。一种是直接发送,不考虑是否成功;另一种是直接发送,成功或失败触发回调函数。第一种方式,如果消息丢失对业务没有太大影响,可以选择这样的方式;否则选择第二种方式。s
Fire-and-forget producer
producer.send(record);
回调 producer
producer.send(record, (rm, ex) -> {
if (ex != null) {
System.out.println("AsynchronousProducer failed with an exception");
}
})
自定义分区器
默认的分区器有3条规则:
- 如果生产者发送的record指定分区号,直接使用这个分区号
- 如果消息没有硬编码的分区号,指定key,根据key进行hash,确定分区号
- 如果既没有指定分区号也没有指定key,分区器将采用轮询算法,均匀分配
为什么需要自定义分区器呢?
因为默认的分区器有存在两个局限性:
- 不同的key可能有相同的hash值。
- 分区号是由key的hash值取模主题的分区数得来。如果你给主题增加分区,原来的key的取模值会发送变化。
所以,在一些场景,我们需要自定义分区器来满足我们需求。例如,我们需要tagA的消息放在前一半的分区,tagB的消息放在后一半分区。
public class SensorProducer {
public static void main(String[] args) throws Exception {
String topicName = "SensorTopic";
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092,localhost:9093");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("partitioner.class", "SensorPartitioner");
props.put("tag.name", "tagA");
Producer<String, String> producer = new KafkaProducer(pros);
for (int i = 0; i < 10; i++)
producer.send(new ProducerRecord<>(topicName, "tagA", "tagA-"+i));
for (int i = 0; i < 10; i++)
producer.send(new ProducerRecord<>(topicName, "tagB", "tagB-"+i));
producer.close();
System.out.println("SimpleProducer Completed.");
}
}
这里的生产者配置参数和上述基本类似。仅有和上述不同的参数 partitioner.class ,我们自定义的分区器限定名。tag.name 参数不是kafka自带参数,而是我们自定义的参数。
我们自定义分区器需要实现 Partitioner 接口,并重写三个方法
- Configure
- Partition
- Close
public class SensorPartitioner implements Partitioner {
private String tagName;
public void configure(Map<String, ?> configs) {
tagName = configs.get("tag.nam").toString();
}
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();
int sp = (int) Math.abs(numPartitions * 0.5);
int p = 0;
if ((keyBytes == null) || (!(key instanceof String)))
throw new InvalidRecordException("All messages must have sensor name as key");
if (((String) key).equals(tagName))
p = Math.abs(key.hashCode()) % sp;
else
p = Math.abs(key.hashCode()) % (numPartitions - sp) + sp;
System.out.println("Key = " + (String) key + " Partition = " + p);
return p;
}
}
自定义序列化/反序列化
在之前的例子中我们都是发送一些简单内容,如果我们需要发送一些复杂的内容(对象等)我们不能使用 StringSerializer 来序列化我们的内容,而且需要自定义序列化、反序列化帮助我们来完成工作。不过,在实际开发并不推荐自定义序列化、反序列化,可以使用通用的框架,Avro 等。首先,我们需要实现 org.apache.kafka.common.serialization.Serializer 接口,实现接口的三个方法:
configurecloseserialize
其中,configure 方法在初始化只被调用一次,close 方法用来做一些清理工作,也是只会被调用一次。serialize 方法就是用来把内容序列化字节数组或反序列化对象。
自定义序列化
public class SupplierSerializer implements Serializer<Student> {
private String encoding = "UTF8";
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
}
@Override
public void close() {
}
@Override
public byte[] serialize(String s, Student student) {
if (student == null) {
return null;
}
try {
byte[] nameData = student.getName().getBytes(encoding);
ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + nameData.length);
buffer.putInt(student.getId());
buffer.putInt(nameData.length);
buffer.put(nameData);
return buffer.array();
} catch (UnsupportedEncodingException e) {
throw new SerializationException("Error when serializing string to byte[] due to unsupported encoding " + this.encoding);
}
}
}
反序列化
public class SupplierDeserializer implements Deserializer<Student> {
private String encoding = "UTF8";
@Override
public Student deserialize(String s, byte[] data) {
if (data == null) {
System.out.println("Null recieved at deserialize");
return null;
}
try {
ByteBuffer buf = ByteBuffer.wrap(data);
int id = buf.getInt();
int nameLength = buf.getInt();
byte[] nameData = new byte[nameLength];
buf.get(nameData);
String name = new String(nameData, encoding);
return new Student(id, name);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
}
@Override
public void close() {
}
}
生产者常用配置参数
生产者有许多的配置参数,并有些都具有默认值。这里列举一些常用的参数。
bootstrap.serverskey.serializervalue.serializerpartitioner.class
影响kafka吞吐量和性能的几个参数:
acksretriesmax.in.flight.requests.per.connection
acks是broker响应的,当生产者发送一条消息到broker时,生产者将会得到一个响应,这个响应要么是 RecordMetaData 或 异常。参数 acks 的值:0,1,all。如果我们设置 acks 的值为0,生产者将不会等待响应,这样做将会有三个影响:
- 可能出现消息丢失
- 高吞吐量
- 不会重试
如果我们的系统允许出现消息丢失的情况,可以配置为0。
如果我们设置 acks 的值为1,那么生产者发送消息后将会等待响应(只要leader写入成功,就返回响应),如果leader宕机和分发消息失败了,生产者将会在一段时间后重试发送。但是这样做,也会出现消息丢失的情况,因为我们只保证写入leader成功,没有保证写入复制集群成功。
如果想要保证消息百分之百的可靠性,我们可以设置 acks 的值为 all。但是这样系统将会变得慢,因为我们需要等待消息写入所有broker中。然后,我们使用异步发送的方式提高系统性能。
当消息发送失败时,生产者将会重试。那么会重试几次呢?重试的时间间隔又是多少呢?
retries 参数就是用来控制重试的次数。retry.backoff.ms. 用来控制重试的时间间隔,默认值是100毫秒。
max.in.flight.requests.per.connection 一次请求可以发送最大消息数量。我们设置它为比较高的值(值越高占用内存越大),使用异步发送提供系统性能。但是如果是异步发送将会顺序问题:假设我们发送10消息到一个分区中,第一批次发送了5条消息失败,第二批次发送5条消息成功,然后重新发送第一批次的消息成功了。它们到达分区的顺序发生了改变了。如果消息顺序对我们很重要,我们必须这样处理:
- 使用异步发送
max.in.flight.requests.per.connection=1
本文详细介绍了Kafka的生产者工作流程,包括同步发送与异步发送、自定义分区器和序列化。生产者需要配置参数如bootstrap.servers、key.serializer和value.serializer。默认分区器根据消息key进行分区,也可自定义分区器以满足特定需求。此外,讨论了自定义序列化/反序列化以适应复杂内容的发送,并列举了一些关键的生产者配置参数,如acks和retries,以确保消息的可靠性和系统性能。
1580

被折叠的 条评论
为什么被折叠?



