Kafka(分区策略以及生产者)


在这里插入图片描述

Kafka生产者

生产者向Topic的分区中发送数据,每个数据对应的进入不同的分区,因为我分区之后就可以横向扩展了,增加节点了,分区之后并发的读写都可以提高并发量,提高吞吐量。那么生产者是怎么向分区发送数据的呢,我们今天producer发送的数据封装成一个ProducerRecord对象

在这里插入图片描述
我们从最后一个开始看,它只传了一个Topic,还有一个value,这个消息要发往哪个Topic,value代表的是我这条数据是什么,就是这个value。那么这样,我们就能够讲这条数据发往Kafka的Topic了。

倒数第二个,中的key就是用来分区用的,而value还是这条数据。

倒数第三个,多了一个Integer partition,这个代表的是分区号,比如0,1,2。

根分区相关的构造器就这三个,我们看看这三个怎么使用

分区原则与ACK机制

(1)指明 partition 的情况下,直接将指明的值作为 partiton 值

意思是指定哪个分区,这条数据就会去往哪个分区。

在这里插入图片描述
(2)没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的

partition 数进行取余得到 partition 值

也就是说余数作为partition值,举例:我一共有5个分区,我key的Hash值除以5,要么得到0或者,1,2,3,4,正好5个分区。

(3)既没有 partition 值又没有 key 值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与 topic 可用的 partition 总数取余得到
partition 值,也就是常说的 round-robin 算法

也就是说下面这个构造器,它要去哪个Partition呢?它怎么round-robin的呢?第一次调用这个分区器的时候,他会先随机生成一个整数,用这个整数去除以这个分区的个数,得到一个余数,这个余数就作为我这条数据要被发往partition的分区号,下一条消息给谁呢,将刚才那个整数加1,然后再除以分区的个数,得到一个新的余数,放到这个余数对应的分区里,这样一来,我们是不是轮询的呀,比如我们呢从2开始,2,3,4,0,1,2,3,4,0,1。。。。。这样轮询的切换。它做了一件事,就是将数据尽可能的均匀的分到不同的分区。也可以理解成负载均衡。

在这里插入图片描述

生产者之消息可靠性

Kafka作为一个传输框架,消息队列,如果要是数据到它这里整丢了能行么?我数据让你缓存一下,你整丢了肯定不行。所以数据消息可靠性是很重要的。所谓可靠性就是完整的送到,别整丢了。

在这里插入图片描述

我们先着重的看第一段,我们提到了Flume,那么FlumeSink其实就相当于Kafka的一个生产者。其底层源码就是Kafka生产者的API。
下面我们以一个分区为例子,看一下生产者发送消息的流程。想要消息可靠性,那就考虑失败了怎么办,失败了我们再发一次就好了,首先生产者会给partition发消息。

在这里插入图片描述
生产者怎么知道发没发送到呢?它肯定会返回给生产者一个相应的。

在这里插入图片描述
生产者收到相应之后,会继续发送下一条消息

在这里插入图片描述

假如我们没有确认收到消息,没有返回给生产者呢?我就重新发送一次,这样的话,我们就能可靠的发送到Partition中,这种机制有可能会数据重复的,有这种情况,消息确实发送过去了,但是确认信息因为网络延迟的缘故,生产者并没有收到确认信息,然后又发送了一遍消息都是有可能的,这种数据重复的情况我们是能解决的,我们后面再说。

在这里插入图片描述
假如们数据发送成功了,我们数据的传输也没有呢么的单纯,一个Partition中会有多个副本,这些副本中还会有Leader和Follower之分,而我生产者发送消息的时候值给Leader发送,而Leader会将数据与Follower进行同步

在这里插入图片描述

问题来了,我什么时候给生产者返回确认收到消息呢?就按照这个消息,一定要可靠的发送到这个Partition中,我应该怎么设计这个东西呢?确保有Follower同步完了之后才会返回给生产者确认收到的消息。为什么不是Leader收到消息之后就发送呢?数据都落盘了呀?如果要是Leader完全收到数据了,直接返回确认收到,但是Follower还没来得及同步数据,Leader就挂掉了,挂掉以后进行选举,从Follower中选出新的Leader之后,这个Leader没有刚才发的数据,生产者再来发送消息,就会对接新的Leader,而发送的数据也会是下一条数据,不会是刚才那一条,那么数据就会丢失。这里就牵扯出一个ack机制,所谓的ack就是确认收到消息,何时发送ack呢?

