消息队列:将生产者产出的消息放到缓冲区中,消费者从缓冲区中得到数据
- 应用场景:
- 异步:发短信
- 消峰:秒杀(从缓冲区中取多用户提交的数据)
- 解耦:flume中source和sick(用channel缓冲)
- 模式:
- 点对点(一对一):一个生产者生产的消息队列只能被一个消费者消费,消息队列不会被持久化(消费者收到消息后这条消息就从队列中删除)
- 发布/订阅(一对多):一个生产者生产的消息队列可以被多个消费者订阅消费,消息队列的消息会被持久化,默认保存一周
kafka在大数据中的应用场景:
- tomcat(logs)->flume(发送数据)->kafka -> spark streaming(分析数据)
- 解释:flume是主动发送数据,不断的发送,那么在spark streaming处理数据时的接收量要用kafka进行控制,否则flume发送的数据量过大产生问题,所以kafka是对flume发送的数据进行缓冲的作用,然后发送个下一阶段进行处理
架构:
- producer生产消息即为topic,topic进行分区,好处是并行处理
- 每个分区在一个broker上,即节点,每个分区是一个有序队列
- 每个broker上和producer联系的那个分区即为leader,每个leader都会有副本follower,且这个follower和leader在不同的节点上,leader挂了follower变成leader进行工作
- 副本数=leader数+follower数,且副本数不能大于节点数
- 分区将数据发送给消费者组,消费者组中有多个消费者,一个分区数据只能发送给一个消费者组中的一个消费者,但是一个分区数据可以发送给多个消费者组中的某个消费者
topic创建流程:
- client创建topic时会在zookeeper上创建一个节点,kafka上有个controller监控zookeeper上的这个节点,当节点发生变化时这个controller就能知道
- topic是缓存在kafka中一个metaCache的地方,metaCache在kafka每个节点都有一个
工作流程
- producer创建topic后,在kafka集群即每个broker上创建分区的leader和follower,producer给每个分区的leader发送数据
- 数据发送是批量发送,leader接收到数据后,follower去leader同步数据
- 消费者从分区leader消费数据
- 每个分区是一个有序队列,都有自己的offset,消费者消费数据的时候会记录消费的那个分区的offset,当消费者挂了重启时就会按照这个offset重新读取数据
- 这个offset的维护(即记录)是要消费者自己维护的,0.9版本前,offset是放到zookeeper上维护的,需要指定,这样问题是topic过多(即分区过多),那么zookeeper就会过载,0.9以后offset保存在kafka内置的一个topic中,叫__consumer__offset-*
- 消费者组中对topic的消费就是对每个分区的消费,那么对topic每个分区的消费的顺序是不能保证的,即全局顺序无法保证,只能保证每个分区中的有序,即分区有序
文件存储机制:
- 文件即topic,每个topic分成多个分区,每个分区中以log日志保存数据
- log日志又分成多个segment,一开始log下只有一个segment,当追加的数据超过设置的每个segment大小时就会生成新的segment,默认保存一周时间
- segment的存在形式是.log文件和.index文件,这样一组文件组成一个segment
log文件和index文件:
- 每个segment中的log名字是以这个segment的第一条消息的offset命名的,存储的是消息数据,且每个数据按照offset排列
- index文件命名通log文件,两列内容,一是当前segment的有序索引,二是每条消息对应的起始磁盘物理偏移量,即log文件中的数据起始偏移量
- 每个log和index属于一个segment,每个segment属于一个分区,每个分区是有序队列,用offset记录,每条消息的就是一个offset,所以保证不会重复
- 当offset=9时,先去找这个offset在哪个segment,比如每6个offset是一个segment,那么offset=9就在第二个segment,这个segment的名字中的offset是6,用9-6=3,定位第二个segment的索引3,然后对应的磁盘物理偏移量就是segment2的log文件中的数据位置
分区策略(生产者):
- 分区原因:
- 方便集群中扩展,只能增加不能减少
- 提高并发,提高吞吐量
- 分区原则:按照分区发送数据,数据封装成producerRecord对象,需要和数据有关的属性(topic名字,value值,key用来分区,指定分区等)
- 指明分区就按照指定的分
- 没有指定分区但有key,就按照key的hash值和topic的分区数取余得到的结果,将数据发到这个分区中
- 也没有key时,会随机生成一个整数(以后自增),这个数和分区数取余得到的数即为数据发往的分区数,即round-robin
- 默认round-robin模式
数据可靠性保证:保证数据从生产者到kafka,再从kafka到消费者的过程中不丢失
- 生产者往分区的leader(kafka集群broker)发送消息,分区收到数据后给生产者响应收到(ack)即完成,若没有响应收到,生产者重新发送消息
- leader收到消息后,follower会和leader同步消息
- ack发送时间:
- leader收到消息后
- follower和leader同步消息后:数据已备份,可以保证leader挂掉后,能重新选举出leader,数据不会丢失。follower同步两种模式:
- follower半数以上同步后:快,但需要2n+1的副本容错
- follower全部同步后:慢,但需要n+1的副本容错
- 结论:kafka存的是真实的数据,多一个副本就多存一份数据,大量数据时副本数多必会加大存储空间,及增大数据冗余,另外kafka高效读写数据(顺序写,零复制)技术可以加快速度,所以用第二种
- 说明:kafka的ack模式有三种,以上结论说的是第三种
- 当ack为-1时,leader要等所有follower返回信息后才会给producer发送ack,那么若有个follower挂掉或数据量很大迟迟没有给leader返回信息怎么办?(ISR动态集合中的follower同步完数据就发送ack)
- ISR:
- 用于leader长时间等待follower同步数据的问题和重新选举leader。
- ISR是一个动态保存follower的集合,每个分区中都有一个,leader是和ISR中的follower进行交互的,比如选举新leader。
- 若follower因某种原因同步时间过长,ISR就将这个follower踢出ISR,参数是一个时间参数replica.lag.time.max.ms,旧版本还有个落后条数的参数,但不合理,因为若是数据量太大,此follower就会产生落后条数过大的情况,当踢出后恢复重新进入ISR,还有可能再被踢出,这样反复消耗资源。
- 踢出的follower是临时踢出,当follower恢复并和leader同步完数据后,此follower将再次进入ISR
- 以上只是保证数据的不会丢失,并没有保证数据不会重复
- ack三种模式:
- acks=1:生产者发送消息,leader接收到就发送ack,若follower没同步完leader挂掉,重新选出的leader并没有新数据,造成数据丢失
- acks=0:生产者发送消息即获得ack,数据丢失
- acks=-1(all):follower都同步完成后,发送ack之前leader挂掉,会造成producer重新发送数据,数据重复
- 出现故障后kafka数据如何处理(即保持一致):
- LEO:leader或follower最后的offset,副本各自不会相同
- HW:所有leader和follower中,那个Leo和hw的offset相等的位置即为所有follower和leader共同的HW,每个副本的HW一样
- 所有follower和leader通信,告诉leader自己的LEO,leader选出最小的Leo即为HW,然后将这个HW发给其他follower,HW存在各自磁盘上
- follower故障:被踢出ISR恢复后,去和leader同步,同步位置以自己记录的HW为准,HW后面到Leo的部分截取调,当从HW位置同步到leader的Leo后,即完成与leader的同步,重新进入ISR
- leader故障:选举出新的leader,其余follower将从自己的HW位置去掉后面的数据,然后和新leader同步
- 以上只保证数据的一致性,不保证不重复或不丢失,数据的丢失或者重复时靠ack决定的
- 消息传递可靠性语义:
- at most once:至多有一次,可能丢失
- at least once:至少有一次,可能重复
- exactly once:有且只有一次。0.11版本后,加入幂等性机制(只序列化一次),配合acks=-1,就可以做到有且只有一次。将一个属性enable.idempotence设置为true,不用设置acks=-1了
- 幂等性机制:给消息加上唯一id。producerId+topicId,此消息写入kafka时将此id缓存,当acks=-1且没有收到ack时producer会再次发送消息,那么会再次将这个id带上发送到kafka,kafka从缓存中见到同样的id就不会保存
消费者:
- 消费方式:
- kafka是拉的方式:可以根据消费者消费能力消费消息。当kafka没有消息时,会循环拉取数据,针对这点,消费者消费数据时会传入一个时长参数timeout,若没有数据会等待timeout时间再返回,默认100毫秒
- push方式:不考虑消费者消费速率,只是最快传递消息
- 分区分配原则:将分区分配给消费者的原则
- round-robin:轮寻方式将分区分给消费者
- range:默认方式。多个topic时,每个消费者所分配到的分区会不均
- stick:0.11版后
高效读写数据:会持久化磁盘
- 顺序写:producer生产数据写入log时,是将数据追加的文件末尾,即顺序写。这样写省去磁头寻址时间,速度非常快
- 零复制技术:传统读取发送文件过程为 文件->内核->用户->内核->发送。kafka发送文件方式 文件->内核->发送,少去了和用户层的交互,因为生产者生产的数据就复合网络发送的协议,不需要用户层的解码等处理
zookeeper在kafka中的作用:
- kafka中的broker会有一个作为controller,负责管理所有broker和分区副本分配及leader选举等
- zookeeper辅助这个controller去完成以上工作
- 启动kafka时,每个broker上都会有个controller实例,但只有先到达zookeeper注册成功的那个才会成为最终唯一的那个controller
- 举例:leader选举流程(leader第一次是分配的,manager中preferred那个就是)
- kafka集群启动时会选举出kafkaController,并且在zookeeper的/brokers/ids上注册每个broker的临时节点(即broker挂了节点id就没了),kafkaController会监听这个临时节点
- 生产者创建一个topic,会在zookeeper上的/brokers/topics/topicName/partitions/index/state注册,注册内容会有leader在那个broker上,和ISR信息
- 当leader挂了后,zookeeper上的临时节点中的id也会消失,监控这个临时节点的kafkaController就会知道,然后从topic在zookeeper上的注册信息中得到ISR信息,并按照顺序将下一个brokerid上的副本编程leader,并更新这个注册信息
- kafkaController是通过每个broker上的metaCache得知broker上的leader信息的
- 当kafkaController挂掉后,其它broker上的Controller会再去zookeeper注册,谁先注册上谁就成为新的kafkaController
producer发送消息流程:
- Producer->send(ProducerRecord)->Interceptors->Serialier->Partitioner->RecordAccumulator->Sender
- 分区器往RecordAccumulator上批量发送数据,根据大小batch.size和时间linger.ms批量发
Producer API:主要是发送消息,同步发送,异步发送,新版本的同步发送是在异步发送中调用future.get()方法实现的
Consumer API:主要是对offset的手动和自动维护。offset可以放到外部数据库中,这样做的目的是保证offset原子性(即完成数据的消费和offset的提交在一个事务中)
- 同步提交会一致提交offset,直到成功为止
- 异步提交只提交一次offset,使用异步提交,一批提交一次offset,若这次没成功下次提交可以带上上一批提交
数据消费重复问题:
比如已经消费了0、1、2,consumer记录的offset是3,那么再消费3、4、5,此时没有提交offset就挂了,当再次通过offset=3读取数据是3、4、5,此时读的数据就重复了。此时就需要保证数据的消费和offset的提交再一个事务中,这样才能保证数据的原子性,即exactly once
重新分配分区:
消费者订阅topic方法中可以传递重新分配分区的属性,里面两个方法,一个是回收的分区,一个是重新分配的分区,当有第二个消费者消费同一个topic且属于同一个消费者组时,就会将所有分区进行回收和重新分配
- 那么就要在这两个方法中,回收分区的方法中将offset进行提交,因为自定义了commit方法,是实时提交的,所以这个方法里不用再写offset提交代码了
- 在重新分配的方法中定位新分配的分区的offset:自定义方法获取offset,调用consumer.seek()方法定位offset。自定义commit方法
- 最终保证消息的消费和commit提交在一个事务里就保证了exactly once
拦截器:
- 有两个方法:
- onSend:在producer的send方法中调用,运行在主线程,在序列化和分区前调用
- onAcknowledgement:在producer的callback调用前调用,或者消息成功或失败发送到broker之后调用,运行在IO线程中,会拖慢速度
- 以上两个方法运行在不同的线程中,但有共享变量或资源时会产生线程安全问题
- 拦截器可以对数据进行简单的预处理,发送前处理和回调前处理下
kafka manager:
- preferred 副本election:初始化kafka时均匀分配的leader分区计划(当leader挂掉会造成leader分配不均,那么这个按钮会按照初始化时的均匀分配进行重新分配,达到再次平均分配leader所在分区的目的)
- reassign partitions:重新分配分区
- 数据倾斜或者某个分区数据很大,其它分区很小时才去重新分配分区,重新分区时数据会大量迁移
kafka项目中应用:
- 通过flume将监控的文件传给kafka,也就是flume的source是某个地方的文件,sink是kafka
- 在这个kafka sink中要指定kafka的topic,那么flume就会把文件传给这个topic的kafka了
- 在kafka创建(或已经有)这个topic,然后打开kafka监控,再然后只要这个flume运行,kafka就会自动监控这个文件了,因为flume已经把这个文件传给kafka了。