kafka 的producer端开发
主要涉及到以下几个方面
- Producer 概要以及构造一个producer实例进行介绍重要参数
- 消息的分区机制
- 消息的序列化
- producer端 —— broker端的消息丢失配置
- 消息压缩
- 多线程处理
直接进入主题:
Producer 概要以及构造一个producer实例进行介绍重要参数
勤学好问:Producer是用来干啥的?
从上一节kafka的介绍已经知道 kafka集群中是有一个生产者的角色的, 这个 Producer就是担任这个角色的。
目前,producer的首要功能就是向某个 topic 的某个分区(partition)发送一条消息, 所以需要首先确认到底是要向哪个分区写入消息——这就交给 分区器(partitioner)去做了。其中 kafka是提供了默认的分区器的,对于每一条待发送的消息如果已经指定了 key (消息键,这个key 在上一节中介绍过,可以用于指定分区),那么partitioner会根据key的哈希值来选择目标分区; 如果没有指定key(相当于没有指定分区), 那这时候就交给partitioner来通过轮询的方式指定分区。
下面先来看看Java版本的 Producer 工作流程:
大致的流程就是:
Producer 首先使用一个线程(用户主线程,启动producer的线程)将待发送的消息 封装进 ProducerRecord 类实例, 然后将其 交给序列化器 进行序列化之后 然后 发送给 partitioner(分区器), partitioner确定目标分区后会将消息先放入producer程序中的一块内存缓冲区中。之后,producer的另一个工作线程(I/O发送线程,也叫sender线程)则负责实时地从该缓冲区中提取出准备就绪的消息封装进一个批次(batch), 然后统一发送给对应的 broker。
下面来看如何构造一个 produce实例:
- Producer程序实例
package testKafka; // import org.apache.kafka.common.serialization.StringSerializer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; import java.util.Properties; /* 构造一个Producer大概需要5个步骤: */ public class ProducerTest { public static void main(String[] args) throws Exception{ // 第一步: 构造一个 java.util.Properties 实例对象 至少指定下面三个必须项 Properties pros = new Properties(); // 创建配置 实例 用于记录 各项producer端的配置 pros.put("bootstrap.servers", "localhost:9092"); pros.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); pros.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 上面三项是必须指定的 pros.put("acks", "-1"); // 表示 等leader broker 和 ISR 集合 都写入本地日志后发响应给producer pros.put("retries", 3); // 重试次数 pros.put("batch.size", 323840); // pros.put("linger.ms", 10); pros.put("buffer.memory", 33554432); // 缓冲区大小 pros.put("max.block.ms", 3000); // 第二步: 使用 上面构造的 properties 实例构造 KafkaProducer 对象 Producer<String, String> producer = new KafkaProducer<>(pros); for (int i = 0; i < 100; i++) { // 第三步: 构造待发送消息对象 ProducerRecord ,指定 topic、分区以及对应的 key 和 value. 其中 分区和key不是必须的。看下面的三个构造方法, 对号入座: /* public ProducerRecord(String topic, Integer partition, K key, V value) public ProducerRecord(String topic, K key, V value) public ProducerRecord(String topic, V value) */ // 第四步: 就是调用KafkaProducer的实例对象的send方法 发送消息 producer.send(new ProducerRecord<>("my-topic", Integer.toString(1), Integer.toString(1))); // 第五步:关闭 KafkaProducer producer.close(); } } }
对于上面的实例进行一些具体的解释【主要是相关参数】:
- 指定向 kafka broker服务器的连接
pros.put("bootstrap.servers", "localhost:9092"); 这里 bootstrap.servers 的参数你写一个,写几个都可以,因为不管指定几台,Producer 都会通过该参数找到并发现集群中所有的 broker, 为其指定多台只是为了 : 故障转移, 因为指定多台的话,即使其中一台挂了, 也可以通过其他指定的机器进入 kafka 集群。就是这么神奇。
- key和value的序列化
pros.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); pros.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 为什么要对 key value进行序列化呢? 这是因为被发送到 broker 端的任何消息的格式都必须是字节数组,再加上 我们是要走网络传输的,所以需要进行序列化才能发送到 broker 。 此外对于key value的序列化指定也可以是下面这样的: Serializer<String> keySerizer = new StringSerializer(); Serializer<String> valueSerizer = new StringSerializer(); Producer<String, String> producer2 = new KafkaProducer<>(pros, keySerizer, valueSerizer);
- 构造 ProducerRecord 对象
ProducerRecord 对象 用于指定 待发送的消息 要发给哪个 topic 的哪个分区的。 下面是 ProducerRecord 的三个构造方法: public ProducerRecord(String topic, Integer partition, K key, V value) public ProducerRecord(String topic, K key, V value) public ProducerRecord(String topic, V value)
- 发送消息 【关键的一步】
其实这里发送消息分为 同步 和 异步 两种发送方式。producer 通过 Java 提供的Future 同时实现了 同步发送 和异步发送+回调 两种发送方式。 异步发送: ProducerRecord record = new ProducerRecord<>("my-topic", Integer.toString(1), Integer.toString(1)); producer.send(record, new Callback() { @Override public void onCompletion(RecordMetadata recordMetadata, Exception e) { if(e == null){ // 表示消息发送成功 }else{ // 消息发送失败 执行错误处理逻辑 } } }); 同步发送: producer.send(record).get(); // 这里 当结果从 broker 处返回时 get 方法要么返回发送结果 要么抛出异常交由 producer 自行处理。如果没有错误 get 方法会返回对应的 RecordMetadata 实例,包括 消息发送的 topic、分区以及消息在对应分区的位移消息。 这里需要注意的是: 不管你是同步发送 还是 异步发送, 发送都有可能会失败。 金进而导致返回异常错误。目前 Kafka的错误类型包含了两类: 可重试错误 和 不可重试错误 。 常见可重试错误: - LeaderNotAvailableException: 分区的 leader 副本不可用,常发生在换届选举期间,瞬时错误,重试后可自行恢复。 - NotControllerException: controller 当前不可用(controller 后面再介绍),通常表明 controller 在经历新一轮的换届选举,重试后可恢复。 - NetworkException: 网络瞬时故障导致的异常,可重试。 上面这些可重试异常 都是继承自 org.apache.kafka.common.errors.RetriableException 抽象类。 不可重试异常: - RecordTooLargeException:发送消息的尺寸过大, 这个重试也没办法的,需要手动解决下。 - SerializationException: 序列化异常,这也没办法重试。 - KafkaException: 其他类型的异常。 所有的这些不可重试异常一旦被捕获都会被封装进 Future 的计算结果并返回 producer 程序,用户需要自行处理这些异常。
- 关闭 producer
关闭 Producer 也是值得说道说道的,他的关闭主要有两种方式: 带参数 和 不带参数。 无参数关闭:producer.close(); producer 被允许先处理完之前发送的消息再进行关闭,即优雅的关闭。 有参数关闭:producer.close(1000); producer 在指定参数事件仍未关闭的话会被强制关闭。实际生产环境中 慎用 此法。
接下来来看看 producer的几个重要参数
acks
这个参数用于控制 producer 生产消息的持久性。 具体点说就是:leader broker 何时发送写入结果返还给 producer,他会影响到消息的持久性 甚至是 producer 端的吞吐量:producer 端越快接收到 leader broker响应,它就能越快地发送下一条消息,即吞吐量也就越大。 producer 端的 acks 参数就是来控制这件事情的。acks指定了 在给 producer 发送响应前, leader broker 必须确保已成功写入该消息的副本数。acks 有三个取值:0、 1 和 all/-1。
- acks = 0 : 设置成 0 表示 producer 完全不理睬 leader broker 端的处理结果。producer 端会立即发送下一条消息,根本不等 leader broker 端的响应。 这样用户就无法感知 发送过程中失败,不过好处就是 提升了 吞吐量。
- acks = all 或者 -1 : 表示发送消息时, leader broker 不仅会将消息写入本地日志, 同时还会等待 ISR(同步副本集合) 中所有其他副本都成功写入各自的本地日志后,才发送响应结果给producer。 只要 ISR 中 至少一个副本是存活的,就能保证这条消息肯定不会丢失,因而可以达到最高的消息持久性,不过通常 吞吐量 是最低的。
- acks = 1 : 是 0 和 all 折中的方案。它是等待 leader broker 将消息写入本地日志后,发响应结果给 producer 端,无须等待 ISR 副本集合响应。 因此即可以达到适当的消息持久性,又可以保证producer 端的吞吐量。
buffer.memory
这个参数指定了 producer 端用于 缓存消息的缓冲区大小, 单位是 字节。 默认是 32 MB。
这里需要注意的是:Java版本的 producer 启动时会首先创建一块 内存缓冲区 用于保存待发送的消息, 然后由另一个专属线程 负责从缓冲区中读取消息执行真正的发送。:: :如果 producer 向缓冲区写消息的速度 大于 专属 I/O 线程发送消息的速度, 那么必然会造成 缓冲区 的空间不断增大。 此时, producer 会停止手头的工作 等待 I/O 线程追上来, 若一段时间之后 I/O 线程 还是无法追上 producer 的进度。 那么 producer 就会抛出异常并期望用户介入来解决问题。
如果 producer 要过很多分区发消息 就需要小心地设置这个参数,可以设置大些。
compression.type
这个参数设置 producer 端是否压缩消息, 默认是 none, 即不压缩消息。支持的压缩方式:【GZIP, Snappy , LZ4】
producer 引入压缩的目的是 可以显著地降低网络 I/O传输开销 从而提升吞吐量,但同时也会增加 producer 端机器的 cpu 开销。另外 如果 broker 端的压缩参数设置的和 producer端 不一样的话, broker 端 会额外使用 CPU 资源进行 解压 和 重压操作。
reries 【默认 0】
Kafka broker 在处理写入请求时可能会因为 瞬时的故障导致消息发送失败,所以通常会进行重试。那么这个参数就是表示 进行重试的次数, 默认是 0 , 表示不重试。
需要注意是: 重试也带来了一些问题:
重试可能造成消息的重复发送。
Kafka从 0.11.0版本开始已经指出 ”精确一次“ 处理语义,从设计上避免了此问题。
重试可能造成消息乱序。
Java 版本的 producer 提供了 max.in.flight.requests.per.connection 参数,将它设置为 1,这样 producer 会确保同一时刻 只能发送一个请求。
NOTE : producer 两次重试之间会停顿一段时间, 防止频繁重试对系统带来的冲击, 时间可以由参数 retry.backoff.ms 指定, 默认 100 ms。那应该设置多少呢? 由于leader 换届选举 是最常见的瞬时错误故障, 所以可以通过计算 平均 leader 选举时间 并根据该时间来设定 retries 和 retry.backoff.ms的值。
batch.size !!!【默认 16 KB】
batch.size 是非常重要的参数!!!
它对调优 producer 端的吞吐量和延时性有着重要作用。前面提到过 Producer 会将发往同一分区的多条消息封装进一个 batch。当batch 满了, Producer 就会发送 batch 中的所有消息。不过, batch 并不总是等到 batch 满了才发送,可以设置等待时间,到了等待时间即使没满,也得给它送走。【就是这么狠】
batch.size 的参数默认是 16 KB 。比较保守的数字, 其实可以设置的大些,实际使用过程中合理设置该值,可以发现 Producer 端的吞吐量得到明显的提升。
linger.ms !!!【默认 0】
这个参数就是用来设置 batch 等待时间。 默认是 0, 即来了消息就理解被发送。这样会拉低producer 端吞吐量,毕竟 Producer 端发送的每次请求中包含的消息数越多, Producer 就能将发送请求的开销分摊到更多的消息上, 从而提升吞吐量。
max.request.size ! ! ! 【默认 10 MB】
这个参数用于控制 producer 发送请求的大小。 实际上该参数它控制 的 是 producer 端能够发送的最大消息大小。(消息大小)默认值是 10 MB。NOTE: 如果Producer 发送的尺寸很大的消息,那么这个参数是需要设置的。
request.timeout.ms 【默认 30 s】
当 producer 发送消息给· broker 后, broker 需要在规定的时间范围内将处理结果返还给 producer。 这段时间 就是由这个参数进行控制的。 默认是 30 秒。
消息分区机制
这是什么? 还记得前面的 Producer 的流程图吧。 消息被封装好后, 要发送给 Partitioner 分区器。
默认分区器:org.apache.kafka.clients.producer.internals.DefaultPartitionerProducer 端提供了分区策略 和 分区器 供用户使用。
默认的 Partitioner 在有key的情况下 会尽力确保具有相同 key 的所有消息都会被发送到 相同的分区上;
没有指定消息 key 的话,即key为null的情况,partitioner 会选择轮询的方式来保证消息在 topic 的分区上均匀分配。
当然,用户也可以使用自定义分区机制。
消息的序列化
在网络中发送数据都是以字节的方式。
序列化器(serializer) 负责在 Producer 发送前将消息转换成字节数组 ; 与之相反的是, **解序列化器(deserializer)**则用于将 consumer 接收到的字节数组转换成相应的对象。
kafka提供常用的十几种序列化器, 主要是包括一些常见的类型:
- ByteArraySerializer
- ByteBufferSerializer
- BytesSerializer
- DoubleSerializer
- IntegerSerializer
- LongSerializer
- StringSerializer
另外,如果要序列化的是复杂类型的数据(如 Avro之类的),就需要自定义序列化。
自定义序列化需要实现 : org.apache.kafka.common.serialization.Serializer 接口。
【这里带下 producer 拦截器】
Producer 拦截器
它主要用于 实现 clients 端的定制化控制逻辑。
拦截器(interceptor) 使得用户 在消息发送前 以及 producer 回调逻辑前有机会对消息做一些定制化需求, 比如修改消息。
无消息丢失配置
Producer 端配置:
【消息丢失问题】KafkaProducer.send 方法仅仅把消息放入缓冲区中,由一个专属 I/O 线程 负责从缓冲区中提取消息封装进 消息 batch 中,然后发送出去。就是在这个过程, 会存在数据丢失的窗口:若 I/O 线程发送之前 producer 崩溃,那么缓冲区的消息就全部丢了。这就是消息丢失问题。
【消息乱序问题】比如说客户端的发送语句:
producer.send(record1); producer.send(record2);
由于某些原因比如网络瞬时故障导致 record1 没有发送成功, 之后重试成功后, record1 在日志中的位置反而位于 record2 之后, 这就造成了消息的乱序。
那该如何解决呢?
首先对于消息丢失问题: 比较简单的就是 既然异步发送可能会丢失数据,改成同步发送似乎还不错。
producer.send(record1).get(); // 阻塞等待
当然实际场景中是不建议这样做的,影响吞吐量还占用资源。
可以通过对producer进行配置,使得异步发送又能保证 消息不丢失。下面挑几个主要的说说:
acks = all/-1 【默认的是 1 即只等leader broker返回响应就行了】
即 必须等待所有的 follower 都响应了发送消息 才能认为提交成功。
retries = Integer.MAX_VALUE
这里想表达的是 producer 开启无限重试, 虽然设置成MAX_VALUE有些极端,但是不用担心,因为Producer 只会重试那些可重试(可恢复)的异常。
max.in.flight.requests.per.connection = 1 【解决乱序】
该参数设置为 1 主要是为防止 topic 同分区下的消息乱序问题。它的效果就是 限制了 producer 在单个broker 连接上能够发送的未响应请求数量。 也就是说, 这里设置成 1, 那么producer 在某个broker 发送响应之前将无法再给该broker 发送请求。
这样的话, 之前的:
producer.send(record1); producer.send(record2);
record1没有响应前, record2是不会发送的。
可以使用带回调机制的 send 【使用callback】
producer.send(record, new Callback() { @Override public void onCompletion(RecordMetadata recordMetadata, Exception e) { if(e == null){ // 表示消息发送成功 }else{ // 消息发送失败 执行错误处理逻辑 } } });
可以进行判断,如果producer端发送失败可以获取到处理结果,然后进行相应的处理,比如 关闭producer——close(0)。这也是为了解决消息乱序问题。
broker 端配置
unclean.leader.election.enable = false
即不允许非 ISR 中的副本被选举为 leader, 从而避免 broker 端因日志水位截断而造成消息丢失。
replication.factor >= 3
业界通用的三备份原则,主要是表达了 一定要使用数据备份解决数据丢失问题。
min.insync.replicas > 1
用于控制某条消息至少被写入到 ISR 中的 多少个副本才算成功,设置成大于 1, 是为了producer端发送语义的持久性。切记:只有 producer 端 acks 设置成 all 或 -1 时,这个参数才有意义。实际使用中,不要用默认值。
要确保 replication.factor > min.insync.replicas
如果两者相等, 那么只要有一个分区挂掉了, 分区就无法正常工作。这是为什么? —— 如果 设置 replication.factor = min.insync.replicas = 2, 即 备份了两个副本, 至少要写入 两个ISR 中的副本才算成功。 那如果有一个分区挂掉了, 那么 有一个副本就没了, 无法满足 至少要写入两个副本 才算成功的要求了, 就无法正常工作了。
推荐的配置是: replication.factor = min.insync.replicas + 1
消息压缩
当前Kafka支持的压缩算法: GZIP、Snappy 和 LZ4 。
使用消息压缩的好处就是可以 显著降低 磁盘占用 或者 带宽占用, 从而有效提升了 I/O 密集型应用的性能。有利也有弊,缺点就是: 引入压缩的同时,也会消耗 CPU 资源, 所以压缩是 I/O性能 和 cpu 资源的平衡。
多线程的处理
在实际的生产环境中单线程是无法满足所需的吞吐量目标的。 所以就有了一下两个基本用法:
先来看看 多线程 单 KafkaProducer 实例
顾名思义:构造一个 KafkaProducer 实例, 然后多个线程共享使用。由于 KafkaProducer 是线程安全的,所以这种使用方式也是线程安全的。
为什么这样说?
可以KafkaProducer 采用单例模式, 这样多个线程可以共享一个实例。 保证线程安全。
再来看看 单线程 多 KafkaProducer 实例
在每个producer 主线程中都构造一个 KafkaProducer 实例,并且保证此实例 在该线程中封闭。【相当于 使用 ThreadLocal 人手一支笔,互不干涉】。
说明 | 优势 | 劣势 | |
---|---|---|---|
单 KafkaProducer 实例 | 所有线程共享一个 KafkaProducer 实例 | 实现简单,性能好 | 1. 所有线程共享一个内存缓冲区,可能需要较多的内存; 2. 一旦 producer 某个线程崩溃导致 KafkaProducer 实例被 “破坏” , 则所有用户线程都为无法工作。 |
多 KafkaProducer 实例 | 每个线程维护自己专属的 KafkaProducer 实例 | 1. 每个用户线程拥有专属的 KafkaProducer 实例、缓冲区空间及一组对应的配置参数,可以进行细粒度的调优;2. 单个 KafkaProducer 崩溃不会影响其他 producer 线程工作 | 需要较大的内存分配开销 |
NOTE: 对于分区数 不多的 Kafka集群而言, 推荐使用 第一种方法 即 共享一个 KafkaProducer 实例。
对于超多分区的集群而言,用第二种方法 可控性更高,也方便后续 producer 管理。
以上内容 参考 《kafka 实战》