生产者客户端由两个线程协调运行,这两个线程分别为主线程和Sender线程(发送线程):
(1)在主线程中由KafkaProducer创建消息,然后通过可能的拦截器、序列化器和分区器的作用之后缓存到消息累加器(RecordAccumulator,也称消息收集器)中。
(2) Sender线程负责从RecordAccumulator中获取消息并将其发送到Kafka中。
我们从创建一个KafkaProducer对象开始,它将创建一个ProducerRecord对象。这个对象是Kafka中的一个核心类,它代表生产者发送到Kafka服务器端的一个消息对象,即一个Key-Value的键值对。
在ProducerRecord对象中,包含如下信息:
(1)Kafka服务器端的主题名称(Topic Name)。
(2)Topic中可选的分区号。
(3)时间戳。
(4)其他Key-Value键值对。
ProducerRecord创建成功后,需要经过拦截器、序列化器将其转换为字节数组,这样它们才能够在网络上传输,然后消息到达分区器。分区器的作用是根据发送过程中指定的有效的分区号,将ProducerRecord发送到该分区;如果没有指定Topic中的分区号,则会根据Key进行Hash运算,将ProducerRecord映射到一个对应的分区。
ProducerRecord默认采用当前的时间,用户也在创建ProducerRecord的时候提供一个时间戳。
Kafka最终使用的时间戳取决于Topic的配置,而Topic时间戳的配置主要有以下两种:
(1)CreateTime表示使用生产者产生的时间戳作为Kafka最终的时间戳。
(2) LogAppendTime表示生产者记录中的时间戳在将消息添加到其日志中时,将由Kafka Broker重写。
ProducerRecord在经过主线程后,最终由发送线程发送到Kafka服务器端。Kafka Broker在收到消息时会返回一个响应,如果写入成功,则返回一个RecordMetaData对象,它包含主题和分区信息,以及记录在分区里的偏移量,上面两种时间戳类型也会返回给用户。如果写入失败,则返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是写入失败,就返回错误消息。
要在Kafka消息集群中写入消息,首先需要创建一个生产者对象。Kafka生产者有三个必选的属性:
该属性指定Kafka集群中Broker的地址列表,其地址的格式为host:port,如果有多个Broker地址,可以用逗号进行分隔。当然,该地址列表中不需要包含所有Broker,因为Kafka会将整个集群的元信息和配置信息存储在ZooKeeper中,生产者会从给定的Broker中,通过ZooKeeper查找到其他Kafka Broker地址信息。在生产环境中,建议至少要提供两个Broker地址信息,这样做的目的是支持容错。一旦其中一个Broker出现了宕机,生产者仍然能够通过另一个Broker连接到Kafka集群上。
发送到Kafka的消息需要经过序列化后,才能实现正常发送与转发。生产者将要发送的消息通过序列化器进行序列化后,生成一个key/value的值,并发送到Broker。当创建Kafka生产者的时候,生产者需要知道采用何种方式把消息(即Java对象)转换为字节数组。因此通过生产者端的参数key.serializer就是这一项配置的工作。它必须实现org.apache.kafka.common.serialization.Serializer接口,然后生产者会使用这个类把键对象序列化为字节数组。
value.serializer与key.serializer一样,用于指定的类会将值序列化。
示例代码:
01 Properties props = new Properties();
02 props.put("bootstrap.servers", "kafka101:9092");
03 props.put("acks", "all");
04
05 props.put("retries", 0);
06 props.put("batch.size", 16384);
07 props.put("linger.ms", 1);
08 props.put("buffer.memory", 33554432);
09
10 props.put("key.serializer",
11 "org.apache.kafka.common.serialization.StringSerializer");
12 props.put("value.serializer",
13 "org.apache.kafka.common.serialization.StringSerializer");
14
15 Producer<String, String> producer = new KafkaProducer<String, String>(props)
第05行~第08行代码不是必需的,如果没有配置这些参数,将会采用默认的参数值
Kafka生产者发送的消息必须经过序列化。实现序列化可以简单地总结为两步,第一步继承序列化Serializer接口;第二步实现接口方法,将指定类型序列化成byte[],或者将byte[]反序列化成指定数据类型。接下来,我们来实现序列化/反序列化方式。
实现Java对象的序列化有很多不同的方式。这里我们介绍基于FastJson的序列化方式。Fastjson是一个Java库,可以将Java对象转换为JSON格式,当然它也可以将JSON字符串转换为Java对象,加入以下依赖。
01
02 com.alibaba</groupId>
03 fastjson</artifactId>
04 1.2.68</version>
05 </dependency>
Kafka生产者的消息发送主要有三种模式:发后即忘(fire-and-forget)、同步模式(sync)及异步模式(async)。
只管向Kafka中发送消息而并不用关心消息是否正确到达。在大多数情况下,这种发送方式没有什么问题,不过在某些时候(比如发生不可重试异常时)会造成消息的丢失。这种发送方式的性能最高,可靠性也最差。EmployeeProducer就是采用的这种模式。
要实现同步的发送方式,可以利用返回的Future对象的阻塞等待Kafka的响应即可实现,直到消息发送成功。如果发生异常,就需要捕获异常并交由外层逻辑处理。改造一下之前的EmployeeProducer代码,使用同步模式将消息发送到Kafka服务器端。
为了在异步发送消息的同时能够对异常情况进行处理,生产者提供了回调支持。一般在send()方法中指定一个Callback的回调函数,Kafka在返回响应时调用该函数来实现异步的发送确认。改造一下之前的EmployeeProducer生产者代码,使用异步模式将消息发送到Kafka服务器端。
Kafka消息系统为什么要进行Topic的分区呢?我们都知道Kafka的主题Topic是由分区组成的,而将Topic进行分区的主要目的就是提供负载均衡和容错的能力,以及实现系统的高伸缩性和高可用性。Kafka的消息组织方式实际上是三层结构:主题—分区—消息。主题下的每条消息只会保存在某一个分区中,而不会在多个分区中保存多份。在创建Topic的时候可以指定每个分区的副本数,用于支持分区中消息的容错。
不同的分区能够放置在Kafka集群中不同的节点上,而生产者和消费者在产生消息和消费消息的时候,也都是针对分区进行的,这样每个节点的机器都能独立执行各自分区的读写请求处理,并且还可以通过添加新的Kafka节点来增加整体系统的吞吐量。
常见的分区策略:
(1)默认分区策略(org.apache.kafka.clients.producer.internals.DefaultPartitioner)。
(2)轮询分区策略(org.apache.kafka.clients.producer.RoundRobinPartitioner)。
如果key值为null,并且使用了默认的分区器,Kafka会根据轮询(Random Robin)策略将消息均匀地分布到各个分区上。
(3)黏性分区策略(org.apache.kafka.clients.producer.UniformStickyPartitioner)。
黏性分区策略就像黏住这个分区一样,只要这个分区没有被填满,就会尽可能地坚持使用该分区。这种策略首先会选择单个分区发送所有无key的消息,一旦这个分区已填满,黏性分区策略就会随机选择另一个分区。通过查看源码,可以得到黏性分区策略是通过org.apache.kafka.clients.producer. internals.StickyPartitionCache来实现的。
(4)散列分区策略。
如果键值不为null,并且使用了默认的分区器,Kafka会对键进行散列,然后根据散列值把消息映射到对应的分区上。
(5)自定义分区策略。前面提到Kafka生产者的分区策略都实现了接口org.apache.kafka.clients. producer.Partitioner,用户可以根据需要对数据使用不一样的分区策略,只需要实现该接口即可。用户创建了自定义分区策略后,只需要在生产者的Properties中指定ProducerConfig.PARTITIONER_CLASS_CONFIG参数即可。
Kafka发送消息的时候,可以在生产者端和Broker端进行消息的压缩。在一般情况下,建议采用的压缩机制是:生产者端负责压缩;Broker端负责保持;消费者端负责解压。Kafka采用这样的压缩机制,主要是节约CPU的时间去换磁盘存储的空间,以及网络I/O的传输量。这样的做法可以以较小的CPU开销带来更少的磁盘占用或更少的网络I/O传输。
在KafkaProducer的主线程中可以创建一个或多个ProducerInterceptors(拦截器)。拦截器是从Kafka 0.10版本中引入的,在生产者端和消费者端均可设置,拦截器主要用于实现生产者端和消费者端的定制化控制逻辑。对生产者而言,拦截器需要实现org.apache.kafka.clients.producer.ProducerInterceptor接口。
ProducerInterceptor中的方法:
01 package org.apache.kafka.clients.producer;
02
03 import org.apache.kafka.common.Configurable;
04
05 public interface ProducerInterceptor<K, V> extends Configurable {
06
07 public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
08
09 public void onAcknowledgement(RecordMetadata metadata, Exceptionexception);
10
11 public void close();
12 }
接口中的三个方法:
(1)onSend。
该方法将在KafkaProducer.send方法的主线程中执行。KafkaProducer确保在消息被序列化以前,调用ProducerInterceptor.onSend方法。用户可以在该方法中对消息进行操作,但最好不要修改消息所属的Topic和分区,否则会影响目标分区的计算。
(2)onAcknowledgement。
该方法会在消息被确认应答之前或消息发送失败时调用。如果生产者采用的是异步发送机制,该方法通常是在生产者回调逻辑触发之前被调用的。需要注意的是,该方法运行在生产者的I/O线程中,因此不要在该方法中放入很重的逻辑,否则会影响生产者的消息发送性能。
(3)close。
关闭拦截器之前,可以将一些资源清理工作放在close方法中。
需要注意的是,如果指定了多个连接器,生产者将按照指定顺序调用它们。如果拦截器中出现了异常,生产者会将异常的错误信息记录到错误日志中,而不是向上传递。
这个参数控制了发送消息的耐用性,用于指定分区中必须有多少个副本成功接收到消息,之后生产者才会认为这条消息写入是成功的,即生产者需要leader确认请求完成之前接收的应答数。通过查看ProducerConfig的源码可以看出acks参数的本质其实就是一个字符串。
acks参数有三种类型的值。
1)acks=1。
这是acks参数的默认值。Kafka的生产者将消息发送到Kafka的Broker服务器端。只要Topic分区中的Leader成功写入消息,就算该消息成功发送。这时候,生产者就会收到Kafka服务器端Broker的成功确认信息,说明发送成功。
如果在生产者写入消息的过程中,Leader分区所在的Broker出现了宕机,将会造成消息无法正常写入。在重新选举Leader的过程中,生产者Producer会受到一个服务器端返回的错误信息。生产者为了支持容错,避免消息的丢失,会尝试重新发送该消息。直至消息成功写入Leader分区。
这里需要注意的是,如果消息已经成功写入Leader所在的分区,但还未同步至其他Follower分区的时候,如果Leader分区所在的Broker出现了宕机,这时候会造成写入Leader分区的消息丢失。所以在这种参数值的设置下,消息是可能丢失的。
2)acks=0。
在这种参数设置下,Kafka的生产者不需要等待任何服务器端的响应,所以这时Kafka集群可以达到最大的吞吐量。如果消息从生产者发送到写入Kafka消息系统的过程中出现异常,比如Broker宕机,生产者将不会得到任何反馈信息,也不会重发消息,导致消息丢失。
3)acks=-1或acks=all。
在这种参数设置下,Kafka集群将达到最高的可靠性。生产者发送完消息后,需要等待Leader分区和所有Follower分区都成功写入消息后,才返回给生产者一个成功写入消息的应答响应。
Kafka生产者的Sender线程在将消息发送到Kafka服务器端之前,会把消息缓存到内存中,这个参数就决定了消息缓存的内存大小,其默认值是32MB。如果生产者产生消息的速度大于将消息发送到服务器端的速度,那么生产者将会被阻塞,并最终导致生产者抛出一个RecordTooLargeException的异常。
在实际的生产环境下,应该根据实际情况进行测试最终决定buffer.memory参数值的大小。
当Kafka客户端将多个消息发送到同一个分区的时候,生产者为了减少客户端与服务器端的请求交互,会尝试将消息批量打包在一起,进行统一发送,这样有助于提升客户端和服务器端的性能。该配置的默认批次大小(以字节为单位)是16 384字节。如果消息的内存大小大于该参数的配置,将不会进行批量打包的过程。
通过提升batch.size的大小,可以允许更多数据缓冲在分区中,那么一次请求服务器端所发送出去的数据量就更多了,这样吞吐量可能会有所提升。但是这样会造成大量内存的浪费。反过来如果减小batch.size的大小,则会系统地降低吞吐量。如果将batch.size设置为0,则批处理机制被禁用。所以需要在这里按照生产环境的发消息速率,调节不同的batch.size大小,从而设置一个最合理的参数。
该参数指定给到Topic中数据的压缩类型,其有效值的设置可以是标准的压缩方式,例如,‘gzip’、‘snappy’、‘lz4’、‘zstd’,同时该参数也可以是’uncompressed’,在这种设置下,消息数将不会被压缩。
当生产者向服务器端发送请求时,传递给服务器端ID字符串。通过这个ID字符串,Kafka服务器端就可以追踪请求的资源,其本质就是将生产者及其请求的资源进行逻辑上的隔离。
当生产者不再往服务器端发送消息时,这个参数用来决定关闭生产者连接的时间阈值,其默认值是9min。
该参数决定消息在由生产者发送到服务器端之前,在客户端延长发送的时间。通过这样的延时发送机制,可以将多个消息组合成一个批处理进行统一发送。从本质上讲,该参数与之前提到过的batch.size参数类似。合理设置batch.size参数和linger.ms参数,将很好地利用Kafka批处理机制。把linger.ms设置得太小了,比如默认就是0ms,或者设置为5ms,那可能导致Batch虽然设置了32KB,但是经常是还没凑够32KB的数据,5ms之后就直接强制Batch将数据发送出去,这会导致你的Batch形同虚设,一直凑不满数据。
该配置控制KafkaProducer.send()和KafkaProducer.partitionsFor()将消息阻塞多长时间。此外也可能是因为缓冲区已满或元数据不可用,导致这些方法被阻止。在用户提供的序列化程序或分区器中的锁定不会计入此超时,其默认值为60 000ms。
Kafka生产者能发送消息的最大值,默认值为1MB。此设置将限制生产者的单个请求中发送的消息批次数,以避免发送过大的请求。这个参数涉及其他一些相关参数,比如服务器Broker端的message.max.bytes参数,如果message.max.bytes参数设置为10,而max. request.size设置为20,这时候就可以造成生产者报错。
如果生产者出现了异常,或者消息没有成功写入Kafka的服务器端,生产者可以配置重试的参数值,通过生产者端的内部重试机制来执行恢复,并不是直接将异常抛出。如果重试达到设定次数,生产者才会放弃重试并抛出异常。retries参数的默认值是0。同时,生产者的重试还与retry.backoff.ms参数有关,该参数用来设定两次重试之间的时间间隔,其默认值是100ms,从而避免无效的频繁重试。在配置retries参数和retry.backoff.ms参数之前,可以设定总重试时间要大于异常恢复时间,最好先估算一下异常恢复时间,避免生产者过早放弃重试。
这个参数用来设置socket接收消息缓冲区的大小,该缓存区大小的默认值32KB。如果将其值设置为-1,则使用操作系统的默认值。
这个参数用来设置socket发送消息缓冲区的大小,默认值为128KB。与receive.buffer.bytes参数一样,如果将其值设置为-1,则使用操作系统的默认值。
消息由生产者发出后,该参数用于决定生产者等待请求响应的最长时间,其默认值为40s。如果响应的时间超过了该参数的设置,客户端将按照重试策略进行重试。注意,这个参数值需要比Broker端的参数replica.lag.time.max.ms值要大,这样可以减少因客户端重试引起的消息重复的概率。
该参数表示Kafka客户端重连的最大时间。每次连接失败,重连时间都会成指数级增加,每次增加的时间会存在20%的随机浮动,以避免连接风暴。
该参数表示Kafka客户端每次重连时候的间隔时间。
当生产者调用send方法后,该参数用于指定客户端等待发送成功或失败报告时,客户端等待时间的上限。时间上限包括:
1) 一条消息在发送前的延时时间。
2) 生产者等待服务器端Broker确认信息的等待时间。
3) 失败时的重试时间。
该参数表示一个实现了org.apache.kafka.clients.producer.Partitioner接口的类。Kafka将使用这个类进行分区操作,其默认值是org.apache.kafka.clients.producer.internals.DefaultPartitioner。通过实现这个接口,可以实现自定义分区。
该参数表示生产者主动终止当前正在进行的操作之前,Kafka等待操作状态更新的最大时间,其默认值是1min。如果该值大于Broker中max.transaction.timeout.ms的设置,则请求失败,并报"InvalidTransactionTimeout"错误。
在事务传递过程中该参数用于表示某个事务的ID。这样可以保证跨多个生产者会话时语义的可靠性。因为它允许客户端保证在开始任何新事务之前使用相同的Transactional Id的事务来完成。
该参数表示在消息被阻塞前,每个客户端上发送的未应答请求的最大数量,其默认值是5。注意,如果该参数值设置大于1,并且消息发送失败,则由于客户端的重试增加消息重新排序的风险。
该参数表示当超过这个时间间隔时,系统就会更新元信息,其默认值5min。Kafka的元数据信息由ZooKeeper维护,包含Topic信息、副本信息、分区信息、Broker信息。
当Topic处于空闲状态时,该参数用于控制生产者抓取Topic元信息的时间。
文章来源:《Kafka进阶》 作者:赵渝强
文章内容仅供学习交流,如有侵犯,联系删除哦!