kafka系列(四)—— Producer API

本文详细介绍了Kafka生产者发送消息的流程,包括配置参数、发送方式、序列化与反序列化,以及异常处理。同时,讲解了消费者如何订阅主题并拉取消息。重点讨论了Kafka的异步发送机制、数据可靠性保证(acks参数)、分区策略和序列化。此外,还提到了生产者拦截器和自定义序列化器的使用。
摘要由CSDN通过智能技术生成

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(分区日志)组成,其组织结构如下图所示:

img

 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、尚硅谷官网、黑马等视频

纯粹个人学习使用,如有侵权,请联系删除!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值