费者可能会陷入循环中, 一直返回空数据。 针对这一点, Kafka 的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费, consumer 会等待一段时间之后再返回,这段时长即为 timeout。
轮训
那么消费者是如何知道生产者发送了数据呢?换一句话来说就是,消费者什么时候 pull 数据呢? 其实生产者产生的数据消费者是不知道的,KafkaConsumer 采用轮询的方式定期去 Kafka Broker 中进行数据的检索,如果有数据就用来消费,如果没有就再继续轮询等待
消息存储的方式
kafka存储的数据采用的文件存储,并且是在后边不断地追加保证数据的读写顺序。
7.0 核心API
四个核心的API
Producer API,它允许应用程序向一个或多个 topics 上发送消息记录
Consumer API,允许应用程序订阅一个或多个 topics 并处理为其生成的记录流
Streams API,它允许应用程序作为流处理器,从一个或多个主题中消费输入流并为其生成输出流,有效的将输入流转换为输出流。
Connector API,它允许构建和运行将 Kafka 主题连接到现有应用程序或数据系统的可用生产者和消费者。例如,关系数据库的连接器可能会捕获对表的所有更改
8.0 消息丢失分析
一般而言,消息丢失的三个阶段:
生产消息
如果出现了网络不可用、消息本身不合格等原因导致消息根本没有被 Broker 接收,那就相当于消息在生产者端就消失了。
存储消息
Broker 端的消息丢失,一般是由 Broker 服务不可用造成的,例如 Broker 都宕机了导致消息丢失
消费消息
消费者在消费消息的过程中,会同时更新消费者位移,也就是「已经消费到哪一条消息了」。这里就存在一个问题,当消费一个消息的时候,是先处理消息,成功后再更新位移,还是先更新位移,再处理消息。
如果先更新位移,在处理消息,当消息处理出现问题,或者更新完位移、消息还未处理,消费者出现宕机等问题的时候,消息就会丢失。
而如果先处理消息再更新位移,虽然可能会出现重复消费同一个消息的问题,但是,我们可以通过消费者处理逻辑实现幂等的方式来解决。
解决方案:
producer 生产消息
ack 机制
生产者 acks参数指定了必须要有多少个分区副本收到消息,生产者才认为该消息是写入成功的,这个参数对于消息是否丢失起着重要作用。
ack 策略
现在我们已经知道生产者发送消息有个确认的机制,那么Kafka里是何时确认呢?Kafka是通过配置acks的值确认机制的,这里一共提供了三种策略,对应不同的ACK机制:
acks=0,生产者不等待broker的响应。这种情况下延迟最低,但是有可能丢失数据,比较适合高吞吐量、接受消息丢失的场景。
acks=1,生产者发送消息等待broker的响应,等待leader落盘成功后响应确认。这种情况下,如果是在leader完成同步消息给follower前发生故障,则可能发生消息丢失。
acks=-1,生产者发送消息等待broker的响应,直到leader和follower全部落盘成功后才会响应确认。此机制能严格保证不丢失数据。但当所有的follower同步完成之后,leader发送ack响应之前,leader发生了宕机,此时生产者会以为发送失败了,然后会重新发送数据给新的leader,因此该情况下会导致数据重复发送。
broker存储消息
存储消息阶段需要在消息刷盘之后再给生产者响应,假设消息写入缓存中就返回响应,那么机器突然断电这消息就没了,而生产者以为已经发送成功了。
如果
Broker
是集群部署,有多副本机制,即消息不仅仅要写入当前,还需要写入副本机中。那配置成至少写入两台机子后再给生产者响应。这样基本上就能保证存储的可靠了。所以broker 消息存储主要是靠的是冗余副本,即多个Replica。ISR机制 和 AR机制
简单来说,分区中的所有副本统称为
AR
(Assigned Replicas)。所有与leader副本保持一定程度同步的副本(包括leader副本在内)组成ISR
(In Sync Replicas)。 ISR 集合是 AR 集合的一个子集。消息会先发送到leader副本,然后follower副本才能从leader中拉取消息进行同步。同步期间,follow副本相对于leader副本而言会有一定程度的滞后。前面所说的 ”一定程度同步“ 是指可忍受的滞后范围,这个范围可以通过参数进行配置。于leader副本同步滞后过多的副本(不包括leader副本)将组成OSR
(Out-of-Sync Replied)由此可见,AR = ISR + OSR。正常情况下,所有的follower副本都应该与leader 副本保持 一定程度的同步,即AR=ISR,OSR集合为空。leader副本负责维护和跟踪 集合中所有follower副本的滞后状态,当follower副本落后太多或失效时,即follower长时间未向leader发送消息,leader副本会把它从 ISR 集合中剔除。如果 集合中所有follower副本“追上”了leader副本,那么leader副本会把它从 OSR 集合转移至 ISR 集合【副本可以在OSP,ISR中来回移动】。默认情况下,当leader副本发生故障时,只有在 ISR 集合中的follower副本才有资格被选举为新的leader,而在 OSR 集合中的副本则没有任何机会(不过这个可以通过配置来改变)。
broker恢复机制
LEO:(Log End Offset)每个副本的最后一个offset
HW:(High Watermark)高水位,指的是消费者能见到的最大的 offset, ISR 队列中最小的 LEO。可以理解为短板效应
follower 故障:follower 发生故障后会被临时踢出 ISR,待该 follower 恢复后, follower 会读取本地磁盘记录的上次的 HW,并将 log 文件高于 HW 的部分截取掉,从 HW 开始向 leader 进行同步。等该 follower 的 LEO 大于等于该 Partition 的 HW,即 follower 追上 leader 之后,就可以重新加入 ISR 了。
leader 故障:leader 发生故障之后,会从 ISR 中选出一个新的 leader,之后,为保证多个副本之间的数据一致性, 其余的 follower 会先将各自的 log 文件高于 HW 的部分截掉,然后从新的 leader同步数据。
comsumer消费消息
消费者拿到消息之后直接存入内存队列中就直接返回给消费成功,这样其实是不算消息消费成功的。我们需要考虑消息放在内存之后消费者就宕机了怎么办,若直接设置为消费成功,当前情况下本条消息相当于丢失了。
所以我们应该在消费者真正执行完业务逻辑之后,再发送给消费成功,这才是真正的消费了。
如何保证消息有序
全局有序和局部有序
全局有序
如果要保证消息全局有序,首先只能由一个生产者往Topic发送消息,并且一个Topic内部只能有一个分区(partition)。消费者也必须是单线程的消费数据。这样消息才会是全局有序的。
不过一般情况下,我们不需要全局有序。
局部有序
绝大多数的需求的有序性的要求都是局部有序,局部有序我们就可以将Topic内部划分成我们需要的分区数,把消息通过分区策略发往固定的分区中。每个partition对应一个单线程处理的消费者,这样既完成了部分有序的需求,又可以通过partition数量的并发来提高消息处理消息。
注意:
每个分区内,每条消息都有offset,所以只能在同一分区内有序,但不同的分区无法做到消息顺序性.
如何保证数据不重复
重复原因
(
Producer
-->) 生产者已经往发送消息了,也收到了消息,并且写入了。当时响应由于网络原因生产者没有收到,然后生产者又重发了一次,此时消息就重复了。( -->
Consumer
)假设我们消费者拿到消息消费了,业务逻辑已经走完了,事务提交了,此时需要更新Consumer offset
了,然后这个消费者挂了,另一个消费者顶上,此时还没更新,于是又拿到刚才那条消息,业务又被执行了一遍。于是消息又重复了解决方案
可以看到正常业务而言消息重复是不可避免的,因此我们只能从另一个角度来解决重复消息的问题。我们如何保证消费重复消息后,最终的结果是一样的。
关键点就是幂等。既然我们不能防止重复消息的产生,那么我们只能在业务上处理重复消息所带来的影响。
什么是幂等性
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
例如这条 SQL
update t1 set money = 150 where id = 1 and money = 100;
执行多少遍money
都是150,这就叫幂等。如何保证幂等
produce – > broke
每个producer会分配一个唯一 的PID,发往同一个broker的消息会附带一个Sequence Number,broker端会对<PID,partitionId,Sequence Number>做一个缓存,当具有相同主键的消息提交时,Kafka只会持久化一条。
注意:
PID 会随着生产者重启而发生变化,并且不同的partition对应的partitionId也不相同。
broke —> comsumer
具体还需要参照业务细节来实现。这里提供一个参考,可以通过上面那条 SQL 一样,做了个前置条件判断,即
money = 100
情况,并且直接修改,更通用的是做个version
即版本号控制,对比消息中的版本号和数据库中的版本号。如何处理消息堆积
堆积原因
消息的堆积往往是因为生产者的生产速度与消费者的消费速度不匹配。有可能是因为消息消费失败反复重试造成的,也有可能就是消费者消费能力弱,渐渐地消息就积压了。
解决方案
阻塞生产者消息
消费速度跟不上,那么阻塞住生产者不就可以了? 但是在使用场景中,业务方的数据是源源不断的,阻塞住很有可能带来损失,一般不采用这种方案。
增加Topic中partition数量
增加消费者数量
消费者数量 < partition的数量, 可以直接增加消费者数量
消费者数量 <= partition的数量,注意队列数一定要增加,不然新增加的消费者
是没东西消费的。一个Topic中,一个partition只会分配给一个消费者。
临时队列
我们可能会遇到这样的一种场景,消费者宕机了好久。等到消费者恢复过来的时候,消息已经堆积成山了。如果还按照以前的速度来进行消费,肯定是不能满足需求的。所以这个时候需要提速消费!!
使用 临时队列 是一个不错的选择:
新建一个 Topic,设置为 20 个 Partition
Consumer 不再处理业务逻辑了,只负责搬运,把消息放到临时 Topic 中
这 20 个 Partition 可以有 20个 Consumer 了,它们来处理原来的业务逻辑。
如何保证数据的一致性
数据的高可用性通常采用的是数据冗余的方式来实现的,而强一致性和高可用性相对应。一致性需要保证副本之间的同步。
9.0 kafka为什么快
顺序读写
零拷贝
消息压缩
分批发送
10.0 分区分配策略
⼀个 Consumer Group 中有多个 Consumer,⼀个 Topic 有多个 Partition。不同组间的消费者是相互独立的,相同组内的消费者才会协作,这就必然会涉及到Partition 的分配问题,即确定哪个 Partition 由哪个 Consumer 来消费。
为什么需要分配
方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个 topic又可以有多个 Partition 组成,因此整个集群就可以适应适合的数据了;
可以提高并发,因为可以以 Partition 为单位读写了。
触发时机
当消费者组内消费者发⽣变化时,会触发分区分配策略(⽅法重新分配),在分配完成前,kafka会暂停对外服务。注意为了尽量确保消息的有序执行,一个分区只能对应一个消费者,这也说明消费者的数量不能超过分区的数量。
生产者分区机制
Kafka 对于数据的读写是以
分区
为粒度的,分区可以分布在多个主机(Broker)中,这样每个节点能够实现独立的数据写入和读取,并且能够通过增加新的节点来增加 Kafka 集群的吞吐量,通过分区部署在多个 Broker 来实现负载均衡
的效果。由于消息是存在主题(topic)的分区(partition)中的,所以当 Producer 生产者发送产生一条消息发给 topic 的时候,你如何判断这条消息会存在哪个分区中呢? 分区策略就是用来解决这个问题的。
kafka分区策略
Kafka 的分区策略指的就是将生产者的消息发送到哪个分区的算法。
具体方法:
1、指定分区
指明partition时,直接将该值作为partition值。
2、随机轮询
3、按key存储
将key的hash值与该topic下可用的分区数取余得到partition值。
4、顺序轮询(round-robin)
若既未指明partition,也没有key时,在第一次调用时随机生成一个整数(后续每次调用都会在这个整数上自增),将该整数与topic下可用的分区数取余得到partition值,也就是常说的
round-robin
算法。
消费者如何与patition匹配
Rebalance消费者再平衡机制
再平衡:在kafka消费者所订阅的topic发生变化时,发生分区的从新分配机制。一般有三种情况,会触发再平衡。
consumer group中的新增或删除某个consumer,导致其所消费的分区需要分配到组内其他的consumer上;
consumer订阅的topic发生变化,比如订阅的topic采用的是正则表达式的形式,如
test-*
此时如果有一个新建了一个topictest-user
,那么这个topic的所有分区也是会自动分配给当前的consumer的,此时就会发生再平衡;consumer所订阅的topic发生了新增分区的行为,那么新增的分区就会分配给当前的consumer,此时就会触发再平衡。
再平衡的策略,主要有三种:
Round Robin
,Range
默认为Range和Sticky
Round Robin
其主要采用的是一种轮询的方式分配所有的分区。
环境说明:
首先假设有三个topic:t0、t1和t2,这三个topic拥有的分区数分别为1、2和3,那么总共有六个分区,这六个分区分别为:t0-0、t1-0、t1-1、t2-0、t2-1和t2-2。这里假设我们有三个consumer:C0、C1和C2,它们订阅情况为:C0订阅t0,C1订阅t0和t1,C2订阅t0、t1和t2。那么这些分区的分配步骤如下:
首先将所有的partition和consumer按照字典序进行排序,所谓的字典序,就是按照其名称的字符串顺序,那么上面的六个分区和三个consumer排序之后分别为:
然后依次以按顺序轮询的方式将这六个分区分配给三个consumer,如果当前consumer没有订阅当前分区所在的topic,则轮询的判断下一个consumer.
分配过程:
尝试将t0-0分配给C0,由于C0订阅了t0,因而可以分配成功;
尝试将t1-0分配给C1,由于C1订阅了t1,因而可以分配成功;
尝试将t1-1分配给C2,由于C2订阅了t1,因而可以分配成功;
尝试将t2-0分配给C0,由于C0没有订阅t2,因而会轮询下一个consumer;
尝试将t2-0分配给C1,由于C1没有订阅t2,因而会轮询下一个consumer;
尝试将t2-0分配给C2,由于C2订阅了t2,因而可以分配成功;
同理由于t2-1和t2-2所在的topic都没有被C0和C1所订阅,因而都不会分配成功,最终都会分配给C2。
按照上述的步骤将所有的分区都分配完毕之后,最终分区的订阅情况如下:
优缺点:
轮询的方式会导致每个consumer所承载的分区数量不一致,从而导致各个consumer压力不均一
Range
所谓的Range重分配策略,就是首先会计算各个consumer将会承载的分区数量,然后将指定数量的分区分配给该consumer。
环境说明:
们假设有两个consumer:C0和C1,两个topic:t0和t1,这两个topic分别都有三个分区,那么总共的分区有六个:t0-0、t0-1、t0-2、t1-0、t1-1和t1-2。那么Range分配策略将会按照如下步骤进行分区的分配:
需要注意的是,Range策略是按照topic依次进行分配的,比如我们以t0进行讲解,其首先会获取t0的所有分区:t0-0、t0-1和t0-2,以及所有订阅了该topic的consumer:C0和C1,并且会将这些分区和consumer按照字典序进行排序;
然后按照平均分配的方式计算每个consumer会得到多少个分区,如果没有除尽,则会将多出来的分区依次计算到前面几个consumer。比如这里是三个分区和两个consumer,那么每个consumer至少会得到1个分区,而3除以2后还余1,那么就会将多余的部分依次算到前面几个consumer,也就是这里的1会分配给第一个consumer,总结来说,那么C0将会从第0个分区开始,分配2个分区,而C1将会从第2个分区开始,分配1个分区;
同理,按照上面的步骤依次进行后面的topic的分配。
最终上面六个分区的分配情况如下:
优缺点
分区方式进行分配,其本质上是依次遍历每个topic,然后将这些topic的分区按照其所订阅的consumer数量进行平均的范围分配。这种方式从计算原理上就会导致排序在前面的consumer分配到更多的分区,从而导致各个consumer的压力不均衡
Sticky
策略是新版本中新增的策略,顾名思义,这种策略会保证再分配时已经分配过的分区尽量保证其能够继续由当前正在消费的consumer继续消费,当然,前提是每个consumer所分配的分区数量都大致相同,这样能够保证每个consumer消费压力比较均衡。
环境说明:
我们假设有三个consumer:C0、C1和C2,三个topic:t0、t1和t2,这三个topic分别有1、2和3个分区,那么总共的分区为:t0-0、t1-0、t1-1、t2-0、t2-1和t2-2。关于订阅情况,这里C0订阅了t0,C1订阅了t0和1,C2则订阅了t0、t1和t2。这里的分区分配规则如下:
首先将所有的分区进行排序,排序方式为:首先按照当前分区所分配的consumer数量从低到高进行排序,如果consumer数量相同,则按照分区的字典序进行排序。这里六个分区由于所在的topic的订阅情况各不相同,因而其排序结果如下:
然后将所有的consumer进行排序,其排序方式为:首先按照当前consumer已经分配的分区数量有小到大排序,如果两个consumer分配的分区数量相同,则会按照其名称的字典序进行排序。由于初始时,这三个consumer都没。
然后将各个分区依次遍历分配给各个consumer,首先需要注意的是,这里的遍历并不是C0分配完了再分配给C1,而是每次分配分区的时候都整个的对所有的consumer从头开始遍历分配,如果当前consumer没有订阅当前分区,则会遍历下一个consumer。然后需要注意的是,在整个分配的过程中,各个consumer所分配的分区数是动态变化的,而这种变化是会体现在各个consumer的排序上的,比如初始时C0是排在第一个的,此时如果分配了一个分区给C0,那么C0就会排到最后,因为其拥有的分区数是最多的,即始终按照所含分区数量从小到大排序。上面的六个分区整体的分配流程如下:
首先将t2-0尝试分配给C0,由于C0没有订阅t2,因而分配不成功,继续轮询下一个consumer;
然后将t2-0尝试分配给C1,由于C1没有订阅t2,因而分配不成功,继续轮询下一个consumer;
接着将t2-0尝试分配给C2,由于C2订阅了t2,因而分配成功,此时由于C2分配的分区数发生变化,各个consumer变更后的排序结果为:
接下来的t2-1和t2-2,由于也只有C2订阅了t2,因而其最终还是会分配给C2,最终在t2-0、t2-1和t2-2分配完之后,各个consumer的排序以及其分区分配情况如下:
接着继续分配t1-0,首先尝试将其分配给C0,由于C0没有订阅t1,因而分配不成功,继续轮询下一个consumer;
然后尝试将t1-0分配给C1,由于C1订阅了t1,因而分配成功,此时各个。
同理,接下来会分配t1-1,虽然C1和C2都订阅了t1,但是由于C1排在C2前面,因而该分区会分配给C1,即:
优缺点:
上面的分配过程中,需要始终注意的是,虽然示例中的consumer顺序始终没有变化,但这是由于各个分区分配之后正好每个consumer所分配的分区数量的排序结果与初始状态一致。这里读者也可以比较一下这种分配方式与前面讲解的进行对比,可以很明显的发现,重分配策略分配得更加均匀一些。
11、kafka的比较
1、与API对比
与其说是与API对比,不如说是MQ与API对比。
其实对于API来说,如果是比较固定的,那么使用API没有问题,也不需要使用MQ。而使用MQ,其实就是因为API不固定,后续可能出现API调用的频繁变更,包括接口的变化,传入数据和传出数据的不固定。此外,API只能够做到一对一,不能够做到一对多,也是给调用端带来问题的情况。通过MQ的方式,可以扩展这样的模式,尤其是前后端的分离系统,采用MQ作为通道,是非常灵活的方案。
此外,对于API的调用而言,都是要求实时性非常高的情况下,才会使用。不过这种情况一般不多。
对于API,会导致前端系统的紧耦合,此时如果前端的调用比较大的情况下, 后端API的压力其实还是很大的。此时会导致不返回,等等这样的状况,导致前端假死。
另外,对于API的扩容也是个问题,前端需要调用不同地址的API,这样还不如消息,扩容的API自己去获取指令,这样效率更高,安全性更好。
消息队列,这种方式,也可以作为使用API的补充,就是通过消息发送命令,具体执行还是通过API,但是存在的问题仍然存在。从整体上来看,效果更加不好,没有发挥出全面的解耦特性。
2、与传统MQ对比
RabbitMQ和Kafka通常是比较的大部分情况
RabbitMQ,单机高效,但是消息会错顺序,消息会丢失
Kafka,分布式,消息不错顺序,消息不丢失
一般情况下,kafka在大数据基本上肯定用,成为事实标准。吞吐量100万,效率太高了。
3、与邮件对比
类比来说,kafka是一个邮箱,生产者是发送邮件的人,消费者是接收邮件的人,Kafka是用来存东西的,只不过它提供了一些处理邮件的机制。
其实kafka更像是邮箱系统。
通常在邮件系统中,kafka的具体应用如下:
设计目的在于将主干流程和枝干流程进行分离。