【Kafka学习-4】生产者详解

生产者发送消息到broker流程

上图是生产者发送消息到broker的图解,流程如下:

1. 先通过main线程创建kafka的Producer,然后Producer把数据发出。数据途中会经过拦截器,序列化器,最终到达分区器,让分区器把数据发送到RecordAccumulator(缓存队列)的分区中。

2. 然后sender线程从缓存队列中拉去数据,当满足batch.size条件或linger.ms条件的时候,就会发送数据到broker。broker收到数据后就会存储到对应的分区,同步副本。

3. 发送数据之后,需要等待broker应答。如果broker没有应答(acks),但是又有数据需要发送,就会先把这些数据包装成请求,存到NetworkClient的InFlightRequest队列中,一个队列对应缓存队列中的一个分区。当收到broker的应答之后,就会继续发送队列中的请求到broker。

4. 如果应答成功,就会删掉InFlightRequest队列中的数据。应答失败,则重试发送数据,重试次数为 int 最大值,2147483647。

acks应答的三种级别:

  • 0:生产者发送过来的数据,不需要等数据落盘应答。
  • 1:生产者发送过来的数据,Leader收到数据后应答。
  • -1 (all):生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答。-1 和all等价。
     

Kafka分区的意义

第一点的负载均衡如何理解?

举个例子,比如有3个broker,有1个broker能存储1T数据,有两个broker可以存储100T数据。那么就分区器“调度”发送消息的时候,使用“能者多劳”的分配策略,就会多往两个100T的broker发送消息,1T的少发,实现负载均衡。

Java Api操作

pom添加依赖

		<dependency>
			<groupId>org.apache.kafka</groupId>
			<artifactId>kafka-clients</artifactId>
			<version>3.0.0</version>
		</dependency>

生产者发送消息代码演示

package com.example.kafka;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class CustomProducer {
    public static void main(String[] args) {
        // 1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        // 2. 给 kafka 配置对象添加配置信息:bootstrap.servers连接集群
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.1.61:9092");
        // 指定对应的key,value的序列化类型(必须):key.serializer,value.serializer
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());  //把发送的key从字符串序列化为字节数组
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());  //把发送消息value从字符串序列化为字节数组
        /* 等价写法:
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                        "org.apache.kafka.common.serialization.StringSerializer");

        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                        "org.apache.kafka.common.serialization.StringSerializer");
         */

        // 3. 创建 kafka 生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
        // key:""  value:"hello kafka"+i
        for (int i = 0; i < 5; i++) {
            // 4. 调用 send 方法,发送消息 (topic,value)
            kafkaProducer.send(new ProducerRecord<>("first","hello kafka"+i));
        }
        //关闭资源
        kafkaProducer.close();

    }
}

三种发送方式

异步发送:生产者只负责把消息发送出去就算发送成功,无需关注broker是否接收成功。调用方法producer.send(producerRecord)。

回调异步发送:调用方法producer.send(producerRecord, callBack),异步发送之后会回调Callback的实现方法。

同步发送:生产者把消息发送出去之后,需要等待broker应答,才算完成发送。只需在异步方法后再调用一个get()方法,即可实现同步发送,并且需要处理异常。

// 异步发送
kafkaProducer.send(new ProducerRecord<>("topicName", "AAAA"));

// 回调异步发送
kafkaProducer.send(new ProducerRecord<>("topicName", "BBB"), new Callback() {
    @Override
    public void onCompletion(RecordMetadata recordMetadata, Exception e) {
        if(e == null){
            System.out.println("send success!");
            System.out.println("topic:" + recordMetadata.topic());
            System.out.println("partition:" + recordMetadata.partition());
        }else {
            e.printStackTrace();
        }
    }
});

// 同步发送
kafkaProducer.send(new ProducerRecord<>("topicName", "AAAA")).get();

Kafka的分区策略

从ProducerRecord的构造方法中,分区策略可以分为3种:

  • 指定分区:可以通过传入partition参数,来决定数据发送到哪个分区
  • 通过key指定分区:没有传入partition参数,就会计算key的哈希值,然后通过哈希值除以主题的分区数取模,决定发送到哪一个partition。
  • 粘性分区:如果既没有传partition,又没有传key,就会随机抽取一个分区发送,如果后续还有数据没有指定分区发送,还会“粘着”刚刚随机抽取的分区继续发送。例如:第一次随机选择0号分区,等0号分区当前批次满了(默认16k) 或者linger.ms设置的时间到,Kafka再随机一 个分区进行使用(如果还是0会继续随机)。

