跟着锋哥学Java

深入浅出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,

      Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka Broker
    

    参数说明:

   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(all):生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答。-1和all等价。

 1.3producer发布消息机制剖析

   1.3.1写入方式

    producer 采用 push 模式将消息发布到 broker,每条消息都被 append 到 patition 中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 kafka 吞吐率)

          1.3.2消息路由

    1.producer 发送消息到 broker 时,会根据分区算法选择将其存储到哪一个 partition。
               2.分区算法策略(生产者发送消息的分区策略)详情见1.5.3

          1.3.3写入流程

           1. producer 先从 zookeeper 的 "/brokers/.../state" 节点找到该 partition 的 leader

   2. producer 将消息发送给该 leader
           3. leader 将消息写入本地 log
   4. followers 从 leader pull 消息,写入本地 log 后 向leader 发送 ACK
   5. leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark,最后 commit 的 offset)并向roducer 发送 ACK

    

     

 

 

 

 

 

 

 

 

 

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上。合理控制分区的任务,可以实现负载均衡的效果

       2.提高并行度,生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。

    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的所有读写请求。

   4)replicas 表示某个partition在哪几个broker上存在备份。不管这个几点是不是”leader“,甚至这个节点挂了,也会列出。
   5)isr 是replicas的一个子集,它只列出当前还存活着的,并且已同步备份了该partition的节点。 

       3.每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 Leader

       4.每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和Leader 数据的同步。Leader 发生故障时,某个 Follower 会成为新的 Leader

      5.一个分区就是一个提交日志,消息以追加的方式写入分区,然 后以先入先出的顺序读取,由于一个主题一般包含几个分区,因此无法在整个主题范围内保证消息的顺序,

但可以保证消息在单个分区内的顺序

     

       6.Partition是一个有序的message序列,这些message按顺序添加到一个叫做commit log的文件中。每个partition中的消息都有一个唯一的编号,称之为offset,用来唯一标示某个分区中的message

       7.每个partition,都对应一个commit log文件。一个partition中的message的offset都是唯一的,但是不同的partition中的message的offset可能是相同的

    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值

           2.例如:key1的hash值=5, key2的hash值=6 ,topic的partition数=2,那么key1 对应的value1写入1号分区,key2对应的value2写入0号分区。

           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.需求:如果研发人员可以根据企业需求,自己重新实现分区器

       2.实现步骤:定义类实现 Partitioner 接口。重写 partition()方法。
       3.在 生产者发送消息时配置如下配置
 // 添加自定义分区器
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

          1.批次大小,设置batch的大小,如果 batch 太小,会导致频繁网络请求,吞吐量下降;

      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.该时间阈值由replica.lag.time.max.ms参设定,默认30s。例如2超时,(leader:0, isr:0,1)。这样就不用等长期联系不上或者已经故障的节点

     (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,生产者发送过来数据就不管了,可靠性差,效率高;大数据统计报表场景,对性能要求很高,对数据丢失不敏感的情况可以用这种

   2.acks=1,生产者发送过来数据Leader应答,可靠性中等,效率中等;
   3.acks=-1或者all,生产者发送过来数据Leader和ISR队列里面所有Follwer应答,可靠性高,效率低;
   在生产环境中,acks=0很少使用;acks=1,一般用于传输普通日志,允许丢个别数据;acks=-1,一般用于传输和钱相关的数据,对可靠性要求比较高的场景。 

     1.8生产经验——数据去重

       1.8.1 数据传递语义

         1.至少一次(At Least Once)= ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2,可以保证数据不丢失,但是不能保证数据不重复

     2. 最多一次(At Most Once)= ACK级别设置为0,可以保证数据不重复,但是不能保证数据不丢失
          3.精确一次(Exactly Once):对于一些非常重要的信息,比如和钱相关的数据,要求数据既不能重复也不丢失。Kafka 0.11版本以后,引入了一项重大特性:幂等性和事务

       1.8.2幂等性

         1.幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复

    2.精确一次(Exactly Once) = 幂等性 + 至少一次( ack=-1 + 分区副本数>=2 + ISR最小副本数量>=2)
    3.重复数据的判断标准:具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的Partition 表示分区号;Sequence Number是单调自增的。
         4.所以幂等性只能保证的是在单分区单会话内不重复
         5.开启幂等性参数 enable.idempotence 默认为 true,false 关闭
            

     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版本之前保证数据单分区有序,条件如下:
  max.in.flight.requests.per.connection=1(不需要考虑是否开启幂等性)。
   3.kafka在1.x及以后版本保证数据单分区有序,条件如下:
     (1)开启幂等性
  max.in.flight.requests.per.connection需要设置小于等于5。
     (2)未开启幂等性
    max.in.flight.requests.per.connection需要设置为1。
       (3) 原因说明
        因为在kafka1.x以后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的          

posted on 2022-04-25 13:15  跟着锋哥学Java  阅读(766)  评论(0编辑  收藏  举报

导航