kafka是一个高吞吐,分布式,基于发布/订阅的消息系统,最大的特性就是可以实时的处理大量的数据以满足各种需求场景:日志收集,离线和在线的消息消费,等等
topic 主题:kafka根据topic对消息进行分类,发布到kafka上的每一条消息都要指定一个topic
producer 生产者: 向kafka主题发布消息的客户端
consumer 消费者: 订阅topic主题,读取消息的客户端
broker : 消息处理中间件,在kafka集群上,一个服务器就是一个broker
partition 分区: 为了实现拓展性,一个大的topic可以分布在多个broker上,也就是一个topic分为多个partition,每个partition的内部消息有序的
一些其他的定义:
consumer group:消费者组 ,多个consumer组成,消费者组中每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费,消费者组之间的消费互不影响。
replica 副本:提升可用性,为每个partition增加若干个副本,分布在不同的broker上,避免某个broker不可用的时候,读不到消息
leader : 多个副本的主,生产者只会往leader上发送数据,消费者也只会从leader上读取数据,
follower: 多个副本的从,从leader中同步数据,当leader发生故障时,某个follower会成为新的leader。
offset 偏移量: 可以唯一的标识一条消息,消费者通过控制偏移量来决定下次去读的消息,消息被消费后,不会立刻被删除,这样,多个业务就可以重复的消费kafka的消息。
缓冲和削峰:某一时候,上游数据有突发流量,下游服务器没有足够的性能来保证性能,kafka在中间就可以起到一个缓冲的作用,把消息暂存在kafka中,即便某一时刻数据激增,下游的服务器也可以按照自己的节奏慢慢处理。
解耦增加拓展性:消息队列可以作为一个接口层,解耦业务流程(kafka想象成一个菜鸟驿站?)
一对多:一个生产者发布的消息,可以被多个消费者消费,供一些没有关联的业务同时使用
增强健壮性:消息队列可以堆积请求,因此即便消费端业务短时间内挂掉,也不会影响主要业务
异步通信:消息队列提供了异步处理机制,允许用户将一些消息放入队列,然后在合适的时间去处理。
数据传输的事务定义有三种级别:
1、最多一次,消息不回重复发送,最多被传输一次,但也有可能一次不传输
2、最少一次,消息不会漏发,但是可能会被重复传输
3、精确的一次(exactly one):不会漏传,但是也不会重复传输,是大家所期望的。
2.8.0一起,kafka是依赖zk的,因此判断是否存活的条件:
1、节点可以维护和zk的链接,这个是zk通过心跳机制来检查
2、如果节点是个follower,必须能够及时的同步leader的写操作,延时不能太久。
2.8.0版本以后,kafka移除了zk: 待补充
producer将消息推送到broker,consumer从broker上拉取消息。
好处是consumer可以自主决定从broker上拉取数据的速率。缺点是如果broker中没有可以供消费的消息,consumer就会不断的轮询,为了避免这点,kafka有个参数可以让consumer阻塞,直到消息到达。
ISR:In-Sync Replicas 副本同步队列
AR: Assigned Replicas 已分配的副本,即所有副本
OSR:outof-Sync Replicas 表示follower与leader副本同步的时候,延迟过多的副本
ISR是由leader进行维护,follower从leader上同步数据会有一定的延迟,如果follower长时间未向leader发送通信请求同步数据,(延迟时间replica.lag.time.max.ms参数设定,默认30s)就会把follower从ISR中剔除,存入OSR列表,新加入的follower也会存在OSR中,即 AR = ISR + OSR
kafka的broker启动后首先在在zk上注册controller节点,利用zk的强一直性,一个节点只能被一个客户端创建,该节点中写入当前broker的信息,创建成功的controller来决定leader的选举
选举出来的controller会监听集群broker节点的变化,然后决定选举leader:
partition的leader选举规则是:在ISR中存活为前提,按照AR中排在前面的优先。例如:ISR【1,0,2】,AR【1,0,2】,那么leader就会按照1,0,2轮询,此时leader就是broker1,
controller此时就会将节点信息上传到ZK,其他controller去ZK上同步节点的信息
假设某一时刻broker1 挂了,controller监听到节点的变化,就会更新ISR, 选举新的leader,然后将信息同步到ZK上
LEO(Log End Offset):每个副本的最后一个offset,LEO其实就是最新的offset + 1
。HW(High Watermark):所有副本中最小的LEO
。
follower故障处理(如图):
leader故障:
可以看出,故障处理只能保证副本之间数据的一致性,但是不能保证数据不丢失,或者不重复。
topic是逻辑上的概念,partition是物理上的概念。
一个topic可以分成多个partition,每个partition对应一个log文件,log文件中存储的就是生产者生产的数据,每次producer生产新的数据,就会追加到log文件末端。
为了保证定位效率,kafka采用了分片和索引的机制:即每个partition又分为多个segment,每个segment包括 .log , .index,.timeindex等文件,一个segment默认是1G,超过1G就会生成一个新的segment。
.log 文件,记录生产信息
.index 记录偏移量,用于快速定位 (index是稀疏索引,每往log日志中记录4Kb数据,会记录一条索引,index文件中记录的offset为相对offset,参数log.index.interval,bytes=4kb, )
.timeindex 记录时间信息,kafka默认是数据保留7天,超过的会清理
kafka默认是数据保留7天,可以通过如下参数修改保存时间:
日志一旦超过了设置的保留时间,将怎么清理呢?
1、delete删除:默认值。就是将过期日志直接删除:log.cleanup.policy = delete
以segment中所有记录中最大的时间戳作为该文件的时间戳。因此,如果一个segment中一部分数据过期,一部分数据没过期,那么是不会删除的。
2、compact 压缩:log.cleanup.policy = compact,相同key的不同value值,只保留最后一个版本(压缩后的offset不一定是连续的,只适用于特殊场景,如消息的key也是实际数据的key,一般不用。)
因为生产者和消费者操作的都是leader partition,如果集群出现了leader partition不平衡,就会导致broker压力太大。
一般情况下,kafka本身会自动把leader均匀分散在各个机器上,来保证每台机器的吞吐量都是均匀的,但是,如果某些broker宕机,leader重新选举,就可能导致leader partition过于集中在少部分broker上,这样一来,少数几台broker读写请求压力过高,造成了集群的负载不均衡。
auto.leader.rebalance.enable,默认是true,说明自动启用leader 再平衡
leader.imbalance.per.broker.percentage,默认是10%,每个broker允许的不平衡的leader的比率
。如果某个broker超过了这个值,控制器会触发leader的平衡
leader.imbalance.check.interval.seconds,默认值300秒。检查leader负载是否平衡的间隔时间。
针对broker3节点,分区3的AR优先副本是3节点,但是3节点却不是Leader节点,所以不平衡数加1,AR副本总数是4
所以broker3节点不平衡率为1/4>10%,需要再平衡。
broker2和broker3节点不平衡率一样,需要再平衡。
broker0和broker1的不平衡数为0,不需要再平衡。
生产环境中,建议不要将自动再平衡打开,即便打开,也要将再平衡因子设置的大一些。
分区好处:
1、便于合理使用存储资源,每个partition在一个broker上存储,可以将海量数据按照分区切割成一块块数据,存储在多台broker上,合理控制分区的任务,实现负载均衡
2、提高并行度,生产者可以以分区为单位发送数据,消费者也可以以分区问单位消费数据。
生产者发送消息的分区策略:
1、如果指明了partition,直接将数据写入指明的分区
2、如果没有指明partition,但是有key的情况下,将key的hash值与该topic中partition的数量取余,存入对应的分区
3、既没有指定partition,也没有key的情况,kafka会采用粘性分区,即随机选取一个分区,并尽可能的一直使用该分区,如果该分区batch已满或者已经完成,则在随机选一个不同的分区。
在消息发送的过程,涉及到两个线程,一个main线程,一个是sender线程
ack应答原理:支持配置三种参数
15.1:如果ack=-1, 但是有一个follower因为故障迟迟无法同步,这个问题怎么解决?
----还是靠ISR队列,如果follower长时间未向leader发送通信请求或者同步数据,就会被提出ISR,而ack=-1时只需要ISR队列中的所有节点响应即可。
数据完全可靠的条件是(解决数据丢失):ack=-1 + 分区副本大于等于2 + ISR应答最小副本数大于等于2
生产环境中,ack=0的基本很少用,ack=1的一般用于传输普通日志,允许个别数据的丢失,而ack=-1 一般用于对可靠性要求比较高的场景(如和钱有关)
如何解决数据丢失问题?
1、producer到kafka端:保证数据完全可靠,即ack=-1 + 分区副本大于等于2 + ISR应答最小副本数大于等于2
2、consumer消费端:业务端数据处理成功后,手动提交offset
幂等性是指producer不论向broker发送多少次重复数据,broker端都只会持久化一条,保证了数据不重复。
精确一次 = 幂等性 + ack=-1 + 分区副本大于等于2 + ISR应答最小副本数大于等于2
幂等性判断数据重复性的一个标准是: PID + Partition + SeqNumber ,相同的主键消息提交时,broker只会持久化一条。
---- 所以可以知道,幂等性只能保证的是在但分区单会话内不重复,消费端消费的时候,也要利用幂等性原理解决,给每条数据加一个唯一标识,保证数据不会被重复消费。
----kafka开启事务必须要开启幂等性。
为什么会乱序?
kafka的sender线程是先将数据请求放到一个in Flight requests 队列里面,这个队列最大允许放置5个请求,每个请求发送到kafka的broker上时,允许在未响应的前提下发送后一个请求,这就有可能导致了乱序(比如1,2,请求发送成功并应答,发送3的时候没有应答就发送了4,结果3发送失败,4发送成功,这就导致了顺序是1,2,4)
如何有序?
但是需要注意的是,kafka只是保证了单分区内有序,多个分区是不保证的。
为什么多分区有序不保证?
如果kafka保证多个partition内的消息也是有序的,不仅broker保存的数据要有序,消费者消费时的也要按照顺序消费,假设partition1阻塞了,其他分区的消息也不能被消费了,这种情况,kafka就退化成了单一队列,失去了并发性和性能。
有没有办法保证整个topic级别的消息顺序性?
可以在业务层面解决:
但是上述操作其实降低了性能,不如就只创建一个分区。
消费者组:
consumer group:消费者组,由多个consumer组成,形成一个消费者组的条件,是消费者的group id相同。(一个consumer也可以是一个消费者组)
消费者发送消费请求 ,通过sendFetches方法, 做一个抓取数据的初始化,准备一些数据
kafka的消费者再平衡,指kafka consumer订阅的topic发生变化时,一种分区重分配机制。
一般如下三种情况会触发consumer的分区分配策略(再平衡机制):
消费者分区分配策略实现的方法有以下四种机制:
通过consumer配置项partition.assignment.strtegy指定分区分配策略类,kafka可以同时使用多个分区分配策略。
kafka默认就是使用range+CooperativeStucky策略。同时,也支持自定义策略,重写ConsumerPartitionAssignor接口。
RangeAssignor 范围分区分配策略:
解释:是按照单个topic为一个维度来计算分配的,负责将每一个的topic尽可能的均衡分配给其他的消费者
示例:
一个topic有四个分区,消费者组中有三个消费者,那么就先进行排序,计算,发现每个消费者最少一个分区,还多了一个分区,那么就分给consumer1
缺点:
range方法虽然针对单个topic情况下比较均衡,但是如果topic很多,consumer排序靠前的消费者负载会变多。
RoundRobinAssignor 轮询分区策略:
解释:轮询针对的是所有的topic分区,他把所有的partition、所有的consumer列举出来进行排序,然后通过轮询策略分配给每个消费者(如果该消费者没有订阅该主题,就跳到下一个消费者)
示例:
1、如果消费者订阅的主题是一样的:
2、如果消费者订阅的主题不一样:
缺点:
就如示例2的情况,消费者分区分配很不平衡,因此consumer group订阅消息不一致的情况下,不太适用于轮询机制。
StickyAssignor 粘性分区策略:
例如:三个consumers(C0、C1、C2),四个Topics(T0、T1、T2、T3)。
则RoundRobinAssignor和StickyAssignor分区分配方案均为:
C0 T0P0、T1P1、T3P0
C1 T0P1、T2P0、T3P1
C2 T1P0、T2P1
现在,假设C1被移除,将触发分区重分配:
RoundRobinAssignor分区分配方案将变为:
C0 T0P0、T1P0、T2P0、T3P0
C2 T0P1、T1P1、T2P1、T3P1
保留之前的分区分配方案的3个分区不变。
StickyAssignor分区分配方案将变为:
C0 T0P0、T1P1、T3P0、T2P0
C2 T1P0、T2P1、T0P1、T3P1
可以看到StickyAssignor尽量保存之前的分区分配方案,分区重分配变动更小。
CooperativeStickyAssignor策略:
CooperativeStickyAssignor其实也是一种粘性分配策略,但是有一定的区别:
示例:
一个Topic(T0,三个分区),两个consumers(consumer1、consumer2)均订阅Topic(T0)。那么分配完成的订阅信息就是:
consumer1 | T0P0、T0P2 |
---|---|
consumer2 | T0P1 |
此时,如果一个新的consumer3加入消费者组,就会触发再平衡:
基于eager协议的 粘性分区策略:
1、consumer1、 consumer2正常发送心跳信息到Group Coordinator。
2、随着consumer3加入,Group Coordinator收到对应的Join Group请求,Group Coordinator确认有新成员需要加入消费者组。
3、Group Coordinator 通知consumer1和consumer2,需要rebalance(再平衡)了。
4、consumer1和consumer2放弃(revoke)当前各自持有的已有分区,重新发送Join Group请求到Group Coordinator。
5、Group Coordinator依据指定的分区分配策略的处理逻辑,生成新的分区分配方案,然后通过Sync Group请求,将新的分区分配方案发送给consumer1、consumer2、consumer3。
6、所有consumers按照新的分区分配,重新开始消费数据。
基于cooperative协议的粘性分区策略:
1、consumer1、 consumer2正常发送心跳信息到Group Coordinator。
2、随着consumer3加入,Group Coordinator收到对应的Join Group请求,Group Coordinator确认有新成员需要加入消费者组。
3、Group Coordinator 通知consumer1和consumer2,需要rebalance了。
4、consumer1、consumer2通过Join Group请求将已经持有的分区发送给Group Coordinator。注意:并没有放弃(revoke)已有分区。
5、Group Coordinator取消consumer1对分区p2的消费,然后发送sync group请求给consumer1、consumer2。
6、consumer1、consumer2接收到分区分配方案,重新开始消费。至此,一次Rebalance完成。
7、当前p2也没有被消费,再次触发下一轮rebalance,将p2分配给consumer3消费。
可以看到,上述两个协议的区别在于:
- EAGER :重新平衡协议要求消费者在参与重新平衡事件之前始终撤销其拥有的所有分区。因此,它允许完全改组分配。
- COOPERATIVE:协议允许消费者在参与再平衡事件之前保留其当前拥有的分区。分配者不应该立即重新分配任何拥有的分区,而是可以指示消费者需要撤销分区,以便可以在下一次重新平衡事件中将被撤销的分区重新分配给其他消费者。
COOPERATIVE协议将全局重平衡,改成了每次小规模的重平衡,从而达到最终的平衡,这样做的好处就是所选了STW时间。
offset 位移就是consumer记录的已经消费数据的位置。在0.9版本以前是保存在zookeeper中的,从0.9版本之后,默认将offset保存在kafka一个内置的topic日志文件后,该topic名称为:__consumer_offsets
__consumer_offsets 主题里面采用 key 和 value 的方式存储数据:
每隔一段时间,kafka会对这个topic进行压缩,也就是每个key只保留最新数据。
22.1 为什么消费者需要使用50个文件记录消费者的offset呢?
如果消费者比较多,都记录在同一个记录中,那么读写的操作就比较麻烦
22.2 消费者怎么知道应该从哪个日志文件中读取数据?
key%50(文件数量),然后就可以到对应的文件夹中去取值。
当kafka中没有初始偏移量(消费者组第一次消费),或者服务器上没有存在偏移量(数据被删除),应该怎么消费呢?kafka提供了三种消费方式:
在生产环境中,会遇到最近消费的几个小时数据异常,想重新按照时间消费。例如要求按照时间消费前一天的数据,怎么处理?
kafkaConsumer.offsetsForTimes(); 这个API可以将时间转换为offset。
1、如果是kafka消费能力不足,可以考虑增加topic的分区数目,同时增加消费者组的消费者数量,消费者数目= topic的分区数
2、如果是下游数据处理不及时,可以考虑提高每批次拉取消息的数量(同时要注意修改每批次最大拉取大小。)
3、上游消费者也可以提示生产吞吐量来增大整个kafka的吞吐量,如增大发送消息缓冲区的大小以及增大batch.size,避免频繁网络请求