1 kafka消息发送的方式
kafka中每条消息称为record,每个记录在构建时指定自己的主题、分区、key。value,其中key的角色用于计算消息分区在多个计算机节点上的broker物理地理位置,如果在构建消息时不指定分区,默认会自己计算,如果指定分区,kafka不会对分区进行校验,比如分区大于节点数量,则数据没有发送的目标,如果在kafkaProducter构建的配置map中,自定义了分区器会使用它的方法,会让没有分区的消息发送到指定为位置,如果都没有,实行默认计算,会获取集群的数据源,通过key的散列特征值%broker数量,如果某一个分区数量操过16k,会分配到其他分区,并更新分区的负载情况,并根据分区的负载情况,来生成负载权重,在决定消息发送到哪个主机上。
生产者使用缓存来接收repoductRecord的好处是减少网络io,在16k缓存中存消息数据,当满的时候,交给下游服务一次次性发送到kafka中,这些缓存并不是只能缓存16k大小的数据,如果当前缓存16k已经满了,就会被锁住,缓存区未满,也会发送,这种是在生产速率小于消费速率,设置上发送的延迟时间,来等待生产者生成足够多的数据,减少网络io次数,创建新的缓存继续存处理后的待发送的消息,16k是一批次发送的最大数量,kafka大量使用了生产者和消费者模式,一个线程向缓存中放数据另外一个线程取数据,主要好处减少io次数。其中主机数量是在多个节点竞争为leader时controller会监控集群节点i把集群的元素据发送到从节点。
2 kafka集群请求过程和选举过程
每一个kafka启动节点拥有一个zkClient当启动后像zk发送注册请求就集群id写入到zk中,并创建监听器监听是否有临时节点controller,如果没有就会创建controller并选举成为leader节点,创建新的监听器来监听集群id,并把自己的集群元数据发送到左右从节点中,如果主节点挂了,触发controller监听器会删除临时节点,其他节点在启动后也一直在监控,如果没有谁先创建临时controller节点谁竞选为leader节点,并发送消息给所有集群节点。
3 kafka消息的应答模式有三种,一种是生产者将消息发送到网路,就认为消息已经发送,缺点消息会因为网络状态导致消息不能成功抵达broker,该应答模式为0 ,第三种是消息发送到主leader后同步副本日志,并把日志同步到从节点上,然后再应答处理完成,模式为2或-1,第二种性能最佳,当消息到达leader后持久化到日志,不等待从节点的日志同步,直接向生产者应答,模式为acks=1.,如mapConfig.put(ProducerConfig.ACKS_CONFIG,"1"/"all/2"/"0/-1"), mapConfig.put("retries",5) 设置超时冲粉丝5次 ,也可以设置超时时间在1s内实现重试,发现会有大量消息重复,超时时间需要合理的设置。
new KafkaPrpducer(mapConfig)
4 kafka的重试机制 在应答模式为1的时候,将数据发送到网络等待目标主节点写入数据后做应答,如果这时写入失败,在一定时间里没有应答,就会触发超时机制,这时使用尝试机制保证消息不丢失,重试次数是可以配置的 ,像写入失败可以继续尝试写入超出次数后停止,触发超时。
场景leader接收消息后写入日志成功,并向客户端响应,由于网络问题,导致网络超时,触发消息重试机制,但事实消息已经持久化。尝试会导致消息重复。
5 broker批量持久化发来的数据data3、data2、data1 ,在队列中是有序的,在持久化操作时数据消息data1写入日志失败,data2、data3写入成功,data3会触发超时机制,重试发送data1数据,这导致数据data1的处理顺序与之前的比乱序,这对有些场景说是不能接收重试机制导致数据重复、乱序)
6 kafka的幂等操作 防止消息重试的方式就是设置幂等性请求策略来防止消息重复。常见的实现幂等性的方式有1 唯一生成标识在订单提交页面会产生随机id号根据发放的号与该订单做唯一绑定,当结算的时候网页的随机id与后端对象的订单id产生的随机号不匹配就提交失败。设置幂等性操作如下所示。设置幂等性要求 1应答模式必须是-1,2必须开启重试机制 3必须设置在途请求缓存区数量不能大于5,要求同步所有的日志文件后做响应。
幂等性实现原理: 首先在broker中有保存生产者的生产者装填producerState,默认保存5个大小(性能测试得出)在这个状态集合中消息记录以生产者id+数据序列号的形式保存,在一个breoker中每个消息都是编辑好顺序,当接收的消息是无序的,broker就会回退消息到客户端的buffer中,如数据6没有到数据7到了,会退回数据,客户端会按顺序继续尝试发送data7、和data6,当broker的容量满的时候就会等待其他线程的消费者来消费容量中的消息,这时添加新的消息到生产者状态集合就保证顺序性,也能保证数据重试不丢失,实现了数据的生产幂等性,缺点只能保证一个分区下的数据是幂等的其他分区与当前分区的顺序无法控制,生产者id在重启后是随机生成的,下次在发送请求比对的时候出问题即不能跨会话的实现幂等性,跨会话的幂等性需要使用kafka的事务解决。
在前面生产数据是没有问题的现在重启生产者后再生产数据就会发现生产者id由0变成2,导致数据成功发送给broker消息是重复的,解决引入事务配置,事务是为解决幂等性的问题而存在的,必须加幂等性配置才能使用,生产者id重启保持不变在同一个分区幂等性才能生效。
底层实现细节
生产者同一批次生产数据到服务中进行productstate中排序接收,小的生产者id加消息序列号写入并应答,批次检验继续工作剩下的,上一批次没处理完下一次发送来的无序消息会退回在消费者客户端的缓存区继续和上一批次排队,确保整体数据发送的有序性,到了服务中途缓存区继续排队,去重(没应答的在,获取消息持久状态完成应答),幂等性只等保证分区下的消息可靠性,但如果生产者重启后生产者id变化生产的消息就重复,解决使用事务保持生产者重启后生产者id不发生变化,在原有的序列号上接着生产。
7 kafka日志文件的写入(稀疏索引)
kafka先将生产者的消息存放在缓存中,来提供以读优先的性能,再异步的时候不间断地将数据持久到文件,日志文件的写入由logcontroller来完成,数据不光要写入日志中还要考虑到读的需求,于是将持久化数据以数据段的形式保持到跳表中ConcurrentSkipListMap存放结构LogSegment,里面描述了三种文件类型index file、Log File、TimeIndex File 当前跳表有0、4、8索引的logsegment存在跳表中,现在要查索引7的数据在日志片段中的数据,会在跳表中找到比7次小的索引所在的logSegment,然后在index中初始偏移量是4,查到7的偏移量对应的位置是95,就会在LogSegment中的log文件中找到position:95那行的记录,完成定位读操作,二索引文件是单项有序所以使用二分查找法,找到目标索引。
8 kafkalog文件的组成
log文件中存在内容 批次头+数据体组成,批次头占用61字节,数据体大小是每条记录的key值和value的大小,如发送一条数据键key1 值value1总共10字节。批次头61,数据体分为数据项和数据,数据项占用9字节总计61+9-2+10=78,关于log的LEO=log end offset指示当前日志存放的数量,如一条数据偏移量从0-到1 LEO的值是1表明有一条记录。
9 消费者可以指定消费的偏移量 默认从LEO处消费消费的模式有last最后LEO处消费和eariest从头开始消费,还可以指定小主题的消费偏移量方式。
如上图对于test主题指定从偏移量2的地方开始消费(获取消费主题的一些分区信息,遍历分区下的主题匹配就从该分区下设置消费的偏移量seek(所在分区,消费偏移量))。
10 消费者消费主题 保存消费者的偏移量时可能重复消费(不漏消费和重复消费)
auto.commit.interval.ms配置会在指定时间内保存一次消费者的消费偏移量,如果一个消费者在消费上设置5s保存一次,消费到第四秒宕机则当前消费者的消费偏移量处在上一次5s保存的地方,如果重启消费者,可能会重复消费,这是自动设置消费者偏移量的缺点,就是宕机后失去部分记忆,在引入幂等性后保证生产者id一致,宕机重启重新生产防止重复的消息产生,对于消费者进制自动偏移量设置默认打开map.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false),在消费的时候需要手动保存偏移量consumer.commitAsync()/commitsync() 异步提交、同步提交(相同点同意批次的消息的最高偏移量进行提交,同步提交等待提交结果,如果失败就重试,异步提交无需等待,失败无感知,同步提交可靠担是影响性能 更多情况是异步提交 )
同步消费时如果先提交消费偏移量,再执行消费任务此时停机,开机后从提交的偏移量消费真的消息处理事务丢失,即消息丢失,如果先执行消息的任务,再提交该任务的偏移量任务执行时成功,提交时宕机,开机后重新消费信息,导致消息重复消费。此时解决就需要绑定在一起,以原子的方式去执行停机时要么都成功要么都失败,这就需要锁机制,分布式下采用分布式锁完成数据一致操作相当于事务操作,卡夫卡开启事务后生产者的id由kafka的服务器生成,并且每次重启后一致,事务由事务协调器管理。
生产者在send之后并没有把数据提交而是写入缓存,真正把数据提交的是producer.commitTransactions完成真是数据提交
11 kafka默认事务隔离级别
在第10步时如果事务常常写如事务标记会成功,我们可以将事务标记写成终止状态
重启消费者发现任然可以消费到数据,回到日志文件,执行kafka的工具类查看日志
kafka-run-class.bat kafka.tools.DumpLogSegments --files log-path --print-data-log
会发现文件最后第10的偏移量的数据是事务终止标记。
配置隔离级别 map.put(ConsumerConfig.ISOLATION_LEVL_CONFIG,read_committed/read_uncommitted)对于读已经提交即便事务的标记为终止任然消费者可以读到数据,对于读未提交事务标记为终止是可读到数据。
12 消费之消费者组之间的关系
如果上图每一个消费者独立消费分区导致消费者需要对每个分区上都进行请求加上其他消费者建立的连接过多影响性能,就引入消费组。
一个分区的消息之,消费之消费时所以分区上的主题都是可以消费到的
多个生产者生产同一个主题到多个分区,一个消费者消费消息到多个分区上拉取数据,确保多个分区上的数据都被消费,缺点生产者多个,消费者消费每个分区,压力较大对每个分区都要请求连接拉取数据,解决,使用消费者组允许该组内对数据平等共享消费,保证这一主题的消息在该组的功能任务都会被执行,而不是由一个消费者去建立连接而是以一个组的形式,每个消费者可以负责一个分区上的数据消费,这个组消费过的消息,在其他组同样可以消费相同的消息,这些消息是持久化的不会像其他mq消费后就数据清除,这提高了kafka的吞吐量。
一个消费者组的消费者可以消费多个分区的数据,一个分区的数据不能被消费者组的多个消费者消费(同一个消费组的多个消费者不能消费一个分区上的数据)
上面的三个消费者对应三个分区,如果添加一个消费者,则这个消费者不能消费其他分区的数据,它只能作为消费者备份,当其他消费者出现问题时备用消费者就会自动工作,只要消费者组id相同,消费者就是一个整体
测试 集群模式 创建主题为两个分区,创建三个消费者,启动三个消费者,启动生产者,生产数据,发现消费者1消费0分区消费者2消费2分区消费者三没有消费任何数据(同组内顺序消费),
停掉消费者1,消费者2、3消费分别消费分区0、1停掉消费者3消费者2消费分区0、1消息
kafka提供了一个_consumer_offsets主题,它记录组内消费的偏移量,默认分区为50,与事务协调器的主题一样,消费组消息偏移量会存在在任意分区下面,由消费者协调器和broker中消费者组协调器交互工作。最好的一个组内每一个消费一个分区
分区与消费者组内消费者分配策略,一个个加入的为leader,由leader分配1轮询
范围消费c策略分区数%消费者数,剩下的一次补一个。
多个消费者的同主题消费的主题在多个分区上,对消费者策略分配,减少消费者在多个主题不同分区分配的分区数量,这个分配策略由消费协调器完成它是一个map结构,键为主题值为list集合,集合里面存消费者被分配的消费主题所在哪些分区,分配触发策略,每当有消费者组有leader当选就会触发重新分配,粘性分配就是减少已经分配的分区再分配,分配功能由协调器实现,会查找协调器位置,kafka默认主题存放有关协调工作的处理器信息,主题分为50分区对50%消费者i组d来找到消费者协调器并获得分区信息(其他消费者加入也如此),当有新消费者来时,leader会踢出消费者组重新竞争leader,并拉取消费者分区的分配信息,再重新分配消费者在哪些分区工作,流程如下:
13 kafka集群脑裂问题
zk在创造controller后确认了leader节点,如果由于网络问题leader在默认的18s时间没有心跳,zk会主动删除controller节点,其他broker的监听器会监听到这一事件主动竞争leader节点,当有新节点存在时会同步新集群的信息,这时由于网络问题的节点突然工作起来。三个节点,A、B、C现在C是最新的leader,A是就的leader,A认为自己是leader,B认为leader为C,解决使用epoch纪元的方式在zk创建新节点controller_epoch该节点管理leader的任期数,多个leader时大的当选leader与副本leader相同处理。