kafka(二):生产与消费

发送消息的幂等性

Broker有判断producer生产消息幂等性的功能:
具体设置:

enable.idempotence=true/false

原理

  • PID(Producer ID)
  • sequence number
      生产者都要有一个唯一的编号,就是PID。每一条消息都要有一个sequence number,如果消息的sequence number小于服务端存储的最大编号,则判定该消息为重复消息。
    在这里插入图片描述
    kafka只保证单个partition的消息具有顺序性,并不保证整个topic的消息具有顺序性。

kafka的事务

什么时候需要事务?

  1. 发送多条消息:例如在分布式微服务系统中的,我们添加增加一条数据可能需要给多个服务发送消息,这个时候就要用到事务来保证整个发送动作的原子性。
  2. 发送消息到多个topic或者多个partition。一条消息需要发送到多个地方,也需要保证原子性。
  3. 消费以后发出消息。就是在消费一条消息后发出一条消息,发送消息失败则消费消息也失败,consume-process-produce。

事务的使用

//初始化事务
producer.initTransactions();
try{
	//开启一个事务
	producer.beginTransaction();
	//发送消息逻辑 ...

	//提交事务
	producer.commitTransaction();
}catch (KafkaException e){
	//终止事务
	producer.abortTransaction();
}

事务实现原理

  1. 2PC
  2. Transaction Coordinator(事务协调者)
  3. 事务日志:topic_transaction_state
  4. 生产者事务ID:transaction.id
    在这里插入图片描述
    A:生产者通过initTransactions API向Coordinator注册事务ID。
    B:Coordinator记录事务日志。
    C:生产者把消息写入目标分区。
    D:分区和Coordinator的交互。当事务完成以后,消息的状态应该是已提交,这样消费者才可以消费到。

Producer发送消息流程及原理

kafka发送消息主要经过下图流程:
  消息先经过拦截器进行预处理,经过序列化器序列化,然后由分区器指定分区,后面追加到累加器。当累加器内消息达到设定的阈值后就会触发Sender线程发送消息。
在这里插入图片描述
我们先看下发送消息的代码:

KafkaProducer<String, String> producer = new KafkaProducer<>(pros);
producer.send(new ProducerRecord<>("topic","key","value"));

拦截器

看其中的send()方法:

 public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
        // intercept the record, which can be potentially modified; this method does not throw exceptions
        ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
        return doSend(interceptedRecord, callback);
    }

可以看到在发送消息前会执行interceptors方法onSend()。这是个拦截器链,可以在消息发送之前做一些定制化的操作。拦截器我们可以自己实现:

public class LogInterceptor implements ProducerInterceptor {
    @Override
    public ProducerRecord onSend(ProducerRecord record) {
        System.out.println("发送了一条消息:"+record.value());
        return record;
    }
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        System.out.println("服务端接收到消息1");
    }
    @Override
    public void close() {
        System.out.println("拦截器关闭");
    }
    @Override
    public void configure(Map<String, ?> configs) {
    }
}

我们简单实现了一个发送消息打印出消息内容的拦截器,然后将拦截器加入到producer中:

List<String> interceptors = new ArrayList<>(1);
        interceptors.add("com.example.demo.LogInterceptor");
        pros.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,interceptors);
        producer.send(new ProducerRecord<>("topic","key","key"));

这样在发送消息的时候就会经过拦截器拦截。

序列化

进入doSend()方法:

serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());

可以看到对消息key和value进行了序列化。我们也可以使用一些其它的序列化工具,例如:ProtoBuf。
序列化配置:

properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

分区选择

在doSend()方法中,有以下代码:

int partition = partition(record, serializedKey, serializedValue, cluster);

partition()方法主要是使用分区器(Partitioner)指定消息的目标分区。

private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
		//获取消息指定的分区
        Integer partition = record.partition();
        //如果指定了则发送到指定的分区,如果未指定则使用partitioner进行选择分区
        return partition != null ?
                partition :
                partitioner.partition(
                        record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
    }

可以看到Partitioner有三个实现类:
在这里插入图片描述

  • DefaultPartitioner:默认分区器,根据key进行hash计算指定到分区。
  • RoundRobinPartitioner:轮询分区器
  • UniformStickyPartitioner:粘滞分区器

自定义分区器

我们可以自定义分区器,实现Partitioner后,配置分区器:

//可以指定kafka分区器和自定义分区器,没有指定分区器就会使用默认的分区器。
props.put("partitioner.class", "com.hj.kafka.producer.MyParatitioner");

批量发送消息

result = accumulator.append(tp, timestamp, serializedKey,
                    serializedValue, headers, interceptCallback, remainingWaitMs, false, nowMs);

