kafka生产者是线程安全的。
直接上代码
import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
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;
public class ProducerDemo {
public static void main(String[] args) {
String server = "127.0.0.1:9092";
String topic = "demo";
String value = "message";
// 参数配置
Properties properties = new Properties();
properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, server);
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 初始化
Producer<String, String> producer = new KafkaProducer<>(properties);
// 构造消息
ProducerRecord<String, String> record = new ProducerRecord<>(topic, value);
// 发送消息
producer.send(record);
// 回收资源
producer.close();
}
}
可以直接使用CommonClientConfigs
和ProducerConfig
定义的参数变量名。
host1:port1,host2:port2
,多个用",“隔开,默认值为”"。byte[]
)。KafkaProducer和ProducerRecord中的泛型<String,String>
对应的就是消息的key、value,通过这两个参数执行消息的序列化器。
生产者实例创建完成之后,接下来就是构建消息,即创建ProducerRecord
对象了,ProducerRecord
有很多的构造方法。
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers);
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value);
public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers);
public ProducerRecord(String topic, Integer partition, K key, V value);
public ProducerRecord(String topic, K key, V value);
public ProducerRecord(String topic, V value);
上面的实例使用的是最后的那个构造方法,最简单的。需要注意的是:ProducerRecord
的创建是一个很频繁的动作。
在生产者实例和消息创建好之后,就可以发送消息了。发送消息主要有三种模式:发后即忘(fire-and-forget)、同步(sync)和异步(async)。
上面的示例就是使用的发后即忘的模式,发只管往kafka发送消息, 并不关心消息是否正确送达。通常来讲,这种方式没有问题,不过在kafka发生了不可重试异常的时候会造成数据的丢失。这种方式是性能最高的,也是可靠性最差的。
KafkaProducer的send()方法井非是void类型,它有两个重载。
Future<RecordMetadata> send(ProducerRecord<K, V> record);
Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback);
同步发送代码如下:
try {
producer.send(record).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
或者
Future<RecordMetadata> future = producer.send(record);
try {
RecordMetadata recordMetadata = future.get();
System.out.println(recordMetadata.topic() + ":" + recordMetadata.partition() + ":" + recordMetadata.offset());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
实际上send()
方法本身就是异步的send()
返回的Future
的get()
方法则会阻塞kafka的响应。get()
返回的RecordMetadata
对象包含了一些消息的元数据信息,如:主题、分区、偏移量等。
KafkaProducer中一般会发生两种类型的异常:可重试的异常和不可重试的异常。常见的可重试异常有:NetworkException、LeaderNotAvailableException、UnknownTopicOrPartitionException、NotEnoughReplicasException、NotCoordinatorException等。比如NetworkException表示网络异常,这个有可能是由于网络瞬时故障而导致的异常,可以通过重试解决;又比如LeaderNotAvailableException表示分区的leader副本不可用,这个异常通常发生在leader副本下线而新的leader副本选举完成之前,重试之后可以重新恢复。不可重试的异常,比如1.4节中提及的RecordTooLargeException异常,暗示了所发送的消息太大,KafkaProducer对此不会进行任何重试,直接抛出异常。
对于可重试的异常,如果配置了retries参数,那么只要在规定的重试次数内自行恢复了,就不会抛出异常。retries参数的默认值为0,配置方式参考如下:
properties.put(ProducerConfig.RETRIES_CONFIG, 3);
同步发送的方式可靠性高,要么消息发送成功,要么消息异常。发生异常,可以捕获并作出相应的处理,不会像“发后即忘”那样造成消息丢失。同步发送性能差很多,需要阻塞等待一条消息发送完成后才能发送下一条。
再来看一下异步消息发送,使用Callback回调的方式。kafka有响应时就有回调,要么发送成功,要么异常。
producer.send(record, (recordMetadata, e) -> {
if (e != null) {
e.printStackTrace();
} else {
System.out.println(recordMetadata.topic() + ":" + recordMetadata.partition() + ":" + recordMetadata.offset());
}
});
对于同一分区而言,如果消息1在消息2之前发送,那么callback1在callback2之前调用,回调函数的调用也可以保证分区有效。
生产者发送消息到Kafka时,需要使用序列化器将消息序列化为字节数组(byte[]
),而消费者需要使用反序列化器将消息还原。这里使用的序列化器为:org.apache.kafka.common.serialization.StringSerializer
,除了String类型的序列化器之外,还有ByteArray、ByteBuffer、Bytes、Double、Integer、Long这几种类型,都实现了org.apache.kafka.common.serialization.Serializer
接口,这个接口有三个方法。
void configure(Map<String, ?> configs, boolean isKey);
byte[] serialize(String topic, T data);
void close();
configure()
用来配置当前类,serialize()
用来执行序列化操作,close()
一般是个空方法,如果实现了该方法,需要保证方法的幂等性,因为这个方法可能会被KafkaProducer调用多次。
生产者序列化器和消费者反序列化器需要一一对应。如果Kafka提供的几种序列化器都无法满足要求,可以选择使用Avro、JSON、Thrift、ProtoBuff等。
消息在发往kafka的时候,可能需要经过拦截器(Interceptor)、序列化器(Serializer)和分区器(Partitioner)的一系列作用后才能真正的发往kafka。消息序列化后就会确定它发往的分区,如果在ProducerRecord里面指定了partition字段,那么就不需要分区器的作用了,partition代表的就是需要发往的分区号。
如果partition不指定,那么就需要分区器的参与了,根据key在计算partition值。
kafka默认的分区器是org.apache.kafka.clients.producer.internals.DefaultPartitioner
,它实现了org.apache.kafka.clients.producer.Partitioner
接口
int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
void close();
partition()方法用来计算分区号,返回值为int类型。参数分别表示主题、键、序列化键、值、序列化值、以及集群的元数据信息。close()方法用来在分区器关闭的时候回收一些资源。
在默认分区器DefaultPartitioner
中,分区号的计算规则为:key不为空,key进行hash(采用MurmurHash2算法,高效率、低碰撞),根据得到的hash值计算分区号,拥有相同key的消息会被写入同一分区。key为空,消息则以轮训的方式发往各分区。
注意:如果key不为空,分区号是所有分区中的任意一个;key为空,分区号是可用分区中的一个。
生产者拦截器可以用来在消息发送前做一些准备工作,如果过滤不合规的消息、修改消息内容等。
生产者拦截器使用比较方便,主要是实现org.apache.kafka.clients.producer.ProducerInterceptor
接口。
ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
onAcknowledgement(RecordMetadata metadata, Exception exception);
void close();
onSend()
用来对消息进行一些定制化操作,一般来说不要修改消息的topic、key、partition等。onAcknowledgement()
消息应答之前或者消息发送失败时调用,在Callback之前执行。这个方法在Producer的I/O线程中,所以这个方法实现逻辑越简单越好,否则会影响消息的发送速度。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 PrefixInterceptor implements ProducerInterceptor<String, String> {
String prefix = "prefix-";
private volatile int success;
private volatile int error;
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
String newValue = prefix + record.value();
return new ProducerRecord<>(record.topic(), record.partition(), record.timestamp(), record.key(), newValue, record.headers());
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
if (exception == null) {
success++;
} else {
error++;
}
}
@Override
public void close() {
if (success + error > 0) {
double successRate = (double) success / (success + error);
System.out.printf("成功率: %f%%\n", successRate * 100);
}
}
@Override
public void configure(Map<String, ?> configs) {
}
}
然后需要在properties指定拦截器
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, PrefixInterceptor.class.getName());
发送的原始消息内容为message
,消费时会发现这个消息变成了prefix-message
。
KafkaProducer还可以指定多个拦截器形成拦截器链。配置ProducerConfig.INTERCEPTOR_CLASSES_CONFIG
的时候,多个拦截器使用,
隔开。
注意:如果拦截器的执行需要依赖于前一个拦截器的输出,可能会产生“副作用”,上一个拦截器执行失败,那么这个拦截器也无法争取执行。
生产者客户端整体架构如下:
生产者客户端由两个线程协调运行,主线程+Sender线程(发送线程)。主线程中,KafkaProducer创建消息,然后消息经过拦截器、序列化器、分区器后,缓存到消息累加器RecordAccumulator(也叫消息收集器中)。Sender线程从RecordAccumulator获取消息并发送到kafka。
RecordAccumulator缓存消息以便Sender线程可以批量发送,减少网络传输的资源以提升性能。RecordAccumulator缓存大小可以通过buffer.memory
配置,默认32MB。生产者发送消息的速度大于发送到服务器的速度,导致生产者空间不足,send()
方法要么阻塞,要么抛出异常,取决于max.block.ms
,默认60秒。
RecordAccumulator为每一个分区维护了一个双端队列,队列内容是ProducerBatch,主线程发送的消息会追加到队尾,Sender线程从队首读取消息。注意是ProducerBatch而不是ProducerRecord,ProducerBatch指一个消息批次,可以理解为多个ProducerRecord组成了一个ProducerBatch。
消息在网络上都是以字节(Byte)形式传输的,在发送之前创建一块内存区域保存对应的消息。Kafka生产者客户端中,通过java.io.ByteBuffer实现消息的创建和释放。频繁的创建和释放比较耗费系统资源的,在RecordAccumulator内部有一个BufferPool,用来实现ByteBuffer的复用。BytePool只针对特定大小ByteBuffer进行管理,而其它大小的ByteBuffer不会缓存到BufferPool中,这个大小可以通过参数batch.size
指定,默认16KB。
Sender从RecordAccumulator获取消息后,将原本的<分区, Deque
Sender线程发往Kafka之前还会保存到InFlightRequests中,格式为Mapmax.in.flight.requests.per.connection
限制每个连接,默认是5,表示最多缓存5个未响应的请求,超过后就不能再向这个连接发送请求了,除非缓存的请求收到了响应。可以通过Deque
可以通过InFlightRequests获得LeastLoadedNode,即Node中负载最小的那个。负载最小是通过每个Node在InFlightRequests中还未相应的请求决定的,未确定的请求越多,负载越大。下图,Node1的负载最小。
KafkaProducer要将消息追加到给定主题的某个分区对应的leader副本之前,需要知道主题的分区数量,然后计算目标分区,之后KafkaProducer需要知道leader副本所在的broker结点地址、端口才能建立连接,最终才能叫消息发送到Kafka,这一过程中需要的信息都属于元数据信息。
元数据是指Kafka集群的元数据,这些元数据具体记录了集群中有哪些主题,这些主题有哪些分区,每个分区的 leader副本分配在哪个节点上,follower副本分配在哪些节点上,哪些副本在AR、ISR等集合中,集群中有哪些节点,控制器节点又是哪一个等信息。
当客户端中没有需要使用的元数据信息时,比如没有指定的主题信息,或者超过metadata.max.age.ms
时间没有更新元数据都会引起元数据的更新操作。客户端参数metadata.max.age.ms
的默认值为300000,即5分钟。元数据的更新操作是在客户端内部进行的,对客户端的外部使用者不可见。当需要更新元数据时,会先挑选出leastLoadedNode,然后向这个Node发送MetadataRequest请求来获取具体的元数据信息。这个更新操作是由Sender线程发起的,在创建完MetadataRequest之后同样会存入InFlightRequests,之后的步骤就和发送消息时的类似。元数据虽然由Sender线程负责更新,但是主线程也需要读取这些信息,这里的数据同步通过synchronized和final关键字来保障。
acks
这个参数用来指定分区中必须要有多少个副本收到这条消息之后,生产者才会认为这条消息是写入成功的。这个参数在生产者客户端中非常重要,它涉及到可靠性和吞吐量之间的权衡。注意:取值是字符串。
acks = 1。默认为1。生产者发送消息后,只要leader副本成功写入消息,那么它就会收到来自服务端的响应。leader崩溃,重新选举leader副本的过程中,导致消息无法写入,生产者会收到一个错误的响应,为了避免消息丢失,生产者可以选择重发消息。写入leader并返回成功响应给生产者,但是其他follower副本拉取之前leader崩溃了,那么消息还是会丢失,因为新的leader中并没有这条消息。acks = 1是消息可靠性和吞吐量之间的这种方案。
acks = 0。生产者发送消息之后不需要等待服务端的响应。消息写入Kafka的过程中出现了异常,导致kafka没有收到这条消息,生产者也无法得知。acks = 0可以达到最大的吞吐量。
acks = -1 或者acks = all。生产者在发送消息后,需要等待ISR所有副本都成功写入消息之后才能收到服务端的响应。acks = -1 或者acks = all可以达到最高的可靠性,但是并不意味着消息一定可靠,因为ISR中可能只有leader副本,这样就退化成了acks = 1的情况。要保证更高的可靠性还需要配合参数min.insync.replicas
。
max.request.size
这个参数用来限制生产者客户端能够发送消息的最大值,默认为1MB。
retries和retries.backoff.ms
retries参数用来配置生产者重试次数,默认值0,即在发生异常的时候不进行重试操作。在生产者发出消息到写入服务器之前,可能会发生一些异常,网络抖动、leader选举等,这些异常是可以通过内部重试机制而成功发送消息的,如果重试达到设置的次数,生产者放弃重试并返回异常。不是所有的异常都可以通过重试来解决的,比如消息太大。
重试相关的还有一个参数retries.backoff.ms
,默认值为100ms,用来设置两次重试的时间间隔。
compression.type
这个参数用来设置消息的压缩方式,默认为"none",即不压缩。还可以配置为"gzip"、“snappy”、“lz4”。压缩消息可以减少网络传输量、降低网络I/O,从而提高性能。消息压缩是一种使用时间换空间的优化方式,如果对延迟有一定的要求,则不建议消息压缩。
connections.max.idel.ms
这个参数用来指定在多久后关闭限制的连接,默认9分钟。
linger.ms
这个参数用来指定生产者发送ProducerBatch之前等待更多消息(ProducerRecord)加入ProducerBatch的时间,默认值为0。生产者客户端会在ProducerBatch被填满或者等待时间超过linger.ms时发送出去。增大这个参数值会增加消息的延迟,同时能提升一定的吞吐量。
receive.buffer.bytes
这个参数用来设置Scoket接受消息缓冲区(SO_RECBUF)的大小,默认值为32KB。设置为-1,则使用操作系统的默认值。如果Producer与Kafka处于不同机房,可以适当的增大这个参数值。
send.buffer.bytes
这个参数用来设置Scoket发送消息缓冲区(SO_SNDBUF)的大小,默认128KB。-1则使用操作系统默认值。
request.timeout.ms
配置Producer等待请求的最长时间,默认30000ms。请求超时可以选择重试。
参数名称 | 默认值 | 参数解释 |
---|---|---|
bootstrap.servers | “” | kafka集群链接地址 |
key.serializer | “” | key对应的序列化类 |
value.serializer | “” | value对应的序列化类 |
buffer.memory | 33554432(32MB) | 生产者客户端缓存消息的缓冲区大小 |
batch.size | 16384(16KB) | ProducerBatch可复用内存区域大小 |
client.id | “” | 客户端ID |
max.block.ms | 6000 | 生产者缓冲区已满,或者没有可用元数据时,发送消息的阻塞时间 |
partitioner.class | org.apache.kafka.clients.producer.internals.DefaultPartitioner |
分区器 |
enable.idempotence | false | 是否开启幂等功能 |
interceptor.classes | “” | 生产者拦截器,多个使用, 隔开 |
max.in.flight.requests.per.connection | 5 | 最多缓存未响应的请求数 |
metadata.max.age.ms | 30000(5分钟) | 这个时间内元数据没有更新的话会强制更新 |
transactional.id | null | 设置事务的ID,唯一 |