深入浅出Kafka(二)之生产者
1.Kafka的生产者
Producer:消息生产者,就是向 Kafka broker 发消息的客户端
1.1Kafka生产者概述
1.一个消息记录是一个 ProducerRecord 对象,对象包含了四个属性:Topic,partition,key,value;topic 和 value 是必须的,key 和 partition 是可选的
2.构建好一个消息对象后,就要准备发送了,在发送的时候,生产者需要将 key 和 value 序列化成 byte 数组,发送会经过分区器,如果指定了 key,那么相同 key 的消息会发往同一个分区,如果实现了自定义分区器,那么就会走自定义分区器进行分区路由,否则就是根据 kafka 客户端 api 的 hash 算法将消息发送到计算出来的分区;
3.发送的时候并不是来一个消息就发送一个消息,这样的话吞吐量比较低,并且频繁的进行网络请求。消息是按照批次来发送的或者等待时间来发的的.
1.2生产者消息发送流程
在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator,
参数说明:
batch.size:只有数据积累到batch.size之后,sender才会发送数据。默认16k
linger.ms:如果数据迟迟未达到batch.size,sender等待linger.ms设置的时间到了之后就会发送数据。单位ms,默认值是0ms,表示没有延迟
buffer.memoryRecordAccumulator: 缓冲区总大小,默认 32m。
compression.type:生产者发送的所有数据的压缩方式。默认是 none,也就是不压缩。支持压缩类型:none、gzip、snappy、lz4 和 zstd。
enable.idempotence:是否开启幂等性,默认 true,开启幂等性。
retries:当消息发送出现错误的时候,系统会重发消息。retries表示重试次数。默认是 int 最大值,2147483647。
应答ACKs:
0:生产者发送过来的数据,不需要等数据落盘应答。
1:生产者发送过来的数据,Leader收到数据后应答。
1.3producer发布消息机制剖析
1.3.1写入方式
producer 采用 push 模式将消息发布到 broker,每条消息都被 append 到 patition 中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 kafka 吞吐率)
1.3.2消息路由
1.3.3写入流程
1. producer 先从 zookeeper 的 "/brokers/.../state" 节点找到该 partition 的 leader
1.4异步发送 API
<dependencies> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>3.0.0</version> </dependency> </dependencies>
import com.alibaba.fastjson.JSON; import org.apache.kafka.clients.producer.*; import org.apache.kafka.common.serialization.StringSerializer; import java.util.Properties; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class MsgProducer { private final static String TOPIC_NAME = "my-replicated-topic"; public static void main(String[] args) throws InterruptedException, ExecutionException { Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.65.60:9092,192.168.65.60:9093,192.168.65.60:9094"); /* 发出消息持久化机制参数 */ /*props.put(ProducerConfig.ACKS_CONFIG, "1"); *//* 发送失败会重试发送失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,但是也可能造成消息重复发送,比如网络抖动,所以需要在 接收者那边做好消息接收的幂等性处理 //注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试 props.put(ProducerConfig.RETRIES_CONFIG, 3); //重试间隔设置 props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300); //设置发送消息的本地缓冲区, props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); *//* kafka本地线程会从缓冲区取数据,批量发送到broker, 设置批量发送消息的大小,默认值是16384,即16kb,就是说一个batch满了16kb就发送出去 *//* props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); *//* 默认值是0,意思就是消息必须立即被发送,但这样会影响性能 一般设置10毫秒左右,就是说这个消息发送完后会进入本地的一个batch,如果10毫秒内,这个batch满了16kb就会随batch一起被发送出去 如果10毫秒内,batch没满,那么也必须把消息发送出去,不能让消息的发送延迟时间太长 *//* props.put(ProducerConfig.LINGER_MS_CONFIG, 10);*/ //把发送的key从字符串序列化为字节数组 props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); //把发送消息value从字符串序列化为字节数组 props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); Producer<String, String> producer = new KafkaProducer<String, String>(props); int msgNum = 5; final CountDownLatch countDownLatch = new CountDownLatch(msgNum); for (int i = 1; i <= msgNum; i++) { Order order = new Order(i, 100 + i, 1, 1000.00); //指定发送分区 /*ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME , 0, order.getOrderId().toString(), JSON.toJSONString(order));*/ //未指定发送分区,具体发送的分区计算公式:hash(key)%partitionNum ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME , order.getOrderId().toString(), JSON.toJSONString(order)); //等待消息发送成功的同步阻塞方法 /*RecordMetadata metadata = producer.send(producerRecord).get(); System.out.println("同步方式发送消息结果:" + "topic-" + metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset());*/ //异步回调方式发送消息 , producer.send(producerRecord, new Callback() { // 回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是元数据信息(RecordMetadata)和异常信息(Exception), // 如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败 public void onCompletion(RecordMetadata metadata, Exception exception) { if (exception != null) { System.err.println("发送消息失败:" + exception.getStackTrace()); } if (metadata != null) { System.out.println("异步方式发送消息结果:" + "topic-" + metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset()); } countDownLatch.countDown(); } }); //送积分 TODO countDownLatch.await(5, TimeUnit.SECONDS); producer.close(); } }
1.5生产者分区
15.1 Kafka 分区好处
1.便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。
1.5.2Kafka 分区说明
1.为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列
2.一个 topic 的每个分区都有若干个副本,一个 Leader 和若干个Follower;Leader 副本才能向外提供服务, Follower副本只有Leader副本挂了,通过某些规则进行选举之后,某个Follower变成了Leader之后才能才能向外提供服务
1)查看下topic的情况
bin/kafka‐topics.sh ‐‐describe ‐‐zookeeper 192.168.65.60:2181 ‐‐topic test1
2)第一行是所有分区的概要信息,之后的每一行表示每一个partition的信息
3)Leader节点负责给定partition的所有读写请求。
3.每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 Leader
4.每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和Leader 数据的同步。Leader 发生故障时,某个 Follower 会成为新的 Leader
5.一个分区就是一个提交日志,消息以追加的方式写入分区,然 后以先入先出的顺序读取,由于一个主题一般包含几个分区,因此无法在整个主题范围内保证消息的顺序,
但可以保证消息在单个分区内的顺序
6.Partition是一个有序的message序列,这些message按顺序添加到一个叫做commit log的文件中。每个partition中的消息都有一个唯一的编号,称之为offset,用来唯一标示某个分区中的message。
1.5.3生产者发送消息的分区策略
1.5.3.1指定分区
1. 指明partition的情况下,直接将指明的值作为partition值;例如partition=0,所有数据写入分区0
2.对应的构造方法:
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers){} public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) {} public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers) {} public ProducerRecord(String topic, Integer partition, K key, V value) {}
1.5.3.2没有指定分区
1.没有指明partition值但有key的情况下,将key的hash值与topic的partition数进行取余得到partition值;
3.对应的构造方法:
public ProducerRecord(String topic, K key, V value){}
1.5.3.2既没有指定分区,也没有指定k
1.既没有partition值又没有key值的情况下,Kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,
待该分区的batch已满或者或者linger.ms设置的时间到,Kafka再随机一个分区进行使用(和上一次的分区不同)
2.例如:第一次随机选择0号分区,等0号分区当前批次满了(默认16k)或者linger.ms设置的时间到,Kafka再随机一个分区进行使用(如果还是0会继续随机)
3.对应构造方法
public ProducerRecord(String topic, V value)
1.5.3.3自定义分区器
1.需求:如果研发人员可以根据企业需求,自己重新实现分区器
// 添加自定义分区器 properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.kafka.producer.MyPartitioner");
4.自定义分区器代码如下:
/** * 1. 实现接口 Partitioner * 2. 实现 3 个方法:partition,close,configure * 3. 编写 partition 方法,返回分区号 */ public class CustomPartitioner implements Partitioner { /** * 返回信息对应的分区 * @param topic 主题 * @param key 消息的 key * @param keyBytes 消息的 key 序列化后的字节数组 * @param value 消息的 value * @param valueBytes 消息的 value 序列化后的字节数组 * @param cluster 集群元数据可以查看分区信息 * @return */ @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { // 获取消息 String msgValue = value.toString(); // 创建 partition int partition; // 判断消息是否包含 test if (msgValue.contains("test")){ partition = 0; }else { partition = 1; } // 返回分区号 return partition; } // 关闭资源 @Override public void close() { } // 配置方法 @Override public void configure(Map<String, ?> configs) { } }
1.6生产经验之生产者如何提高吞吐量
1.通过修改如下的参数配置就可以提升生产者的吞吐量
1.6.1buffer.memory
1.设置发送消息的缓冲区,默认值是33554432,就是32MB
2.如果发送消息出去的速度小于写入消息进去的速度,就会导致缓冲区写满,此时生产消息就会阻塞住,所以说这里就应该多做一些压测,尽可能保证说这块缓冲区不会被写满导致生产行为被阻塞住
1.6.2compression.type
默认是none,不压缩,但是也可以使用snappy压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大producer端的cpu开销
1.6.3batch.size
2.如果batch太大,会导致一条消息需要等待很久才能被发送出去,而且会让内存缓冲区有很大压力,过多数据缓冲在内存里
3.其默认值是:16384,就是16kb,也就是一个batch满了16kb就发送出去,一般在实际生产环境,这个batch的值可以增大一些来提升吞吐量
1.6.4linger.ms
1.默认是0,意思就是消息必须立即被发送,但是这是不对的。
2.一般设置一个100毫秒之类的,这样的话就是说,消息会被装进batch,如果100毫秒内,这个batch装满了16kb(默认),自然就会发送出去。
3.但是如果100毫秒内,batch没满,那么也必须把消息发送出去了,不能让消息的发送延迟时间太长,也避免给内存造成过大的一个压力
4.linger.ms设置太大,消息的延迟时间就会太长,设置太小会导致频繁网络请求,吞吐量下降
1.7 生产经验之数据可靠性
数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
1.7.1ACK应答级别
kafka的ACK应答级别有三种,分别是 0,1和-1(all)
1.0:生产者发送过来的数据,不需要等数据落盘应答 , 数据可靠性分析:丢数
(1)表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢消息
(2)大数据统计报表场景,对性能要求很高,对数据丢失不敏感的情况可以用这种
2.1:生产者发送过来的数据,Leader收到数据后应答。数据可靠性分析:丢数
ps:至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一条消息。
这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。
3.-1(all) :生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答(节点个数可配置)
1.这意味着leader需要等待所有备份(min.insync.replicas配置的备份个数)都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据。
这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。当然如果min.insync.replicas配置的是1则也可能丢消息,跟acks=1情况类似
2. min.insync.replicas参数说明:
1.极端情况1:默认min.insync.replicas=1,极端情况下如果ISR中只有leader一个副本时满足min.insync.replicas=1这个条件,此时producer发送的数据只要leader同步成功就会返回响应,如果此时leader所在的broker crash了,就必定会丢失数据!这种情况不就和acks=1一样了!所以我们需要适当的加大min.insync.replicas的值
2.极端情况2:min.insync.replicas=3(等于副本数),这种情况下要一直保证ISR中有所有的副本,且producer发送数据要保证所有副本写入成功才能接收到响应!一旦有任何一个broker crash了,ISR里面最大就是2了,不满足min.insync.replicas=3,就不可能发送数据成功了!
3.根据这两个极端的情况可以看出min.insync.replicas的取值,是kafka系统可用性和数据可靠性的平衡!
4.减小 min.insync.replicas 的值,一定程度上增大了系统的可用性,允许kafka出现更多的副本broker crash并且服务正常运行;但是降低了数据可靠性,可能会丢数据(极端情况1)。
5.增大 min.insync.replicas 的值,一定程度上增大了数据的可靠性,允许一些broker crash掉,且不会丢失数据(只要再次选举的leader是从ISR中选举的就行);但是降低了系统的可用性,会允许更少的broker crash(极端情况2)
问题:Leader收到数据,所有Follower都开始同步数据,但有一个Follower,因为某种故障,迟迟不能与Leader进行同步,那这个问题怎么解决呢?
(1)Kafak解决方案:
1.Leader维护了一个动态的in-sync replica set(ISR),意为和Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2),如果Follower长时间未向
Leader发送通信请求或同步数据,则该Follower将被踢出ISR。
(2)数据可靠性分析
1.如果分区副本设置为1个,或 者ISR里应答的最小副本数量( min.insync.replicas 默认为1)设置为1,和ack=1的效果是一样的,仍然有丢数的风险(leader:0,isr:0)
2.总结得出:数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
(3)数据重复性性分析:
1.接收了两份Hello数据,导致数据重复 具体如何解决数据重复? 后面会讲
2.问题描述:
4.可靠性总结
1.acks=0,生产者发送过来数据就不管了,可靠性差,效率高;大数据统计报表场景,对性能要求很高,对数据丢失不敏感的情况可以用这种
1.8生产经验——数据去重
1.8.1 数据传递语义
1.至少一次(At Least Once)= ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2,可以保证数据不丢失,但是不能保证数据不重复
1.8.2幂等性
1.幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复。
1.8.3生产者事务
1.Kafka的事务主要是保障一次发送多条消息的事务一致性(要么同时成功要么同时失败)
2.说明:开启事务,必须开启幂等性。
3.Producer 在使用事务功能前,必须先自定义一个唯一的 transactional.id.有了 transactional.id,即使客户端挂掉了,它重启后也能继续处理未完成的事务
4.一般在kafka的流式计算场景用得多一点,比如,kafka需要对一个topic里的消息做不同的流式计算处理,处理完分别发到不同的topic里,这些topic分别被不同的下游系统消费(比如hbase,redis,es等),这种我们肯定希望系统发送到多个topic的数据保持事务一致性。Kafka要实现类似Rocketmq的分布式事务需要额外开发功能
2)Kafka 的事务一共有如下 5 个 API // 1 初始化事务 void initTransactions(); // 2 开启事务 void beginTransaction() throws ProducerFencedException; // 3 在事务内提交已经消费的偏移量(主要用于消费者) void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,String consumerGroupId) throws ProducerFencedException; // 4 提交事务 void commitTransaction() throws ProducerFencedException; // 5 放弃事务(类似于回滚事务的操作) void abortTransaction() throws ProducerFencedException;
3)单个 Producer,使用事务保证消息的仅一次发送 package com.test.kafka.producer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; import java.util.Properties; public class CustomProducerTransactions { public static void main(String[] args) throws InterruptedException { // 1. 创建 kafka 生产者的配置对象 Properties properties = new Properties(); // 2. 给 kafka 配置对象添加配置信息 properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "ip:9092"); // key,value 序列化 properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 设置事务 id(必须),事务 id 任意起名 properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction_id_0"); // 3. 创建 kafka 生产者对象 KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties); // 初始化事务 kafkaProducer.initTransactions(); // 开启事务 kafkaProducer.beginTransaction(); try { // 4. 调用 send 方法,发送消息 for (int i = 0; i < 5; i++) { // 发送消息 kafkaProducer.send(new ProducerRecord<>("first", "atguigu " + i)); } // int i = 1 / 0; // 提交事务 kafkaProducer.commitTransaction(); } catch (Exception e) { // 终止事务 kafkaProducer.abortTransaction(); } finally { // 5. 关闭资源 kafkaProducer.close(); } } }
3.9生产经验之数据顺序
3.9.1数据有序
1.单分区内,有序多分区,分区与分区间无序;
2.kafka在1.x版本之前保证数据单分区有序,条件如下: