深入浅出理解kafka ---- 万字总结(下)

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 深入浅出理解kafka ---- 万字总结(下)

深入浅出理解kafka ---- 万字总结(上):

https://developer.aliyun.com/article/1393775?spm=a2c6h.24874632.expert-profile.14.6af22f31AcelSW


2.6 生产者


生产者是数据的入口。Producer在写入数据的时候永远是找leader,不会直接将数据写入follower。

13.png

2.6.1 分区可以水平扩展


Kafka 的消息组织方式实际上是三级结构:主题 - 分区 - 消息。主题下的每条消息只会保存在某一个分区中,而不会在多个分区中被保存多份。


分区的作用主要提供负载均衡的能力,能够实现系统的高伸缩性(Scalability)。不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理。这样,当性能不足的时候可以通过添加新的节点机器来增加整体系统的吞吐量。


分区原则:需要将 Producer 发送的数据封装成一个 ProducerRecord 对象。


该对象需要指定一些参数:


topic:string 类型,NotNull。


partition:int 类型,可选。


timestamp:long 类型,可选。


key:string 类型,可选。


value:string 类型,可选。


headers:array 类型,Nullable。


2.6.2 分区策略


所谓分区策略是决定生产者将消息发送到哪个分区的算法。Kafka 为我们提供了默认的分区策略,同时它也支持自定义分区策略。


分区的原因


  • 方便在集群中扩展:每个partition通过调整以适应它所在的机器,而一个Topic又可以有多个partition组成,因此整个集群可以适应适合的数据


  • 可以提高并发:以Partition为单位进行读写。类似于多路。


2.6.2.1 轮询策略


Round-robin 策略,即顺序分配。比如一个主题下有 3 个分区,那么第一条消息被发送到分区 0,第二条被发送到分区 1,第三条被发送到分区 2,以此类推。当生产第 4 条消息时又会重新开始,即将其分配到分区 0

14.png

轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认
情况下它是最合理的分区策略
,也是我们最常用的分区策略之一。


2.6.2.2 随机策略-Randomness 策略


随机就是我们随意地将消息放置到任意一个分区上,如下面这张图所示。

15.png

随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。


2.6.2.3 按消息键保序策略-Key-ordering 策略


Kafka 允许为每条消息定义消息键,简称为 Key。这个 Key 的作用非常大,它可以是一个有着明确业务

含义的字符串,比如客户代码、部门编号或是业务 ID 等;也可以用来表征消息元数据。特别是在 Kafka

不支持时间戳的年代,在一些场景中,工程师们都是直接将消息创建时间封装进 Key 里面的。一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略。


key1 — 落在同一分区


key2 — 落在同一个分区


2.6.2.4 默认分区规则。


  1. 如果指定的partition,那么直接进入该partition。


  1. 如果没有指定partition,但是指定了key,使用key的 hash一选择partition。


  1. 如果既没有指定partition,也没有指定key,使用轮询一的方式进入partition。


2.7 消费者


传统的消息队列模型的消息一旦被消费,就会从队列中被删除,而且只能被下游的一个Consumer 消费。这种模型的伸缩性scalability)很差,因为下游的多个 Consumer 都要“抢”这个共享消息队列的消息。发布 / 订阅模型倒是允许消息被多个 Consumer 消费,但它的问题也是伸缩性不高,因为每个订阅者都必须要订阅主题的所有分区。这种全量订阅的方式既不灵活,也会影响消息的真实投递效果。


当 Consumer Group 订阅了多个主题后,组内的每个实例不要求一定要订阅主题的所有分区,它只会消费部分分区中的消息。Consumer Group 之间彼此独立,互不影响,它们能够订阅相同的一组主题而互不干涉。再加上 Broker 端的消息留存机制,Kafka 的 Consumer Group 完美地规避了上面提到的伸缩性差的问题。可以这么说,Kafka 仅仅使用 Consumer Group 这一种机制,却同时实现了传统消息引擎

系统的两大模型:


如果所有实例(消费者)都属于同一个 Group,那么它实现的就是点对点消息队列模型;


如果所有实例(消费者)分别属于不同的 Group,那么它实现的就是发布 / 订阅模型。


2.7.1 消费方式


Consumer 采用 Pull(拉取)模式从 Broker 中读取数据。


Pull 模式则可以根据 Consumer 的消费能力以适当的速率消费消息。Pull 模式不足之处是,如果 Kafka没有数据,消费者可能会陷入循环中,一直返回空数据。


因为消费者从 Broker 主动拉取数据,需要维护一个长轮询,针对这一点, Kafka 的消费者在消费数据时会传入一个时长参数 timeout。如果当前没有数据可供消费,Consumer 会等待一段时间之后再返回,这段时长即为 timeout。


2.7.2 分区分配策略


一个消费者可以订阅多个主题,可以去消费多个分区,一个分区不支持多个消费者(同一个消费组)读

取。


一个消费者组中有多个 consumer,一个 topic 有多个 partition,所以必然会涉及到 partition 的分

配问题,即确定那个 partition 由哪个 consumer 来消费。当消费者组里面的消费者个数发生改变的时

候,也会触发再平衡。


Kafka 有四种分配策略,可以通过参数 partition.assignment.strategy 来配置,默认 Range + CooperativeSticky。


RoundRobin:针对集群中的所有topic;轮询的方式依次将分区分配给消费者。


Range,默认为Range:针对每个topic;通过 分区数 / 消费者数 决定每个消费者消费几个分区。如果除不尽则前面几个消费者会多消费1个分区(最好要保证分区数量可以整除消费者数量,不然会随着topic数量的增多,数据倾斜会越严重)。


Sticky:首先会尽量均衡放置分区到消费者上面,出现同一消费组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。


CooperativeSticky:在不停止消费的情况下进行增量再平衡。


(1)RangeAssignor分配策略


RangeAssignor 分配策略的原理是按照消费者总数和分区总数进行整除运算来获得一个跨度, 然后将分

区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。


每一个主题,RangeAssignor策略会将消费组内所有订阅这个主题的消费者按照名称的字典序排序,然

后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分

区。


假设n= 分区数/消费者数量,m= 分区数%消费者数量,那么前m个消费者每个分配n+1个分区,后面

的(消费者数量-m)个消费者每个分配n个分区。


假设消费组内有2个消费者C0和C1都订阅了主题t0和t1, 并且每个主题都有4个分区,那 么订阅的所有分

区可以标识为: t0p0、t0p1、t0p2、t0p3、t1p0、t1p1、t1p2、t1p3。最终的分配结果为:


消费者C0: t0p0、t0p1、t1p0、t1p1


消费者C1: t0p2、t0p3、t1p2、t1p3


这样分配得很均匀,那么这个分配策略能够一直保持这种良好的特性吗?我们不妨再来看 另一种情况。

假设上面例子中2个主题都只有3个分区,那么订阅的所有分区可以标识为:t0p0、 t0p1、t0p2、

t1p0、t1p1、t1p2最终的分配结果为:


消费者C0: t0p0、t0p1、t1p0、t1p1


消费者C1: t0p2、t1p2


可以明显地看到这样的分配并不均匀,如果将类似的情形扩大,则有可能出现部分消费者过载的情况。


对此我们再来看另一种RoundRobinAssignor策略的分配效果如何。


总结:


最好要保证分区数量可以整除消费者数量,不然会随着topic数量的增多,数据倾斜会越严重


(2)RoundRobinAssignor分配策略


RoundRobinAssi gnor 分配策略的原理是将消费组内所有消费者及消费者订阅的所有主题的分 区按照字

典序排序,然后通过轮询方式逐个将分区依次分配给每个消费者。RoundRobinAssignor 分配策略对应

的Partition.assignment.strategy参数值为org.apache.kafka.C1ients.Consumer.RoundRobinAssignor。


如果同一个消费组内所有的消费者的订阅信息都是相同的,那么RoundRobinAssignor分配策略的分区

分配会是均匀的。


比如:假设消费组中有2个消费者C0 和C1都订阅了主题 t0和t1, 并且每个主题都有3个分区,那么订阅的

所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、 t1p1、t1p2。最终的分配结果为:


消费者C0: t0p0、t0p2、t1p1


消费者C1: t0p1、t1p0、t1p2


如果同一个消费组内的消费者订阅的信息是不相同的, 那么在执行分区分配的时候就不是完全的轮询分

配,有可能导致分区分配得不均匀。 如果某个消费者没有订阅消费组内的某个主题, 那么在分配分区的

时候此消费者将分配不到这个主题的任何分区。


比如:假设消费组内有3个消费者(C0、 C1和C2), 它们共订阅了3个主题(t0、t1、 t2) , 这 3个主题分别有

1、2、3个分区, 即整个消费组订阅了t0p0、 t1p0、 t1p1、 t2p0、 t2p1、 t2p2这6个分区。 具体而

言, 消费者 C0 订阅的是主题t0, 消费者C1 订阅的是主题t0和t1, 消费者C2 订阅的是主题t0、t1和t2, 那

么最终的分配结果为:


消费者C0: t0p0


消费者C1: t1p0


消费者C2: t1p1、t2p0、 t2p1、t2p2


可以看到RoundRobinAssignor策略也不是十分完美, 这样分配其实并不是最优解, 因为完全可以将分

区t1p1 分配给消费者C1。


所以需要注意:如果使用RoundRobinAssignor策略,则消费者应该订阅相同的主题。


(3)StickyAssignor分配策略


我们再来看一下StickyAssignor分配策略, “sticky"这个单词可以翻译为“ 黏性的”, Kafka 从0.11.x版本

开始引入这种分配策略, 它主要有两个目的:


(1)分区的分配要尽可能均匀。


(2)分区的分配尽可能与上次分配的保待相同。


当两者发生冲突时, 第一个目标优先于第二个目标。


鉴于这两个目标, StickyAssignor分配策略的具体实现要比RangeAssignor和RoundRobinAssignor这

两种分配策略要复杂得多。 我们举 例来看一下StickyAssignor分配策略的实际效果。


假设消费组内有3个消费者(C0、C1和C2),它们都订阅了4个主题(t0、t1、t2、t3),并且每个主题有2个

分区。 也就是说,整个消费组订阅了t0p0、 t0p1、 t1p0、 t1p1、 t2p0、 t2p1、 t3p0、 t3p1这8个分区。 最终的分配结果如下:


消费者C0: t0p0、t1p1、t3p0


消费者C1: t0p1、t2p0、t3p1


消费者C2: t1p0、t2p1


这样初看上去似乎与采用RoundRobinAssignor分配策略所分配的结果相同, 但事实是否真的如此呢?

再假设此时消费者 C1脱离了消费组, 那么消费组就会执行再均衡操作,进而消费分区会重新分配。 如

果采用RoundRobinAssignor 分配策略, 那么此时的分配结果如下:


消费者C0: t0p0、t1p0、t2p0、t3p0


消费者C2: t0p1、t1p1、t2p1、t3p1


如分配结果所示,RoundRobinAssignor分配策略会按照消费者C0 和C2进行重新轮询分配。 如果此时

使用的是StickyAssignor分配策略,那么分配结果为:


消费者C0: t0p0、t1p1、t3p0、t2p0


消费者C2: t1p0、t2p1、t0p1、t3p1


可以看到分配结果中保留了上一次分配中对消费者 C0 和C2的所有分配结果,并将原来消费者C1的 “ 负

担 “ 分配给了剩余的两个消费者 C0 和C2, 最终 C0 和C2的分配还保持了均衡。


如果发生分区重分配,那么对于同一个分区而言,有可能之前的消费者和新指派的消费者不是同一个,

之前消费者进行到一半的处理还要在新指派的消费者中再次复现一遍,这显然很浪费系统资源。


StickyAssignor 分配策略如同其名称中的"st1cky" 一样,让分配策略具备一定 的 “ 黏性 ” ,尽可能地让

前后两次分配相同,进而减少系统资源的损耗及其他异常情况的发生。


到目前为止,我们分析的都是消费者的订阅信息都是相同的情况,我们来看一下订阅信息不同的清况下

的处理。


举个例子,同样消费组内有3个消费者(C0、C1和C2) , 集群中有3个主题(t0、t1和 t2) , 这3个主题分别有

1、2、3个分区。也就是说,集群中有t0p0、 t1p0、 t1p1、 t2p0、 t2p1、 t2p2这6个分区。消费者

C0 订阅了主题t0,消费者C1订阅了主题t0和t1, 消费者C2订阅了主题t0、t1和t2。 如果此时采用

RoundRobinAssignor分配策略,那么最终的分配结果如RoundRobinAssignor分配策略时的一样


RoundRobinAssignor分配策略的分配结果


消费者C0: t0p0


消费者C1: t1p0


消费者C2: t1p1、t2p0、t2p1、t2p2


如果此时采用的是StickyAssignor分配策略,那么最终的分配结果如下所示。


StickyAssignor分配策略的分配结果


消费者C0: t0p0


消费者C1: t1p0、t1p1


消费者C2: t2p0、t2p1、t2p2


可以看到这才是一个最优解(消费者C0 没有订阅主题t1和t2, 所以不能分配主题t1和t2 中的任何分区给它, 对于消费者C1也可同理推断)。


假如此时消费者C0 脱离了消费组, 那么RoundRobinAssignor分配策略的分配结果为:


消费者C1: t0p0、t1p1


消费者C2: t1p0、t2p0 、t2p1、 t2p2


可以看到RoundRobinAssignor策略保留了消费者C1和C2中原有的3个分区的分配:t2p0、 t2p I和t2p2。


如果采用的是StickyAssignor分配策略, 那么分配结果为:


消费者C1: t1p0、t1p1、t0p0


消费者C2: t2p0、t2p1、 t2p2


可以看到StickyAssignor分配策略保留了消费者C1和C2中原有的5个分区的分配:t1p0、t1p1、t2p0、

t2p1、t2p2。


使用StickyAssignor分配策略的一个优点就是可以使分区重分配具备 “ 黏性"’ 减少不必要的分区移动(即

一个分区剥离之前的消费者,转而分配给另一个新的消费者)。


StickyAssignor分配策略比另外两者分配策略而言显得更加优异,但这个策略的代码实现也异常复杂,就不介绍了


2.8 数据可靠性保证


为保证 Producer 发送的数据,能可靠地发送到指定的 Topic,Topic 的每个 Partition 收到 Producer发送的数据后,都需要向 Producer 发送 ACK(ACKnowledge 确认收到)。如果 Producer 收到 ACK,就会进行下一轮的发送,否则重新发送数据。

16.png

2.8.1 副本数据同步策略


确保有 Follower 与 Leader 同步完成,Leader 再发送 ACK,这样才能保证 Leader 挂掉之后,能在 Follower 中选举出新的 Leader 而不丢数据。

17.png

2.8.1.1 ISR(同步副本集)


猜想???


采用了第二种方案进行同步ack之后,如果leader收到数据,所有的follower开始同步数据,但有一个follower因为某种故障,迟迟不能够与leader进行同步,那么leader就要一直等待下去,直到它同步完成,才可以发送ack,此时需要如何解决这个问题呢?


解决!!!


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


2.8.2 ACK 应答机制


Kafka 为用户提供了三种可靠性级别,用户根据可靠性和延迟的要求进行权衡,选择以下的配置。

18.png

ACK 参数配置:


0:Producer 不等待 Broker 的 ACK,这提供了最低延迟,Broker 一收到数据还没有写入磁盘就已经返回,当 Broker 故障时有可能丢失数据。


1:Producer 等待 Broker 的 ACK,Partition 的 Leader 落盘成功后返回 ACK,如果在 Follower同步成功之前 Leader 故障,那么将会丢失数据。



-1(all):Producer 等待 Broker 的 ACK,Partition 的 Leader 和 Follower 全部落盘成功后才返回 ACK。但是在 Broker 发送 ACK 时,Leader 发生故障,则会造成数据重复。


2.8.3 可靠性指标


1. 分区副本,你可以创建更多的分区来提升可靠性,但是分区数过多也会带来性能上的开销,一般来说,3个副本就能满足对大部分场景的可靠性要求。