在这里插入图片描述

问题又来了,多少个Follower完成数据同步之后之后,才会发送ack?这个数字其实是和我们Leader挂掉之后选举相关,为不同的选举机制,有不同的策略才对,我们知道的有半数选举,也可以所有的Follower全部同步完成之后再发送ACK,都不用选了,随便来一个都行,两种方案都可以。

在这里插入图片描述
思考题1:为什么Follower要获得半数以上的投票,才会被选举成新的Leader?

答: 如果不超过半数,有可能两个Follower得票数相同,会发生脑裂,我们要的是只有一个人能够获得半数。

思考题2:选举一个新的Leader,应该有多少人投票?

答: 至少半数以上。

思考题3:我如何确保,我一定能够选举出一个合格的Leader?

答: Follower与Leader同步完了之后才叫合格,假如我们有9个Folower,我应该有几个都同步数据完成了,才能确保一定能够选举出一个新的Leader?至少要有一半以上,要5个才行,我从9个里面随便拿出5个是不是都至少有一个同步完成的Follower。

那么这两种方案的优缺点是什么呢?我们怎么选择呢?

方案 优点 缺点
半数以上完成同步,就发送ack 延迟低 选举新的leader时,容忍n台节点的故障,需要2n+1个副本
全部完成同步,才发送ack 选举新的leader时,容忍n台节点的故障,需要n+1个副本 延迟高

为什么半数以上完成同步,就延迟低呢?数量上只等一半,这不是主要原因,我先同步完的肯定都是快的,如果是慢的,肯定就不用等它了呀,如果要是全部同步,那么最慢同步完的我也要等待,如果是半数,我直接过滤掉最慢的那个,达到半数,就直接发送ACK这样会更快一些。

那么半数的缺点是什么呢?如果我有2n+1个副本,我是半数选举,我只能容忍n个节点挂掉,一定要保证n+1个副本是活着的。那么全部同步完成的这一点要比半数好得多,它只需要一个活着的就行,因为它全部都同步完成了,选谁做Leader都可以。半数选举,因为需要的副本数量太多,会造成大量的数据冗余,会占用大量的磁盘,全部同步完成策略要比半数选举少一半的资源占用。我们的Kafka选择了第二种方案。

原因如下:

  1. 同样为了容忍n台节点的故障,第一种方案需要2n+1个副本,而第二种方案只需要n+1个副本,而Kafka的每个分区都有大量的数据,第一种方案会造成大量数据的冗余
  2. 虽然第二种方案的网络延迟会比较高,但网络延迟对Kafka的影响较小。
    它这里的网络延迟,是指的Follower与Leader同步数据的网络延迟,而Kafka搭建集群的时候应该在同一个机架上面的层面非常大,所以这个网络延迟对Kafka的影响不是很大。还有一点就是,我网络延迟高,也可以优化自己的带宽,但是上面第一个我们就没法优化了,我需要多少个副本就是多少个副本,这是无法改变的事实

ISR

我们上面说了,Kafka选择了第二种方案来进行同步数据以及选举,这里有一个知名的缺陷,如果其中一个Follower迟迟不能同步完成,可能是网络问题,也可能是挂了,那么我们怎么办,我们的Leader要等一辈子,ACK就返回不到生产者那边,这时候ISR闪亮登场,他是干嘛的呢?ISR是in-sync replica set (ISR)的缩写,意思是保持同步的副本集,和谁保持同步的副本集,与Leader保持同步的副本集,在Kafka中,它维护了一个与Leader同步的副本集,是一个集合,这个集合中保存的都是与Leader保持同步的副本,这个东西它有什么用呢?前面说了我们第二种情况是等所有的Follower与Leader都同步完之后Leader才发送ACK,但是真正的Kafka不是这样的,它是等ISR中的Follower与Leader都同步完成之后才发送ACK,这个ISR是一个动态的集合,假如说我们ISR中有一个Follower迟迟不与Leader进行同步,那么我就会将这个Follower踢出ISR,这个踢出是暂时性的踢出,不会永久踢出,踢出它之后,我ISR剩下的Follower都同步完成了,这个时候我的Leader就会给生产者发送ACK。这个机制就能解决我们之前的那个问题。

