文章目录

  • 前言
  • 一、消息发送
  • 二、源码分析
  • 1.发送流程
  • 2.步骤解析
  • 2.1 初始化
  • 2.2 序列化消息
  • 2.3 路由分区
  • 2.3 写入内部缓存
  • 2.3 消费缓存
  • 三、参数解析
  • 1.acks
  • 2.batch.size
  • 3.retries
  • 四、消息重复,丢失?
  • 1.provider消息重复
  • 2.provider与broker阶段消息丢失
  • 总结



前言

kafka作为消息中间件,适用于大数据的场景。但是如何保证消息的高效和准确性呢?刨根问底,源码走起…(本文希望读者有简单的kafka运用基础,且文章较长,有条件的可以准备瓜子板凳)


一、消息发送

简单的消息发送,仅需配置服务端的必填参数bootstrap.server(kafka集群中的broker地址),调用其send方法即可:

kafka API封装 如何优雅封装kafka_初始化


但是这里只是进行了简单的消息发送,成不成功完全没管。

这怎么能行呢,如果有人找上门来,说我没收到消息,肯定是你小子没发送成功。我这暴脾气,肯定是要反驳他的呀,怎么办呢,拿出证据来,让他哑口无言,乖乖认错!

当然,kafka的API早就考虑到了这个问题!发送之后只需调用get()方法即可同步获取发送结果

kafka API封装 如何优雅封装kafka_消息发送_02


同步的呀,那不是效率不高,这样每次还要等待同步发送结果。

那有没有效率高一点的呢?回头再去看下send()方法,其实他是异步的,变慢都是因为我们调用了get()方法。所以还是回过头来,看看send()方法的API是否有解决方案。
欧吼,还真被我们找到了:

Future<RecordMetadata> send(ProducerRecord<K, V> producer, Callback callback);

callback是一个回调接口,在消息发送完成后回调我们的自定义实现

kafka API封装 如何优雅封装kafka_初始化_03


同样的也能获取结果,同时发现回调的线程并不是上文同步时的主线程,这样也能证明是异步回调的。

同时回调的时候会传递两个参数:

RecordMetadata 和上文一致的消息发送成功后的元数据。

Exception 消息发送过程中的异常信息。

但是这两个参数并不会同时都有数据,只有发送失败才会有异常信息,同时发送元数据为空。

所以正确的写法应当是:

kafka API封装 如何优雅封装kafka_java_04

二、源码分析

1.发送流程

首先还是来谈谈消息发送时的整个流程是怎么样的,Kafka 并不是简单的把消息通过网络发送到了 broker 中,在 Java 内部还是经过了许多优化和设计。

kafka API封装 如何优雅封装kafka_java_05

从上至下依次是:
1.初始化以及真正发送消息的 kafka-producer-network-thread IO 线程。
2.将消息序列化。
3.得到需要发送的分区。
4.写入内部的一个缓存区中。
5.初始化的 IO 线程不断的消费这个缓存来发送消息。

2.步骤解析

2.1 初始化

public KafkaProducer(Map<String, Object> configs) {
        this((ProducerConfig)(new ProducerConfig(configs)), (Serializer)null, (Serializer)null);
    }

调用该构造方法进行初始化时,不止是简单的将基本参数写入 KafkaProducer。比较麻烦的是初始化 Sender 线程进行缓冲区消费。

2.2 序列化消息

在调用 send() 函数后其实第一步就是序列化,毕竟我们的消息需要通过网络才能发送到 Kafka。

kafka API封装 如何优雅封装kafka_初始化_06


其中的 valueSerializer.serialize(record.topic(), record.value()); 是一个接口,我们需要在初始化时候指定序列化实现类。

2.3 路由分区

接下来就是路由分区,通常我们使用的 Topic 为了实现扩展性以及高性能都会创建多个分区。

如果是一个分区好说,所有消息都往里面写入即可。

但多个分区就不可避免需要知道写入哪个分区。

通常有三种方式。

1.指定分区

可以在构建 ProducerRecord 为每条消息指定分区(这种一般在特殊场景下会使用)。

2.自定义路由策略

如果没有指定分区,则会调用 partitioner.partition 接口执行自定义分区策略。

而我们也只需要自定义一个类实现 org.apache.kafka.clients.producer.Partitioner 接口,同时在创建 KafkaProducer 实例时配置 partitioner.class 参数。

kafka API封装 如何优雅封装kafka_kafka_07


通常需要自定义分区一般是在想尽量的保证消息的顺序性。

或者是写入某些特有的分区,由特别的消费者来进行处理等。