自定义分区器

需求:如果消息中包含"hello"发送到分区1,不包含则发送到分区0

第一步:实现Partitioner,重新编写partition方法,返回分区的序号

package com.example.kafka;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

import java.util.Map;

public class MyPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        String msg = value.toString();
        if (msg.contains("hello")) {
            return 1;
        }
        return 0;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> configs) {

    }
}

第二步:在生产者的配置中,加上自定义的分区器配置

properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "com.example.kafka.MyPartitioner");

提高生产者吞吐量

可以修改以下4个参数

  1. batch.size:批次大小,默认16k (也就是一个batch满了16kb就发送出去).如果 batch 太小,会导致频繁网络请求,吞吐量下降;如果batch太大,会导致一条消息需要等待很久才能被发送出去,而且会让内存缓冲区有很大压力,过多数据缓冲在内存里.一般在实际生产环境,这个batch的值可以增大一些来提升吞吐量。
  2. linger.ms:等待时间,默认值是0ms(意思就是消息立即被发送,不延迟,来一条发送一条),但是这是不对的。可以将其修改为5-100ms.假如linger.ms设置为为50ms,消息被发送出去后会进入一个batch,如果50ms内,这个batch满了16kb就会被发送出去。但是如果50ms时间到,batch没满,那么也必须把消息发送出去了,不能让消息的发送延迟时间太长,也避免给内存造成过大的一个压力。
  3. compression.type:默认是none,不压缩,可以使用lz4,snappy等压缩,压缩之后可以减小数据量,提升吞吐量,但是会加大producer端的cpu开销。
  4. RecordAccumulator(buffer.memory):设置发送消息的缓冲区,默认值是33554432(32MB).如果发送消息出去的速度小于写入消息进去的速度,就会导致缓冲区写满,此时生产消息就会阻塞住,所以说这里就应该多做一些压测,尽可能保证说这块缓冲区不会被写满导致生产行为被阻塞住.缓冲区大小,可以将其修改为64m

Java相关配置

// batch.size:批次大小,默认 16K
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);

// linger.ms:等待时间,默认 0
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);

// RecordAccumulator:缓冲区大小,默认 32M:buffer.memory
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);

// compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");

发送数据的可靠性

kafka的可靠性,依靠的就是acks应答机制。

当acks=0时,生产者发送过来数据就不管了,可靠性差,效率高。

当acks=1时,生产者发送过来数据只有Leader应答,follwer是否同步完成不管,可靠性中等,效率中等。

当ack=-1时,生产者发送过来数据Leader和ISR队列里面所有Follwer应答,可靠性高,效率低;

思考:Leader收到数据,所有Follower都开始同步数据,但有一个Follower,因为某种故障,迟迟不能与Leader进行同步,那这个问题怎么解决呢?

Leader维护了一个动态的in-sync replica set(ISR),意为和Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2)。如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms参数设定,默认30s。例如2超时,(leader:0, isr:0,1)。这样就不用等长期联系不上或者已经故障的节点。

简单来说,就是如果follwer超过一定时长没有应答,将被踢出ISR队列。

数据可靠性分析(以下2种情况和ack=1的效果是一样的,仍然有丢失数据的风险(leader:0,isr:0)):

  • 分区副本partition设只有1个
  • ISR里应答的最小副本数量(min.insync.replicas,默认为1)设置为1。这种情况就等于只需leader接收到数据,就完成了应答。

所以要使kafka的数据完全可靠,必须要有以下条件

  1. ack=-1
  2. 保证分区副本数量>=2
  3. isr应答的最小副本数量>=2

Java相关配置

// 设置 acks
properties.put(ProducerConfig.ACKS_CONFIG, "all");

// 重试次数 retries,默认是 int 最大值,2147483647
properties.put(ProducerConfig.RETRIES_CONFIG, 3);

数据重复发送问题

