一个应用程序在很多情况下需要往Kafka写入消息:记录用户的活动(用于审计和分析),记录度量指标,保存日志消息,记录智能家电的信息,与其他应用程序进行异步通信,缓冲即将写入到数据库的数据,等等。
多样的使用场景意味着多样的需求:是否每个消息都很重要?是否允许丢失一小部分消息?偶尔出现重复消息是否可以接受?是否有严格的延迟和吞吐量要求?
不同的使用场景对生产者API的使用和配置会有直接的影响。
消息发送过程
首先创建一个ProducerRecord对象。
ProducerRecord对象包含目标主题和要发送的内容。
可以指定键或分区。
如果有键的话,将键序列化成字节数组,以便在网络上传输。同样地,要发送的内容,即值,也需要序列化成字节数组。
接下来,数据被传给分区器。
如果之前在ProducerRecord对象里指定了分区,那么分区器就不会再做任何事情,直接把指定的分区返回。
如果没有指定分区,那么分区器会根据ProducerRecord对象的键来选择一个分区。
选好分区后,生产者就知道该往哪个主题和分区发送记录了。
紧接着,这条记录被添加到了一个记录批次里。
这个批次里的所有消息都会被发送到相同的主题和分区上。
有一个独立的线程负责把这些记录批次发送到相应的broker上。
服务器在收到这些消息时会返回一个相应。
如果消息成功写入Kafka,就返回一个RecordMetaData对象,它包含了主题和分区信息,以及记录在分区里的偏移量。
如果写入失败,则会返回一个错误。
生产者在接收到错误之后会尝试重新发送消息,几次之后如果还是失败,就返回错误信息。
发送消息的方式
生产者可以使用单个消费者单个线程,也可以使用单个消费者多个线程。
或者增加消费者。
发送并忘记
producer.send(record)
我们把消息发送给服务器,但并不关心它是否正常到达。
大多数情况下,消息会正常到达, 因为Kafka是高可用的,而且生产者会自动尝试重发。
不过这种方式有时候也会丢失一些消息。
同步发送
producer.send(record).get()
如果服务器返回错误,get()方法会抛出异常。如果没有发生错误,我们会的到一个RecordMetadata对象,可以用它获取消息的偏移量。
我们使用send()发送消息,它会返回一个Future对象,调用get()方法进行等待。就可以知道消息是否发送成功。
异步发送
producer.send(record, ? extends org.apache.kafka.clients.producer.Callback)
Callback接口只有一个onCompletion方法。
如果Kafka返回一个错误,onCompletion方法会抛出一个非空异常。
我们调用send()方法,并指定一个回调函数,服务器在返回响应时调用该函数。
KafkaProducer异常
KafkaProducer一般会发生两类错误。
其中一类是可重试错误。这类错误通过重发消息来解决。比如连接错误,无主错误等。KafkaProducer可以被配置成自动测试,如果在多次重试后仍无法解决问题,应用程序会收到一个重试异常。
另一类错误无法通过重试解决,比如消息太大异常。对于这类错误,KafkaProducer不会进行任何重试,直接抛出异常。
生产者配置
org.apache.kafka.clients.producer.ProducerConfig
bootstrap.servers
该属性指定broker的地址清单,地址的格式为host:port。
清单不需要包含所有的broker地址,生产者会从给定的broker里查找到其他broker的信息。
建议至少要提供两个broker的信息,一旦其中一个单机,生产者仍然能够连接到集群上。
该参数为必选参数。
key.serializer
broker希望接收到的消息的键和值都是字节数组。
生产者接口允许使用参数化类型,因此可以把Java对象作为键和值发送给broker。这样的代码具有良好的可读性,不过生产者需要知道如何把这些Java对象转化成字节数组。
key。serializer必须被设置为一个实现了org.apache.kafka.common.serialization.Serializer接口的类,生产者会使用这个类把键对象序列化成字节数组。
Kafka客户端默认提供了ByteArraySerializer,StringSerializer和IntegerSerializer。
该参数必须设置,即使只发送值。
value.serializer
用指定的类将值序列化。
该参数必须设置。
acks
acks参数指定了必须要有多少个分区副本接收到消息,生产者才会认为消息写入是陈宫的。
这个参数对消息丢失的可能性有重要影响。
该参数有如下选项:
acks=0.生产者在写入消息之前不会等待任何来自服务器的响应。所以该值可以以网络能够支持的最大速度发送消息,从而达到很高的吞吐量。
acks=1.只要集群的首领结点接收到消息,生产者就会收到一个来自服务器的成功响应。如果首领结点发生崩溃,一个没有收到消息的结点称为新首领,消息将丢失。
acks=all.只有当所有参与复制的结点全部接收到消息时,生产者才会收到一个来自服务器的成功响应。这种模式是最安全的,但是延迟最高。
buffer.memory
该参数用来设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。
如果应用程序发送消息的速度超过发送到服务器的速度,会导致生产者空间不足。这个时候,send()方法调用要么被阻塞,要么抛出异常,取决于如何设置max.block.ms参数。
compression.type
默认情况下,消息发送时不会被压缩。
使用压缩可以降低网络传输开销和存储开销,这也往往是Kafka发送消息的瓶颈所在。
该参数指定了消息被发送给broker之前使用哪一种压缩算法进行压缩,可以设置为:
snappy -- 占用较少的cpu,提供较好的性能和相当可观的压缩比
gzip -- 占用较多的cpu,提供更高的压缩比
lz4
retries
生产者从服务器收到的错误有可能是临时性的错误(比如分区找不到首领)。
retries参数的值决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试并返回错误。
默认情况下,生产者会在每次重试之间等待100ms,不过可以通过retry.backoff.ms参数来改变这个时间间隔。
一般情况下,因为生产者会自动进行重试,所以就没必要在代码逻辑里处理那些可重试的错误。只需要处理那些不可重试的错误或重试次数超出上限的情况。
batch.size
该参数指定了一个批次可以使用的内存大小,按照字节计算(而不是消息个数)。
当有多个消息需要被发送到同一个分区时,生产者会把它们放到同一个批次里。
当批次被填满,批次里的所有消息会被发送出去。
不过生产者并不一定都会等到批次被填满时才发送(见下一个参数linger.ms)。
所以,就算把批次大小设置的很大,也不会造成延迟,只是会占用更多的内存而已。
但如果设置的太小,因为生产者需要更频繁的发送消息,会增加一些额外的开销。
linger.ms
该参数指定了生产者在发送批次之前等待更多消息加入批次的时间。
KafkaProducer毁在批次填满或linger.ms达到上限时把批次发送出去。
默认情况下, 只要有可用的线程,生产者就会把消息发送出去,就算批次里只有一个消息。
把linger.ms设置成比0大的书,让生产者在发送批次之前等待一会儿,使更多的消息加入到这个批次。虽然这样会增加延迟,但也会提升吞吐量。
https://www.itread01.com/content/1524315728.html
只要滿足linger.ms和batch.size滿了就會激活sender線程來發送消息。
client.id
该参数可以使任意的字符串,服务器会用它来识别消息的来源,还可以用在日志和配额指标里。
max.in.flight.requests.per.connection
该参数指定了生产者在接收到服务器响应之前可以发送多少个消息。
它的值越高,就会占用越多的内存,不过也会提升吞吐量。
把它设为1可以保证消息时按照发送的顺序写入服务器,即使发生了重试。
timeout.ms
该参数制定了broker等待同步副本返回消息确认的时间,也acks的配置相匹配。
如果在指定时间内没有收到同步副本的确认,那么broker就会返回一个错误。
request.timeout.ms
该参数制定了生产者在发送数据时等待服务器返回响应的时间。
如果等待响应超时,那么生产者要么重试发送数据,要么返回一个错误。
medata.fetch.timeout.ms
该参数制定了生产者在获取元数据时等待服务器返回相应的时间。
max.block.ms
该参数制定了在调用send()方法或使用partitionsFor()方法获取元数据时生产者的阻塞时间。
当生产者的发送缓冲区已满,或者没有可用的元数据时,这些方法就会阻塞。在则色时间达到max.block.ms时,生产者会抛出超时异常。
max.request.size
该参数用于控制生产者发送的请求大小。
它可以指定能发送的单个消息的最大值,也可以指单个请求里所有消息总的大小。
另外,broke对可接收的消息最大值也有自己的限制(message.max.bytes),所以,两边的配置最好可以匹配,避免生产者发送的消息被broker拒绝。
receive.buffer.bytes
该参数指定了TCP socker接收数据包的缓冲区大小。
如果被设为-1,就使用操作系统的默认值。
如果生产者或消费者与broker处于不同的数据中心,那么可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的宽带。
send.buffer.bytes
该参数指定了TCP socker发送数据包的缓冲区大小。
如果被设为-1,就使用操作系统的默认值。
如果生产者或消费者与broker处于不同的数据中心,那么可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的宽带。
序列化器
创建一个生产者对象必须制定序列化器。
自定义序列化器
如果发送到Kafka的对象不是简单的字符串或整型,那么可以使用序列化框架来创建消息记录,如Avro,Thrift或Protobuf,或者自定义序列化器。
建议使用通用的序列化框架。
自定义序列化器需要实现org.apache.kafka.common.serialization.Serializer接口。
Avro
Apache Avro是一种与编程语言无关的序列化格式。
Avro数据通过与语言无关的schema来定义。
schema通过JSON来描述,数据被序列化成二进制文件或JSON文件,不过一般会使用二进制文件。
Avro在读写文件时需要用到schema,schema一般会被内嵌在数据文件里。
Avro有一个很有意思的特性,当负责写消息的应用程序使用的新的schema,负责读消息的应用程序可以继续处理消息而无需做任何改动,这个特性使得它特别适合用在像Kafka这样的消息系统上。
不过,有以下两个需要注意的地方:
用于写入数据和读取数据的schema必须是相互兼容的。
反序列化器需要用到用于写入数据的schema,即使它可能与用于读取数据的schema不一样。Avro数据文件里就包含了用于写入数据的schema,不过在Kafka里一般会将schema文件注册到注册表中。
在Kafka里使用Avro
Avro的数据文件里包含了整个schema,不过这样的开销是可接受的。
但是如果在每条Kafka记录里都嵌入schema,会让记录的大小成倍地增加。
通过schema注册表可以将schema保存在注册表中,然后在记录里引用schema的标识符。
分区
ProducerRecord对象包含了目标主题,键和值。
Kafka的消息是一个个键值对,ProducerRecord对象可以只包含目标主题和值,键可以设置为默认的null,不过大多数应用程序会用到键。
键有两个用途:
作为消息的附加信息
决定消息该被写到主题的哪个分区
如果键值为null,并且使用了默认的分区器,那么记录将被随机地发送到主体内各个可用的分区上。
分区器使用轮询算法将消息均匀地分布到各个分区上。
如果键不为空,并且使用了默认的分区器,那么Kafka会对键进行散列,然后根据散列值把消息映射到特定的分区上。
同一个键总是被映射到同一个分区上,所以在进行映射时,我们会使用主题所有的分区,而不仅仅是可用的分区。这也意味着,如果写入数据的分区时不可用的,那么就会发生错误。
只有在不改变主题分区数量的情况下, 键与分区之间的映射才能保持不变。
如果要使用键来映射分区,那么最好在创建主题的时候就把分区规划好,而且永远不要增加新分区。
自定义分区策略
实现org.apache.kafka.clients.producer.Partitioner接口