3.默认策略

我们啥都不做的话就会使用该策略,该策略也会使得消息分配比较均匀。

kafka API封装 如何优雅封装kafka_kafka API封装_08


分为以下几步:

获取 Topic 分区数。

将内部维护的一个线程安全计数器 +1。

与分区数取模得到分区编号。

其实就是很典型的轮询算法哈哈哈

2.3 写入内部缓存

在 send() 方法拿到分区后会调用一个 append() 函数:

kafka API封装 如何优雅封装kafka_kafka_09


该函数中会调用一个 getOrCreateDeque() 写入到一个内部缓存中 batches。

kafka API封装 如何优雅封装kafka_kafka_10


kafka API封装 如何优雅封装kafka_初始化_11

2.3 消费缓存

在最开始初始化的 IO 线程其实是一个守护线程,它会一直消费这些数据。

其中有个关键方法completeBatch

kafka API封装 如何优雅封装kafka_kafka_12


调用该方法时候肯定已经是消息发送完毕了,所以会调用 batch.done() 来完成之前我们在 send() 方法中定义的回调接口

kafka API封装 如何优雅封装kafka_消息发送_13

三、参数解析

1.acks

acks 是一个影响消息吞吐量的一个关键参数。

kafka API封装 如何优雅封装kafka_java_14


主要有 [all、-1, 0, 1] 这几个选项,默认为 1。

由于 Kafka 不是采取的主备模式,而是采用类似于 Zookeeper 的主备模式。
当 acks = all/-1 时:
意味着会确保所有的 follower 副本都完成数据的写入才会返回。
这样可以保证消息不会丢失!但同时性能和吞吐量却是最低的。
当 acks = 0 时:
producer 不会等待副本的任何响应,这样最容易丢失消息但同时性能却是最好的!
当 acks = 1 时:
这是一种折中的方案,它会等待副本 Leader 响应,但不会等到 follower 的响应。一旦 Leader 挂掉消息就会丢失。但性能和消息安全性都得到了一定的保证。

2.batch.size

见名思义,这是缓冲区大小设置。对他适当的调大可以提高吞吐量。
但也不能极端,调太大会浪费内存。小了也发挥不了作用,也是一个典型的时间和空间的权衡

3.retries

retries 该参数主要是来做重试使用,当发生一些网络抖动都会造成重试。
这个参数也就是限制重试次数。
但也有一些其他问题。
因为是重发所以消息顺序可能不会一致,这也是上文提到就算是一个分区消息也不会是完全顺序的情况。
由于网络问题,本来消息已经成功写入了但是没有成功响应给 producer,进行重试时就可能会出现消息重复。这种只能是消费者进行幂等处理。

⚠️注意⚠️:1.Producer 在使用过程中消耗了不少资源(线程、内存、网络等)因此需要显式的关闭从而回收这些资源1,默认是close()方法。
2.如果消息量真的非常大,同时又需要尽快的将消息发送到 Kafka。一个 producer 始终会收到缓存大小等影响,此时可以考虑用多个producer,减轻单个压力。

四、消息重复,丢失?

可以分两种情况:

1.provider消息重复

起因:生产发送的消息没有收到正确的broke响应,导致生产者重试。
生产者发出一条消息,broke落盘以后因为网络等种种原因发送端得到一个发送失败的响应或者网络中断,然后生产者收到一个可恢复的Exception重试消息导致消息重复。
解决:
1.要启动kafka的幂等性,设置: enable.idempotence=true ,以及 ack=all 以及 retries > 1 。
2.ack=0,不重试。可能会丢消息,适用于吞吐量指标重要性高于数据丢失,例如:日志收集。

2.provider与broker阶段消息丢失

起因:

  1. ack=0,不重试
    生产者发送消息完,不管结果了,如果发送失败也就丢失了。
  2. ack=1,leader crash
    生产者发送消息完,只等待Leader写入成功就返回了,Leader分区丢失了,此时Follower没来及同
    步,消息丢失。
  3. unclean.leader.election.enable 配置true
    允许选举ISR以外的副本作为leader,会导致数据丢失,默认为false。生产者发送异步消息,只等待Lead写入成功就返回,Leader分区丢失,此时ISR中没有Follower,Leader从OSR中选举,因为OSR中本来落后于Leader造成消息丢失。

方案:1.禁用unclean选举,ack=all
2.min.insync.replicas > 1
3.失败的offset单独记录


总结

文中的kafka生产者借鉴了多个博文,在此感谢,希望看完对你有些许帮助。