消息并不会立即发送,而是追加到累加器中,到达一定数量或时间后才会一起发送消息。
累加器RecordAccumulator使用了ConcurrentHashMap来存储消息:

private final ConcurrentMap<TopicPartition, Deque<ProducerBatch>> batches;

判断累加器是否写满或到时间,达到阈值则批量发送消息:

  if (result.batchIsFull || result.newBatchCreated) {
                log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
                //线程发送
                this.sender.wakeup();
            }

在Producer config中可以进行配置:

		//多少数据发送一次,默认16k
        pros.put("batch.size",16384);
        //批量发送的等待时间
        pros.put("linger.ms",5);

发送消息服务端响应设置

副本消息同步

当Producer将消息发送到分区主节点时,主分区需要发送ack信号给producer来确认消息接收完成。但是当kakfa有多个broker时,就需要考虑副本是不是同步完成。如下图:
在这里插入图片描述
kafka副本同步数据有三种设置:

min.insync.replicas=0/1/-1
  1. Ack = 0:Producer不等待副本同步完消息后Learder发送ack信号,消息发送完毕直接进行下一轮发送。所以该配置情况下,集群可靠性最低,延迟性也是最低,Broker在未接收到消息时宕机了就会消息丢失。
  2. Ack = 1:Leader收到消息后就发送ack信号到Producer,并不等待所有副本同步完成。当follower未同步消息时Leader挂掉,就会造成消息丢失。
  3. Ack = -1:Leader接收到消息后等收到所有follower同步完成的ack信号后,再发送ack信号给Producer。这种情况下可靠性最高,单延迟性也是最高的。

哪些节点参与同步?

Leader需要等待所有副本同步完毕才返回Ack信号,这就意味着若一个follower失去与leader的连接时,就会造成leader接收不到ack信号,导致消息接收失败。针对解决这个问题,kafka使用了ISR来做响应。

  • ISR:in-sync replica set。与leader保持同步的节点。
    配置replica.lag.time.max.ms:如果一个follower在这个时间内没有发送fetch请求或消费leader日志到结束的offset,leader将从ISR中移除这个follower,并认为这个follower已经挂了。

副本分配及消息存储

kafka中副本的数量一定要小于等于机器的节点数,因为当副本数>机器节点数时,肯定有个机器上有多个副本,当这个机器挂掉后,这个机器上的副本都不可用。

副本分配规则

当设置副本数小于机器节点数的时候,肯定需要考虑所有分区在各个副本上怎样分配才均匀。kafka使用了以下方式来进行副本分配:

  1. 副本因子不能大于Broker的个数。
  2. 第一个分区(编号为0)的第一个副本放置位置是随机从BrokerList选择的。
  3. 其它分区的第一个副本的放置位置是相对于0分区的第一个分区位置依次后移。
  4. 剩下的副本随机分配。
    例如:3个Broker,4个分区,2个副本。红色的都是leader节点。
    在这里插入图片描述
为什么要将leader节点错开分布?

防止broker挂掉导致触发多个leader选举。

消息存储位置

kafka会将消息存储在一个log文件中:
在这里插入图片描述

  • .index:消息的offset索引文件
  • .log:存储消息的文件
  • .timeindex:消息的时间索引文件
segment

一个分区内的消息并不一直存储在一个log文件中。当数据量越来越多时会才分为segment存储,每个segment都配两个索引文件,是配套存在的:在这里插入图片描述
配置每个segment大小和轮转时间:
在这里插入图片描述
索引文件也可以设置大小,满了后就会创建一套文件:
在这里插入图片描述
注意kafka的索引并不是每一条消息都会建立索引,而是一种稀疏索引sparse index。这个索引到底有多稀疏?也就是说隔几条才产生一个索引记录?

  • 实际上是用消息的大小来控制的,默认是4kb:
    在这里插入图片描述
    思考:为什么kafka不用B+ Tree?
    kafka是大数据量写入,如果kafka使用B+Tree作为索引,会在写入的时候重建索引,树的调整是非常耗性能的。

索引查找过程

  1. 根据offset判断在哪个segment中。
  2. 在segment的indexfile中,根据offset查找怕position。
  3. 根据position从log文件中找到消息。

消费记录

kakfa会创建50个__consumer_offset topic用来存储消费者的消费offset:
在这里插入图片描述

消息保留(清理)机制

开关与策略

在这里插入图片描述
清理策略有两种:

  • delete:直接删除。定时任务实现,默认5分钟执行一次。
  • compact:将key相同的进行整理,保留最新的一条。
    在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hi wei

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值