(举个例子如果分区过多的话,那么我们kafka需要维护这些分区,并且还要创建大量副本来保证,虽然分区多了,一个坏了丢失的数据会少很多,但是同样的性能就下降很多了,因为分区副本需要同步进行维护)



2. ACKS,生产者发送消息的可靠性,也就是我要保证我这个消息一定是到了broker并且完成了多副本的持久化。


(但是如果ack如果要确保全部副本的持久化和同步的话其实也会带来性能下降问题,不理解的可以去ack应答机制那里反复看看)



3. 保障消息到了broker之后,消费者也需要有一定的保证,因为消费者也可能出现某些问题导致消息没有消费到。



4.enable.auto.commit默认为true,也就是自动提交offset,自动提交是批量执行的,有一个时间窗口,这种方式会带来重复提交或者消息丢失的问题,所以对于高可靠性要求的程序,要使用手动提交。 对于高可靠要求的应用来说,宁愿重复消费也不应该因为消费异常而导致消息丢失。


总结


一个主题多个分区的场景下,kafka只能保证同一个分区的消息顺序性,不能保证不同分区间的消息顺序性。


一般,配置三个副本就可以满足绝大部分需求。


一个消费者可以订阅多个主题,可以去消费多个分区,但一个分区不支持多个消费者(同一个消费组)读取。


      如果所有的Consumer都属于一个group,那么就是一对一、点对点的消费,如果每个consumer属于不同的group,那么消息就是广播给所有的消费者。这个实际上是根据partition来分的,一个partition,只能被消费组里的一个消费者消费,但是可以同时被多个消费组的消费者轮流消费(不会产生竞争关系),消费组里的每个消费者拥有与之对应的一个partition,因此,对于一个topic,同一个group中中不能有多于partition个数的consumer同时消费,否则某些consumer将无法消费到数据。


相关文章
|
1月前
|
消息中间件 存储 Java
Kafka核心知识点整理,收藏再看!
Kafka核心知识点整理,收藏再看!
Kafka核心知识点整理,收藏再看!
|
6月前
|
消息中间件 JSON Kafka
【十九】初学Kafka并实战整合SpringCloudStream进行使用
【十九】初学Kafka并实战整合SpringCloudStream进行使用
121 1
【十九】初学Kafka并实战整合SpringCloudStream进行使用
|
6月前
|
消息中间件 JSON Kafka
深入浅出分析kafka客户端程序设计 ----- 生产者篇----万字总结(上)
深入浅出分析kafka客户端程序设计 ----- 生产者篇----万字总结(上)
198 1
|
6月前
|
消息中间件 存储 Kafka
深入浅出分析kafka客户端程序设计 ----- 消费者篇----万字总结(上)
深入浅出分析kafka客户端程序设计 ----- 消费者篇----万字总结(上)
276 1
|
6月前
|
消息中间件 存储 监控
深入浅出理解kafka ---- 万字总结(上)
深入浅出理解kafka ---- 万字总结(上)
161 0
|
6月前
|
消息中间件 存储 负载均衡
深入浅出分析kafka客户端程序设计 ----- 生产者篇----万字总结(下)
深入浅出分析kafka客户端程序设计 ----- 生产者篇----万字总结(下)
326 0
|
6月前
|
消息中间件 Kafka 网络安全
深入浅出分析kafka客户端程序设计 ----- 消费者篇----万字总结(下)
深入浅出分析kafka客户端程序设计 ----- 消费者篇----万字总结(下)
301 0
|
消息中间件 安全 Java
kafka入门必备知识
Kafka是一个分布式流处理平台: 1. 可以让你发布和订阅流式的记录。这一方面与消息队列或者企业消息系统类似。 2. 可以储存流式的记录,并且有较好的容错性。 3. 可以在流式记录产生时就进行处理。
111 1
|
消息中间件 存储 Kafka
Kafka 实战开篇-讲解架构模型、基础概念以及集群搭建(下)
Kafka 实战开篇-讲解架构模型、基础概念以及集群搭建(下)
180 0
|
消息中间件 NoSQL 中间件
Kafka 实战开篇-讲解架构模型、基础概念以及集群搭建(上)
Kafka 实战开篇-讲解架构模型、基础概念以及集群搭建
340 0