ISR概念:
Leader维护了一个动态的in-sync replica set (ISR),意为和leader保持同步的follower集合。当ISR中的follower完成数据的同步之后,leader就会给producer发送ack。如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阈值由replica.lag.time.max.ms参数设定。Leader发生故障之后,就会从ISR中选举新的leader

补充:因为ISR是一个集合,所以当Leader挂了,ISR选举会找最靠前的Follower来做Leader。

ACK

经过ISR的概念,我们是可以解决数据丢失问题,但是有些情况,我们可以允许数据丢失,我们对数据可靠性没有那么高的追求,我不需要花那么大的资源来帮我们进行数据的可靠性,这个时候我们怎么去办呢?Kafka为我们提供了,三种可靠性级别,我们可以通过acks这个参数进行设定。

0:producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据

解释:我都不管你怎样,我就是往里面发数据,不管你收没收到,我就一直发。

1:producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据

解释:我不等待Follower同步,只要Leader收到了,我就发ACK,我也不管Leader收到后挂了没挂,和我没关系。

-1(all):producer等待broker的ack,partition的leader和follower全部落盘成功后才返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复

解释:走ISR流程,保证数据不丢。

ACKS=1时,数据丢失案例刨析

首先我们假设Peoducer向Topic中的一个Leader发送了一个Hello。

在这里插入图片描述

Leader收到消息之后将消息落盘,写入追加到.log文件里面

在这里插入图片描述
这个时候Leader应该给Producer发送ACK了,确认收到消息了

在这里插入图片描述

正常我们Leader接受完数据之后,Follower应该同步数据了,但是这个时候,Leader发送完ACK就挂了。。。

在这里插入图片描述

我们数据还没有同步,但是这个时候,我们不管数据会不会同步了,就会选举一个新的Leader。

在这里插入图片描述
选完之后,我们是不是就开始向新的Leader发送数据了呢。比如说发了个Kafka,我新的Leader收到信息之后又开始将名喂Kafka的数据进行落盘,但是,我之前的Hello没有了,Hello就丢了,这个就是我们ACK=1,数据丢失原理细节。

在这里插入图片描述

ACKS=-1时,数据重复案例

下图是一个Producer给Leader发送数据Hello。

在这里插入图片描述
Leader收到消息之后肯定是要落盘的,将数据追加到.log文件中。

在这里插入图片描述

那么,我们什么时候发送ACK呢?要等待全部都同步完才能发送ACK。Follower全部同步到了Hello这个数据。

在这里插入图片描述

数据同步完,全部落盘之后,我们开始发送ACK。但是,发送ACK之前Leader就挂掉了。Leader挂了,ACK是不是发送不了了

在这里插入图片描述

发送不了了,怎么办,生产者还得发送数据呀,但是没有ACK发不了,我们就得选一个新的Leader出来。

在这里插入图片描述

选出Leader之后,我们生产者没有收到ACK,还会继续发送上一条Hello这个数据,给新的Leader,这样我们的新的Leader,那么我们的Hello这个数据是不是就重复了呀。这就是我们消息重复的这个场景。

在这里插入图片描述
如果要是比较重要的数据,我们肯定保证数据不丢失,那我们肯定选择-1,对于一般的数据,我们就让ack=1就好。而且Kafka它默认ACK也是等于1的。

Kafka故障处理细节

所谓的故障指的是什么故障呢?无非就是我一个Partition里面,我的Leader可能会挂掉,我的Follower也可能会挂掉。这个Leader会挂掉完试怎么处理的?Follower挂掉,我又是怎么处理的呢?这里作为简单了解。

首先我们来看下图,下图代表的是一个Partition,里面有Leader,有Follower,这里的数字代表的是offset,大家会发现,为什么我同一个Partition中,我的Leader和Follower怎么长度不一样呢?不是应该保持同步的么?其实这里不是实时的同步,它肯定是有一个过程的,HW是缩写,翻译过来就是高水位,LEO代表的是Log最末端的Offset是多少。每一个副本都有一个自己的Log and Offset,这个LOE应该是我这个副本级别的,我们都知道木桶原理,我们可以将多个副本都堪称是木桶中的每个木片,而HW代表的是我这一个Partition中最小的那个LEO,HW是分区Partition级别。

