4.1 消息发送流程
生产者要发送消息的属性封装到Properties中,将Properties传到KafkaProducer构造器里,创建一个生产者
发送的消息封装成ProducerRecord对象,包含topic、分区、key、value。分区和key可不指定,由kafka自行确定目标分区
KafkaProducer调用KafkaProducer的send()方法发送到zookeeper,
消费者将要订阅的主题封装在Properties对象中,传入KafkaConsumer构造器中,创建一个消费者
KafkaConsumer调用poll()从zookeeper拉取消费消息
发送时将数据序列化,消费时将数据反序列化
producer使用一个线程(用户主线程,也就是用户启动producer的线程)将待发送的消息封装成一个ProducerRecord实例,然后将其序列化后发送给partition,再由partition确定目标分区后一同发送到位于producer程序中的一块内存缓冲区中。而producer的另一个线程(I/O发送线程,也称Sender线程)则负责实时地将缓冲区中提取出准备就绪的消息封装进一个批次(batch),统一发送给对应的broker。
Kafka 的 Producer 发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程,以及一个线程共享变量——RecordAccumulator。
main 线程将消息发送给 RecordAccumulator, Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker
相关参数:
- batch.size: 只有数据积累到 batch.size 之后, sender 才会发送数据。
- linger.ms: 如果数据迟迟未达到 batch.size, sender 等待 linger.time 之后就会发送数据。
4.2 producer hello-world
1)pom依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
// 不带回调函数的发送者
package com.atguigu.kafka;
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
public static void main(String[] args) throws ExecutionException,InterruptedException {
Properties props = new Properties();
//kafka 集群, broker-list // 必须指定 // 如果kafka集群中机器数很多,那么只需指定部分broker即可,不需要列出所有的机器。因为不管指定几台机器,producer都会通过该参数找到并发现集群中所有broker。为该参数指定多态机器只是为了故障转移使用。这样即使某一台broker挂掉了,producer重启后依然可以通过该参数指定的其他broker连入kafka集群
props.put("bootstrap.servers", "hadoop102:9092");
props.put("acks", "all");
//重试次数
props.put("retries", 1);
//批次大小
props.put("batch.size", 16384);
//等待时间
props.put("linger.ms", 1);
//RecordAccumulator 缓冲区大小
props.put("buffer.memory", 33554432);
// 必须指定
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);
for (int i = 0; i < 100; i++) {
producer.send(
new ProducerRecord<String, String>(
"first",
Integer.toString(i),
Integer.toString(i)));
}
producer.close();// 所有的通道打开都需要关闭
}
}
①Properties对象
http://kafka.apache.org/documentation/#producerconfigs
必须指定
bootstrap.servers:指定了ip:port对,用于创建向kafka broker服务器的链接。如k1:9092,k2:9092。如果kafka集群中机器数很多,那么只需指定部分broker即可,不需要列出所有的机器。因为不管指定几台机器,producer都会通过该参数找到并发现集群中所有broker。为该参数指定多态机器只是为了故障转移使用。这样即使某一台broker挂掉了,producer重启后依然可以通过该参数指定的其他broker连入kafka集群
key.serializer:被发送到broker端的任何消息的格式都必须是字节数组,因此消息的各个组件必须首先做序列化,然后才能发送到broker。该参数就是为消息的key做序列化用的。这个参数指定的是实现了org.apache.kafka.common.serialization.StringSerializer接口的类的全限定名称。kafka为大部分的初始类型默认提供了现成的序列化器。用户可以自定义序列化器,只要实现Serializer接口即可。即使没有指定key,key.serializer也必须设置。
value.serializer:与上面类似,不过是用来对消息体部分做序列化,将消息value部分转换成字节数组。这两个参数都得使用全限定类名,不能只写类名。这两个参数可以卸载properties中,也可以卸载下面构造函数的后面。
可以选择参数:
acks:producer给broker生产消息,broker返回“已提交”信息给producer。详见之前的ack 应答机制章节。properties.put("acks","1")注意在java中不写引号会报错
acks=0:不等broker的返回
acks=-1或者all:等待ISR中所有副本都成功落盘后发送回来“已提交”信息
acks=1:只等leader的落盘成功就应答
buffer.memory:producer段缓存消息的缓冲区大小,字节为单位,默认值32MB。由于采用了异步发送消息的设计架构,java版本的producer启动时会首先创建一块内存缓冲区用于保存待发送的消息,然后由另一个专属线程负责从缓冲区读取消息执行真正的发送。这部分内存空间的大小即是由buffer.memory参数指定的。若producer向缓冲区写消息的速度超过了专属I/O线程发送消息的速度,那么必然造成缓冲区的不断增大。此时producer会停止手头的工作等待I/O线程追上来,若一段时间之后I/O线程还是无法追上producer的进度,那么producer就会抛出异常并期望用户介入进行处理。若producer要给很多分区发送消息,那么就需要注意别让这个参数降低了producer整体的吞吐量。properties.put("buffer.memory",33554432)或properties.put(Producer.Config.BUFFER_CONFIG,33554432)
compression.type:producer发送时是否压缩数据。默认none。还有GZIP、Snappy、LZ4。LZ4性能最好。还有Zstandard
reties:broker写入请求时可能有瞬时故障(比如leader选举)导致发送失败,这种失败可自行恢复,可以封装进回调函数中重新发送,但我们并不需要使用回调函数,直接设置该参数即可实现重试。默认为0不重试。
重试可能导致消息重复:比如瞬时的网络抖动使得broker段已成功写入但没有发送响应给producer,因此producer认为失败而重发。为了应对这一风险,kafka要求用户在consumer段必须执行去重操作。0.11.0.0版本开始支持“精准一次”处理语义,从设计上避免了类似的问题。
重试可能造成消息的乱码—当前producer会将多个消息发送请求(默认5个)缓存在内存中没如果由于某种原因发送了消息发送的重试,就可能造成小溪流的乱序。为了避免乱序发送,java版本的producer提供了max.in.flight.requets.per.connection参数。一旦用户设置为1,producer将确保某一时刻只能发送一个请求
两次重试之间会停顿一段时间,防止频繁重试对系统带来冲击。try.backoff.ms设置,默认100ms,推荐用户通过测试来计算平均leader选举时间来设定retries和retry.backoff.ms
properties.put(“reties”,100)或properties.put(ProducerConfig.RETRIES_CONFIG,100)
batchsize:重要。通常一个小的batch中包含的消息数很少,因而一次发生请求能够写入的消息数也很少,所以producer吞吐量会很低。但若batch非常大就会给内存使用带来压力,因为不管是否能够填满,producer都会为该batch分配固定的大小。
默认16KB,一般都增加。
linger.ms:即使batchsize没满,超过该设置时间后也会发送。默认为0表示消息立即发送,无需关心batch是否填满。但这会降低吞吐量,producer将更多的时间花费在了发送上。
max.request.size:能发送的最大消息大小,但包含一些消息头。默认1048576太小了
request.timeout.ms:超过默认的30s后仍没收到返回结果就会发生异常
②KafkaProducer对象
Producer<String, String> producer = new KafkaProducer<>(props);
③ ProducerRecord对象
ProducerRecord(topic, partition, key, value);
ProducerRecord(topic, key, value);
ProducerRecord(topic, value);
<1> 若指定Partition ID,则PR被发送至指定Partition
<2> 若未指定Partition ID,但指定了Key, PR会按照hasy(key)发送至对应Partition
<3> 若既未指定Partition ID也没指定Key,PR会按照round-robin模式发送到每个Partition
<4> 若同时指定了Partition ID和Key, PR只会发送到指定的Partition (Key不起作用,代码逻辑决定)
④发送消息
kafka producer发送消息的主方法是send()方法。通过Java提供的Future同时实现了同步发送和异步发送+回调两种发送方式
异步方式
send返回一个java的Future对象供用户稍后获取发送结果,也就是所谓的回调机制。
for (int i = 0; i < 100; i++) {
producer.send(
new ProducerRecord<String, String>(
"first",
Integer.toString(i),
Integer.toString(i)),
new Callback() {
//回调函数, 该方法会在 Producer 收到 ack 时调用,为异步调用
@Override
public void onCompletion(RecordMetadata metadata,//两个参数不会同时非空,即至少一个为null。若消息发送失败,metadata为null
Exception exception) {//当消息发送成功时e为null。
if (exception == null) { // 发送成功
System.out.println("success->" +
metadata.offset());
} else {
exception.printStackTrace();
}
}
});
}
同步发送
同步发送和异步发送是通过java的Future来区分的,调用Future.get()无限等待结果返回,即实现同步发送的效果
for (int i = 0; i < 100; i++) {
producerRecord<String,String> record = new producerRecord<>("first",Integer.toString(i));
// get方法会一直等待下去直至broker将发送结果返回给producer程序。
// 返回的时候要不返回发送结果,要么抛出异常由producer自行处理。
// 如果成功,get将返回对应的RecordMetadata实例(包含了已发送消息的所有元数据消息),包含了topic、分区、位移
producer.send(record).get();// send的返回结果是Future<RecordMetadata>
}
⑤异常
不管是同步发送还是异步发送,发送都有可能失败,导致返回异常错误。当前kafka的错误类型包含了两类:可重试异常和不可重复异常。
就常见的可重试异常:
LeaderNotAvailableException:分区的leader副本不可用,这通常出现在leader换届选择期间,因此通常是瞬时的异常,重试之后可以自行恢复
NotControllerException:controller当前不可用。这通常表明controller在经历着新一轮的选举,这也是可以通过重试机制自动恢复的
NetworkException:网络瞬时故障导致的异常,可重试
对于可重试异常,如果在producer程序中配置了重试次数,那么只要在规定的重试次数内自行恢复了,便不会出现在onCompletion的exception中。不过若超过了重试次数仍没成功免责仍然会封装进exception中。此时就需要producer程序自行处理这种异常
所有可重试异常都继承自org.apache.kafka.common.errors.RetriableException抽象类。理论上讲所有未继承RetriableException类的其他异常都属于不可重试异常,这类异常都表明了一些严重或kafka无法处理的问题,与producer相关的如下:
RecordToolLargeException:发送的消息尺寸太大,超过了规定的大小上限
SerializationException:序列化失败异常
KafkaException:其他类型的异常
这些不可重试异常一旦被捕获都会被封装进Future的计算结果并返回给producer程序,用户需要自行处理这些异常。由于不可重试异常和可重试异常在producer程序段可能有不同的处理逻辑,因此可以使用下面的代码进行区分:
producer.send(record,new Callback(){
@Override
public void onCompletion(RecordMetaData metadata,Exception exception){
if (exception == null) { // 发送成功
System.out.println("success->" +
metadata.offset());
} else {
if(exception instanceof RetriableException){
//处理可重试异常
}else{// 不可重试异常
exception.printStackTrace();
}
}
}
})
⑥关闭producer
producer程序结束一定要关闭producer。因为producer程序运行时占用了系统资源(比如创建了额外的线程,申请了很多内存以及创建了多个Socket链接等),因此必须要显示地调用KafkaProducer.close()。不管发送成功还是失败,只要producer程序完成了既定的工作,就应该被关闭。
producer.close(参数)
- 无参:处理完发送的请求后再关闭
- 有参:等待timeout完成锁处理的请求后强制关闭
4.4 生产过程细节
4.4.1 写入方式
producer采用推(push)模式将消息发布到broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障kafka吞吐率)。
4.4.2 分区(Partition)
消息发送时都被发送到一个topic,其本质就是一个目录,而topic是由一些Partition Logs(分区日志)组成,其组织结构如下图所示:
4.4.3 数据可靠性保证(确认机制)
1)producer先从zookeeper的 "/brokers/…/state"节点找到该partition的leader(kafka不知道谁是kafka集群的leader,但zk知道谁是kafka集群的leader)
2)producer将消息发送给该leader
3)leader将消息写入本地log
4)followers从leader pull消息,写入本地log后向leader发送ACK
5)leader收到所有ISR中的replication的ACK后,增加HW(high watermark,最后commit 的offset)并向producer发送ACK
为保证 producer 发送的数据,能可靠的发送到指定的 topic, topic 的每个 partition 收到producer 发送的数据后(kafka集群同步基本完成), 都需要向 producer 发送ack(acknowledgement 确认收到) ,如果producer 收到 ack, 就会进行下一轮的发送,否则重新发送数据
1) 副本数据同步策略
方案 | 优点 | 缺点 |
---|---|---|
半数以上完成同步, 就发送 ack | 延迟低 | 选举新的 leader 时, 容忍 n 台 节点的故障,需要 2n+1 个副本 |
全部完成同步,才发送 ack | 选举新的 leader 时,容忍 n 台节点的故障,需要 n+1 个副本 | 延迟高 |
只要ISR集合中欧冠同步完成即可发送ack |
Kafka 选择了第二种方案,原因如下:
- 1.同样为了容忍 n 台节点的故障,第一种方案需要 2n+1 个副本,而第二种方案只需要 n+1个副本,而 Kafka 的每个分区都有大量的数据, 第一种方案会造成大量数据的冗余。
- 2.虽然第二种方案的网络延迟会比较高,但网络延迟对 Kafka 的影响较小
2) ISR
采用第二种方案之后,设想以下情景: leader 收到数据,所有 follower 都开始同步数据,但有一个 follower,因为某种故障,迟迟不能与 leader 进行同步,那 leader 就要一直等下去,直到它完成同步,才能发送 ack。这个问题怎么解决呢?
Leader 维护了一个动态的 in-sync replica set (ISR),意为和 leader 保持同步的 follower 集合,即每个partition动态维护一个replication集合。当 ISR 中的 follower 完成数据的同步之后,follower 就会给 leader 发送 ack。如果 follower长 时 间 未 向 leader 同 步 数 据 , 则 该 follower 将 被 踢 出 ISR , 该 时 间 阈 值 由 replica.lag.time.max.ms 参数设定。
- 对于一个partition,集合中每个replication都同步完后,kafka才会将该消息标记为“已提交”状态,认为该条消息发送成功
- 只要这个集合中至少存在一个replication或者,已提交的信息就不会丢失
- 当一小部分replication开始落后于leader replication的速度速度时,就踢出ISR
- 被踢出去的replication还在同步,只是不算在ISR里。被踢出去的同步追上leader后,又重新计入ISR
bin/kafka-topics.sh --describe --topic first --zookeeper hadoop102:2181
# 输出
Topic:first PartitionCount:1 ReplicationFactor:3 Configs:
Topic: first Partition: 0 Leader: 3 Replicas: 3,4,2 Isr: 3
# 看最后的ISR
老版本中两个条件: leader与follower消息差距条数、距离上次同步的时间
leader和follower发消息差距大于10条就踢出ISR,如果小于10条再加进来。为什么踢出ISR还会又加进来呢?因为ISR只是决定了什么时候返回ACK,而无论在不在ISR里,都仍在继续同步数据。我们不能因为他慢了点就直接不用他备份。
生产者以batch发送数据,比如这个batch12条,如果batch大于大于了设定的10条阻塞限制,那么所有的follower都被踢出ISR。频繁发送batch,就频繁加入ISR,踢出ISR,频繁操作ZK
3) ack 应答机制
对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等 ISR 中的 follower 全部接收成功,(分为只要leader收到、或leader写入磁盘就行、ISR全部写入才能确认,即数目为0、1、all)。(本来ISR就不是kafka集群的全部机器了,ISR居然也能不是全部)
所以 Kafka 为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,在生产者段选择以下的配置参数。
acks 参数配置:
- acks=0: producer 不等待 broker 的 ack,这一操作提供了一个最低的延迟, broker一接收到还没有写入磁盘就已经返回,当 broker 故障时有可能丢失数据;这种情况后面的producer.send的回调也会完成失去作用
- acks=1: producer等待broker的ack,partition的==leader 落盘(写入磁盘)==成功后返回ack(只等待leader写完就发回ack),如果在 follower同步成功之前leader故障,那么将会丢失数据;
- acks=-1(all):producer 等待 broker 的 ack, partition的leader和follower(ISR里的follower) 全部落盘成功后才返回ack。但是如果在follower同步完成后, broker发送ack之前,leader发生故障,那么会造成数据重复。比如ISR中只有一个leader,leader写完了就发送ACK,但是还没同步就挂掉了,此时也会丢失数据。(生产者以为成功了,不会再发送了)
acks=-1时的数据丢失问题:
acks=-1时的数据重复问题:都同步完了,但是还没发ACK时,leader挂掉了,选举一个follower做leader,生产者没有接受到ACK,又重发了一次,造成数据重复。
5 消息序列化
默认的序列化:
在网络中发送数据都是以字节的方式,kafka也不例外。kafka支持用户给broker发送各种类型的消息。它可以是一个字符串、一个整数、一个数组或是其他任意的对象类型。序列化器负责在producer发送前将该消息转换成字节数组;而与之相反,解序列化器则用于将consumer接收到的字节数组转换成相应的对象。
自定义序列化器:org.apache.kafka.common.serialization.Serializer
jackson-mapper-asl包的ObjectMapper可以把对象转换成字节数组
objectMapper.writeValueAsString(data).getBytes("utf-8");
6 拦截器
Producer拦截器是个相当新的功能,他和consumer端interceptor是在kafka 0.10版本被引入的,主要用于实现clients端的定制化控制逻辑
生产者拦截器可以用在消息发送前做一些准备工作,producer也支持指定多个interceptor按序作用域同一条消息从而形成一个拦截器链。实现接口org.apache.kafka.clients.producer.ProducerInterceptor
使用场景:
- 按照某个规则过滤掉不符合要求的消息
- 修改消息的内容
- 统计类需求
public interface ProducerInterceptor<K, V> extends Configurable {
// 获取配置信息和初始化数据时使用
configure(config);
// 该方法被封装进KafkaProducer.send()方法中,即它允许在用户主线程中。producer确保在消息被序列化计算分区前调用该方法。可以操作消息,但最好不要修改topic和分区,否则会影响目标分区的集散
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
// 消息被应答之前或消息发送失败时调用。运行在producer的IO线程中因此不要在该方法中放入很“重"的逻辑,否则会拖慢producer的发送效率
// 可以用e==null时判断消息发送成功计数
public void onAcknowledgement(RecordMetadata metadata, Exception exception);
// 拦截器关闭时调用
public void close();
}
如前所述,interceptor可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全。另外倘若指定了多个interceptor,则producer将按照指定顺序调用它们,并仅仅是捕获每个interceptor可能抛出的异常记录到错误日志中而非在向上传递。这在使用过程中要特别留意。
参考:
1、原文链接:https://blog.csdn.net/hancoder/article/details/107446151
2、尚硅谷官网、黑马等视频
纯粹个人学习使用,如有侵权,请联系删除!