问题场景:假设生产者发送了一个数据“hello”给leader,leader接收到数据,并且把数据同步给了follower,但是在leader应答之前宕机了,然后重新选举一个副本作为leader。这时可能就会出现问题,“hello”这个消息在旧leader挂掉之前已经同步给了follower,也就是现在新的leader已经也有“hello”这个消息,但是生产者没有收到旧leader的应答,就会认为“hello”这个消息没有发送成功,然后重新发送“hello”这个消息给新的leader,这样就导致新的leader就会有两个“hello”,从而造成数据重复。

数据传递语义:

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

为了解决保证数据不重复的问题,于是有了“精确一次”的概念。

  • 精确一次(Exactly Once)= 幂等性+至少一次。对于一些非常重要的信息,比如和钱相关的数据,要求数据既不能重复也不丢失。Kafka 0.11版本以后,引入了一项重大特性:幂等性和事务

幂等性

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

重复数据的判断标准:具有<PID, Partition, SeqNumber> 相同主键的消息提交时,Broker只会持久化一条。其中PID是指生产者id号,Kafka每次重启都会分配一个新的; Partition 表示分区号; Sequence Number是单调自增的。

当接收到有两个或多个数据的 PID, Partition, SeqNumber 都相同的情况下,只会把第一条接收到的数据持久化到磁盘。如果kafka重启,PID就会重新分配,无法判断是否有数据和重启之前的重复。

所以幂等性只能保证的是在单分区单会话内不重复,当重启之后就不再是之前的会话,还是会有数据重复的问题。

配置

开启参数 enable.idempotence 默认为 true,false 表示关闭。

对应的Java配置

properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,"true");

事务

幂等性不能完全解决数据重复的问题,是因为重启之后的PID会改变。所以保证数据的唯一,就需要先保证PID不会改变。

自定义transcational.id就可以解决这个问题,transcational.id会和PID绑定一起存储在磁盘中,即使kafka重启,只要有transcation.id就可以找回之前的PID,从而保证了重启之后生产者的PID不会变。

下面是尚硅谷事务的流程图,具体原理尚硅谷解释的有点懵逼,因为transcational.id的作用解释的很模糊,也没有在流程图中体现出来。

尚硅谷讲解这个流程的时候我是难理解的,详情还是看看别人的笔记吧:

kafka学习笔记

kafka之事务

Kafka事务特性详解

事务相关的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;

代码演示

package com.hyj.kafka.producer2;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;

public class CustomProducerTransactions {
    public static void main(String[] args) {
        // 1. 创建 kafka 生产者的配置对象
        Properties properties = new Properties();
        // 2. 给 kafka 配置对象添加配置信息
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102: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_1");
        // 3. 创建 kafka 生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
        // 初始化事务
        kafkaProducer.initTransactions();
        // 开启事务
        kafkaProducer.beginTransaction();
        try {
            // 4. 调用 send 方法,发送消息
            for (int i = 0; i < 5; i++) {
                // 发送消息
                kafkaProducer.send(new ProducerRecord<>("first", "kafka" + i));
            }
            //int i = 1 / 0;
            // 提交事务
            kafkaProducer.commitTransaction();
        } catch (Exception e) {
            // 终止事务
            kafkaProducer.abortTransaction();
        } finally {
            // 5. 关闭资源
            kafkaProducer.close();
        }
    }
}

数据有序

kafka目前只有在单分区内才能实现有序消费,多个分区之间无法实现。

kafka在1.x版本之前保证数据单分区有序,条件如下:
max.in.flight.requests.per.connection=1(不需要考虑是否开启幂等性):在阻塞之前,客户端将在单个连接上发送的未确认请求的最大数量。


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

图解

保证数据有序图解

假设生产者需要发送request12345到broker,消息的seqNumer也是按12345排序。

request1和2按顺序发送到broker,seqNumber顺序正确,就会先落盘。

轮到发送request3的时候,出现网络波动,发送失败需要重试。

在重试期间,request4和5被发送到broker,但是不会被落盘,而是先被缓存起来,因为根据seqNumber排序,必须要等到seqNumber=3的数据才能落盘。

等到request3重新发送成功,就会在缓存按照seqNumber重新排序剩下的345,然后落盘。
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值