在这里插入图片描述

HW之前的数据才能对Consumer可见,意思是Consumer只能消费到我都同步完的数据。

在这里插入图片描述

Follower发生故障:
他就会迟迟的不向Leader去同步,我没有办法发送ACK,那么我就会将你踢出ISR,follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步,等该follower的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。

leader故障:
leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据

那么问题来了,有没有这种情况,我Leader也挂了,最长的Follower也挂了,只剩下最短的那个Follower,最短的它最后变成了Leader,但是当我长的Follower活过来了,我从哪里开始同步数据呢?我的LEO比最短的还要长很多,怎么办呢?

在这里插入图片描述

我们上面有提到的,follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,我们的每一个Partition它的HW都会动态的增长,每一次增长,我们的每一个副本都会将HW进行落盘,写到文件里,每次Follower挂掉之后呢,恢复之后,都会读取这个HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步,也就是说,它会将多余的截掉,然后与新的Leader进行同步。

如下图所示:

在这里插入图片描述
它这么做的好处就是,我一个Partition中所有的副本的数据都是完全一致的。要么就都丢数据,要么就都重数据,反正就是一致。

注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复数据丢不丢,重不重是ACK的活儿,不是它的活儿。

Exactly Once语义

再消息传输的领域有三个语义
At Least Once 语义
At Most Once语义
Exactly Once语义

什么是At Least Once ,至少一次,那就证明我会有很多次,可能重复,但是不可能丢失,At Most Once是至多一次,我不会重复,但是有可能丢失,Exactly Once就是不多不少,正好一次,既不丢失,也不重复,我们肯定是想要Exactly Once语义,但是Exactly Once语义不是那么容易实现的,Kafka这边是0.11版本之后,才开始实现的Exactly Once语义,我们现在说的是从生产者到Kafka的Topic这段过程的Exactly Once语义,还没有说从kafka到消费者那段Exactly Once语义,这里我们还要说一下幂等性。

幂等性:我不管你发送多少次消息,我最终的效果就相当于你只发送了一次。

那么Kafka如果有了幂等性,会出现什么神奇的效果呢?我生产者发送数据失败,我会继续发送,这样才有可能造成数据的重复,如果有了幂等性机制,我甭管你发多少次,我的效果就相当于你发了一次,是不是相当于再Topic下的Partition下Log中的Segment下的.log文件中只追加了一次,落了一次盘,如果配合At Least Once语义让 ack = -1,再加上幂等性,是不是就相当于间接性的实现了Exactly Once语义呢?

幂等机制怎么用?很简单,只需要我们再Kafka的生产者这一端设置一个属性

At Least Once + 幂等性 = Exactly Once

要启用幂等性,只需要将Producer的参数中enable.idompotence设置为true即可。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的Producer在初始化的时候会被分配一个PID,发往同一Partition的消息会附带Sequence Number。而Broker端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker只会持久化一条

但是PID重启就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区跨会话的Exactly Once

幂等原理

幂等性到底怎么做到的呢?所谓的幂等就是去重,Kafka中式怎么去重的呢?

看图:

在这里插入图片描述

如果按照上图来,我们在Kafka中维护一个集合,就来干这个确认ID的活儿,是不是开销有点大啊,我们的消息有可能成千上万,上亿条,我们维护这么多数据,Kafka受得了么?那么Kafka到底怎么做的呢?

我们Kafka会将这个数据的ID暂时保存在内存中,内存中的数据ID永远为最新

在这里插入图片描述

我们发送了一条数据,ID为0,我们保存到了内存中,然后返回个ACK给Producer,然后我们将ID=0的数据进行落盘,追加到log文件中去落盘,我们再发送第二条数据ID=1的时候,这个时候,我们内存中保存的ID=0就会和这个ID=1进行比较,如果发现不是0了,我们将替换成ID=1,然后将ID=1这个数据追加到log文件中去落盘,等待下一条数据来,就会变成下图这样。那么为什么我们单凭一个ID就能判断是不是最新的消息呢?我们这个ID不是随机的,是有规律的,如果我们这个ID是随机的,那么内存中必须维护着所有的ID才行,我这变的ID是递增的,所以不用维护所有的就行了。这就是我们Kafka中实现幂等的原理。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_45284133/article/details/106879540