快速入门
Kafka消息生产者逻辑应当具备以下步骤:
- 配置生产者参数,创造生产者实例
KafkaProducer
- 构建待发送的消息
ProducerRecord
- 发送消息
- 程序退出或者无需生产消息时关闭生产者实例
KafkaProducer
示例代码:
public class Producer {
public static void main(String[] _args) throws Exception {
Properties p = new Properties();
p.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.142.128:9092"); //Broker的IP地址和端口
p.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); //key序列化器
p.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//value序列化器
p.put(ProducerConfig.CLIENT_ID_CONFIG, "producer.client.id.demo");
KafkaProducer<String, String> producer = new KafkaProducer<>(p);
for(int i = 0; i < 100; i++) { //发送100条消息
ProducerRecord<String, String> record = new ProducerRecord<>("topic-test", "message-" + i);
RecordMetadata meta = producer.send(record).get();
System.out.println(meta.topic() + " " + meta.partition() + " " + meta.offset());
}
producer.close();
}
}
上述代码向地址为192.168.142.128:9092
的Broker主题topic-test
发送了100条消息
生产者实例的创建
创建生产者实例KafkaProducer
前需要指定相应的配置参数,配置参数需要存储在Properties
中,有3个参数是必须要指定的:
bootstrap.servers
(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG
):该参数指定了生产者客户端连接Kafka集群所需的Broker列表,格式为host:port
,用逗号分隔。这里并非需要所有的Broker,生产者可以根据一个Broker来自动获取其它Broker的信息。建议设置为2个以上,当客户端连接的Broker因为网络故障或者该Broker宕机时可以根据该配置连接至其它Brokerkey.serializer
和value.serializer
(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG
和ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG
):在传递给Broker时必须将消息ProducerRecord
中的key
和value
转换为字节数组(byte[]
)的形式,其转换逻辑由key.serializer
和value.serializer
实现,这两个参数必须是一个实现了Serializer
接口的类的全限定名,且具备一个无参构造方法。
将上述参数存入Properties
对象后,就可以构造生产者实例KafkaProducer
了。
消息的发送
创建完生产者实例后,接下来的工作就是构建消息ProducerRecord
,一个ProducerRecord
实例代表一个独立的消息,ProducerRecord
的成员变量有:
public class ProducerRecord<K, V> {
private final String topic; //主题
private final Integer partition; //分区编号
private final Headers headers; //消息头
private final K key; //键
private final V value; //值
private final Long timestamp; //消息的时间戳
}
ProducerRecord
具备了多个构造方法,其中topic
必须不为null
,其它参数均可为null
。指定分区编号partition
后可以将消息发送到该主题下的指定分区。消息头一般用来设定一些与应用相关的信息。键key
可以让消息进行一个二次分类,相同key
的消息可以被发送到同一个分区,同时也会影响到Broker端的日志压缩。value
为消息体,一般不为空。timestamp
指消息时间戳,有CreateTime
和LogAppendTime
两种类型,前者为消息创建时间,后者为消息追加到服务器日志的时间。
构造好ProducerRecord
后,就可以通过KafkaProducer
将这个消息发送给Broker了,发送消息主要有两种模式:同步发送、异步发送。具体可以通过KafkaProducer
的send
方法来实现:
public Future<RecordMetadata> send(ProducerRecord<K, V> record);
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback);
可以看到send
方法都会返回一个Future
对象,因为这个方法本身就是异步的,如果不需要关心这个消息是否成功送达,那么只需要调用其send
方法就好了。如果应用程序需要同步发送,那么只需要调用get
方法阻塞等待Kafka
响应就可以了:
ProducerRecord<String, String> record = ...;
try {
RecordMetadata meta = kafkaProducer.send(record).get();
System.out.println(meta.topic() + "-" + meta.partition() + ":" + meta.offset());
} catch(ExecutionException | InterruptedException e) {
//Do something...
}
RecordMetadata
包含了消息的一些元数据,例如消息的主题、分区、分区中的偏移量、时间戳。
KafkaProducer
在发送消息时产生的异常分为两种类型:可重试异常和不可重试异常,这些异常都属于KafkaException
的子类,也都继承自RuntimeException
。
常见的可重试异常有:LeaderNotAvailableException
、NetworkException
、UnknownTopicOrPartitionException
、NotEnoughReplicasException
、NotCoordinatorException
等。例如NetworkException
表示网络异常,有可能是因为网络故障导致消息发送失败或者没有收到Broker的响应,可以通过重试来解决。LeaderNotAvailableException
则表示分区的leader
副本不可用,即发生在leader
副本下线而新的leader
正在进行选举,重试后可能恢复。不可重试的异常有RecordTooLargeException
,即消息过大,KafkaProducer
不会对其进行重试。
可以设置参数retries
(ProducerConfig.RETRIES_CONFIG
)来让KafkaProducer
自动进行重试操作而不抛出异常,该参数值的类型为数字,即最大重试次数,如果超出该次数依然没有成功发出消息,那么会直接抛出异常。
send
方法还有一个重载的方法,也就是可以添加一个回调Callback
。使用Callback
很简单,Kafka有响应就会进行回调,不论是消息发送成功或者抛出异常都会进行回调。
Callback
是一个接口,定义了一个方法:
void onCompletion(RecordMetadata metadata, Exception exception);
onCompletion
两个参数是互斥的:即要么metadata
为null
,要么exception
为null
。当消息发送成功时,metadata
不为null
,消息发送失败抛出异常时,exception
不为null
,metadata
为null
。
序列化器Serializer
序列化器负责将Java对象转换为字节数组的形式以便通过网络的形式发送给Broker。Serializer
本身是个接口,它定义了以下方法:
public interface Serializer<T> extends Closeable {
void configure(Map<String, ?> configs, boolean isKey);
byte[] serialize(String topic, T data);
default byte[] serialize(String topic, Headers headers, T data) {
return serialize(topic, data);
}
@Override
void close();
}
configure
方法用来配置当前类,serialize
两个重载方法都用于执行序列号操作,close
方法用来关闭当前序列化器,一般来说close
方法是空实现,如果有相应的逻辑实现则需要保证close
方法具有幂等性,因为同一个对象close
方法可能会被KafkaProducer
调用多次。
configure
方法传入的configs
参数和构造KafkaProducer
时传入的Properties
参数是等价的。
Kafka自带了几个基本的Serializer
实现类:StringSerializer
、UUIDSerializer
、IntegerSerializer
、LongSerializer
、ByteArraySerializer
、ByteBufferSerializer
、ShortSerializer
、FloatSerializer
、DoubleSerializer
。如果用户需要将自定义的对象序列化为字节流,那么需要自己定义对应的Serializer
实现类。
我们以比较常用的StringSerializer
为例:
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 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
}
}
configure
方法会尝试获取两个参数:key.serializer.encoding
和value.serializer.encoding
,即ProducerRecord
的key
和value
字符串编码格式。如果没有指定,则默认为UTF-8
。serialize
方法也很直观,直接将字符串通过编码将其转换为byte[]
。
生产者拦截器ProducerInterceptor
拦截器分为两种:生产者拦截器和消费者拦截器,这里我们讨论生产者拦截器ProducerInterceptor
。
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();
}
public interface Configurable {
void configure(Map<String, ?> configs);
}
KafkaProducer
会在执行序列化器和分区器之前调用onSend
方法并传入待发送的ProducerRecord
,在这里可以对ProducerRecord
进行一些修改。
发送完消息后,如果消息被Broker应答或者消息发送失败时就会调用到onAcknowledgement
方法,优先于用户的Callback
执行,类似于Callback
接口,metadata
和exception
两个参数互斥。还需要注意的是onAcknowledgement
方法逻辑应尽量简短,因为该方法由IO线程执行,否则会影响到消息发送的速度。
生产者可以通过指定参数interceptor.classes
来确定拦截器。
分区器Partitioner
在KafkaProducer
发送完消息前,分区器在调用完序列化器的逻辑后执行,其主要任务是决策消息发往的分区编号。
Partitioner
接口定义了以下方法:
public interface Partitioner extends Configurable, Closeable {
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
public void close();
}
partition
方法返回的int
值就是该消息的最终分区号
如果没有在构造KafkaProducer
前指定partitioner.class
参数,那么默认的分区器为DefaultPartitioner
,它实现了Partitioner
接口:
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) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic); //获取主题topic的分区信息
int numPartitions = partitions.size(); //获取该主题一共有多少个分区
if (keyBytes == null) { //如果该消息没有指定key
int nextValue = nextValue(topic); //通过AtomicInteger获取一个数
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size(); //将该数与分区数量进行模运算
return availablePartitions.get(part).partition(); //返回分区列表集合中第part的分区号
} else { //如果分区列表为空
return Utils.toPositive(nextValue) % numPartitions;
}
} else { //如果指定了key,以该key的hash取模的得出分区为准
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); //存入Map
if (currentCounter != null) {
counter = currentCounter;
}
}
return counter.getAndIncrement(); //原子加1
}
public void close() {}
}
其它重要的生产者参数
了解完上述生产者的相关概念后,我们再来关注一些比较重要的生产者参数。
acks
(ProducerConfig.ACKS_CONFIG
):这个参数用来指定分区中必须要有多少个副本收到这条消息,生产者客户端才认定这个消息是成功发送到了Kafka服务器。这个参数可以取三种值
ack = 1
,如果没有显式指定该参数那么就以这个数为默认值。具体策略是:生产者发送消息后,只要分区的leader
副本成功写入消息,那么就会收到来自服务器的成功响应。如果该leader
副本宕机,或者正在重新选举新的leader
副本,那么生产者就会收到一个错误的响应,之前我们提到过,生产者可以通过自定义参数来实现消息的自动重发。需要注意的是这种策略依然有可能造成消息的丢失:如果消息成功写入leader
副本,且在被其它follower
副本拉取之前leader
副本崩溃,那么此时消息还是会丢失,因为新选举的leader
副本不会存在该消息。该值是消息可靠性与吞吐量之间的折中方案,如果不是需要严格保证消息不丢失,那么建议还是使用该参数的。ack = 0
,这个策略是生产者发送消息后不等待服务端响应,如果消息在发送到Broker之间发生了异常,那么生产者也是无从知晓的,消息自然也就丢失了。此策略能够达到最大的吞吐量,也最容易造成消息的丢失。ack = -1
或ack = all
,生产者在发送消息后,需要等待ISR中所有的副本都成功写入消息后才能够收到服务端成功的响应,这种配置带来的消息可靠性是最强的。但是也不一定是绝对可靠,如果ISR中只有leader副本不就成了ack = 1
的情形?
max.request.size
:该参数可以限制生产者所能发送的消息的最大值,默认值为1048576B
,即1MB
。这个默认值可以满足绝大多数场景了,此外也并不建议盲目增大该值。因为该参数还涉及到了其它参数的联动,例如Broker端的message.max.bytes
参数,如果该参数小于客户端指定的max.request.size
,当发送的消息大于Broker端的message.max.bytes
参数时,就会抛出RecordTooLargeException
,之前也讲到了这是一个不可恢复的异常。retries
和retry.backoff.ms
:retries
作用不再赘述了。retry.backoff.ms
用来设定两次重试时间的间隔时间,单位为毫秒,默认值100
compression.type
,消息压缩方式,默认值为none
,即不压缩。也可以配置为gzip
、snappy
、lz4
。如果对消息时延有严格要求,则不建议进行压缩。connections.max.idle.ms
,用来指定多久之后关闭空闲的连接,单位为毫秒,默认为540000
,即9分钟。linger.ms
,该参数用来指定生产者发送ProducerBatch
(由多个ProducerRecord
序列化后的字节数组组成的集合)之前等待更多消息加入ProducerBatch
的时间,生产者会在ProducerBatch
被填满或者未被填满时等待时长超出linger.ms
将ProducerBatch
发送出去,默认为0。增大该值会增加消息的时延,但是可以增加Broker的吞吐量。receive.buffer.bytes
,Socket接收缓冲区(SO_RECBUF
)大小,默认为32768字节,即32KB。设置为-1时采用操作系统默认值。send.buffer.bytes
,Socket发送缓冲区(SO_SNDBUF
)大小,默认为131072字节,设置为-1时采用操作系统默认值。request.timeout.ms
,生产者等待Broker响应的最长时间,默认为30000ms,超过该时间后可以选择重试。
生产者客户端整体结构
整个生产者客户端需要至少两个线程运行:一个主线程,由用户自己定义,还有一个Sender
线程,该线程由KafkaProducer
负责构造。
从由主线程构造ProducerRecord
并调用KafkaProducer
的send
方法开始,发送流程如下:
- 如果指定了对应的拦截器,那么调用拦截器的
onSend
方法并传入ProducerRecord
- 依次调用
key
和value
对应的序列化器的serialize
方法,并传入ProducerRecord
中的key
和value
,获取返回的字节数组 - 调用分区器的
partition
方法,获取分区编号 - 根据分区编号,找到
RecordAccmulator
对应的双端队列。之后,将序列化后的结果存入ProducerBatch
,如果队尾的ProducerBatch
已满则构建一个新的ProducerBatch
并存入。 - 接下来,由
Sender
从RecordAccmulator
获取ProducerBatch
,由之前的【分区,Deque<ProducerBatch>】转换为【Node,Request】,Node
表示Kafka集群中的单个Broker。转换完成后将其暂存在InFlightRequests
中,表示这些消息还没有收到服务端的响应。 - 根据
Node
和Request
的映射关系,通过选择器Selector
发送到Kafka集群,等待响应 - 收到成功响应后,通过响应内容从
InFlightRequests
删除缓存。如果超时或者发生可重复发送异常,那么根据retries
参数策略进行重试 - 如果存在拦截器,则执行拦截器的
onAcknowledgement
方法。 - 如果在调用
KafkaProducer
的sender
方法传入了Callback
,那么执行Callback
定义的逻辑。