摘要
我们上一节讲解了Kafka架构-基本原理,主要是降级了kafka的高性能,高可用,分布式存储,负载均衡故障感知。这一节主要讲解下kafka架构-底层原理。
高可用底层实现:
Partition中核心offset:LEO、HW;以及LEO跟HW如何更新;HW在0.11.x版本前存在的问题以及后面版本如何解决这个问题的?
高性能:
Kafka分段日志如何存储,如何快速定位。
负载均衡故障感知:
Kafka如何通信?Kafka的Controller如何实现故障转移、Leader选举、创建/删除Topic时候Controkler会做什么操作?Controller如何感知Broker上线以及崩溃的?
思维导图
内容
1、Partition的核心offset
LEO:
log end offset:指代下次写入到此Partition的下一条数据的offset;是partition最大offset+1;
HW:
HW:(High Water Mark) Leader partition同步到其所有follower的offset;
LEO跟HW的作用:
LEO作用:
1、LEO的作用是负责推算Leader partition的HW;当所以的follower partition的LEO推送给Leader partition时候,Leader partition根据min{LEO1...LEOn}即可得到Leader的HW.
HW作用:
1、更新follower的HW;当follower请求leader数据同步时候,leader会返回自己的HW,然后follower会更新min{Leader(HW),LEO}。
2、划分已提交/为提交数据:HW=3;表示前3条数据是已经同步到其他所有的follower里面去了,所以我们也将其叫做committed(已提交数据),消费者是消费不到HW之后的uncommmitted数据的。
3、消费:Consumer只能看到base offset到HW offset之间的数据,这部分数据是committed;可以被消费。
2、Leader跟Follower的HW和LEO如何更新?
每次follower通过fetch请求同步数据的时候,都会携带:自己的LEO参数,
leader返回参数的时候,每次都会返回HW的数据。
原则:
Leader partition的HW是由follower的LEO更新,follower的HW是由Leader 的HW更新。
Leader的LEO跟HW更新:
1、LEO:生产者producer生产1条数据之后,会往leader partition里面插入1条数据,然后对应的LEO会+1
2、HW:每一个follwer同步数据时候都会携带自己的LEO,然后leader partiotion进行:min{LEO0,...LEOn}就是HW
follower的LEO跟HW更新:
1、当follower拉取到leader的数据后,根据返回的数据条数更新自己的LEO。
2、当follower进行数据同步时候,会从Leader响应获取到对应的HW,然后选择min{HW,LEO}更新follower自己的HW。
3、高水位HW切换时候会发生哪些问题?
高水位HW切换时候会发生的问题,我们基于2个原则:
原则:
1、Kafka Broker重启时候,会根据自己的HW来恢复数据
2、假设副本数为2,设置min.insync.replicas = 1
数据丢失
1、数据丢失主要发生在leader partition已经更新到最新的HW跟LEO同步了,此时leader partition上的数据都是已提交状态。假设一个topic下的partition有2个副本,此时参数:min.insync.replicas = 1;代表了只要往此partition的一个副本里面写如数据即可认为写入成功,然后返回。原始leader partitiond的LEO=HW=0;假设写入一条数据后LEO=1,HW=0;然后follower携带LEO=0拉取数据,leader更新HW=min{LEO}=0,然后将HW=0携带返回给follower,follower此时更新自己的HW=min{HW,LWO}=0跟LEO=1;然后第二次follower携带LEO再次获取数据,此时leader更新HW=1;此时leader已经提交的数据是1;但是此时follower宕机,然后根据HW=0恢复数据删除之前的LEO=1.此时LEO数据为0,然后向leader同步数据时候,leader宕机,此时follwer变成leader,然后leader重新启动之后,变成follower,将会从新的leader同步数据,然后LEO=HW=0;
数据不一致
1、数据不一致问题主要发生在leader切换时候,follower成为新的leader时候,在原来leader同步数据之前,写入了新的数据,让新旧的LEO相同;由于参数设置为1,当2条数据写入到leader时候就算写入成功,LEO=2,HW=0;然后follower同步拉取两条数据,leader更新HW=2;返回数据给follower前,follower的LEO=HW=1;此时leader宕机。然后follower成为leader后收到了1条写入数据,此时新的leader的HW=1,LEO=2,然后之前宕机的leader重新启动后根据本机的HW=2然后发现跟新的leader,此时leader更新HW=2,然后返回HW给follower,发现数据HW一致,不会拉取新的数据,导致数据不一致。
4、Kafka的0.11.x版本如何解决之前版本高水位机制的弊端?(重点)
结论:
Kafka 0.11.x版本引入leader epoch机制解决高水位机制弊端;
leader的leader epoch机制如下:
1、每个leader都会保存一个leader epoch。
2、所谓的leader epoch大致理解为每个leader的版本号,以及自己是从哪个offset开始写数据的,数据结构类似:[epoch, offset],这个第一位表示版本号,第二位表示写数据的offset;
leader epoch解决数据丢失问题(类似于以LEO重启后代替HW):
1、假如此时follower来第二次拉取数据,则原来的leader partition的LEO=HW=1;意思就是leader partition的数据已经全部同步给follower了,此时对应的leader epoch为[0,1],此时follower的LEO=1,HW=0,此时follower宕机,然后follower重启之后,原来的leader宕机,此时从zk里面获取数据进行leader选举follower成为新的leader对应的leader epoch为[1,1]--意思就是当前版本为1,对应的的下次写入数据的offset为1;然后接着老leader恢复变为follower,从新leader看一下epoch跟自己对比,人家offset = 1,自己的offset = 1,也不需要做任何数据截断
leader epoch解决数据不一致问题(类似于以LEO重启后代替HW):
1、假如老的leader在写数据到partition的offset为0,1的位置写了两条数据,但是此时第二条数据还没有同步到follower里面就宕机了,此时follow里面的数据在offset=0的位置有数据,offset=1的位置没有数据;此时follower变成了leader接收写请求的时候,会在LEO=1的offset=1的位置写数据,此时[epoch = 1, offset = 1],然后如果说老leader恢复之后作为follower,从新leader(原来的follower)看到[epoch = 1, offset = 1],此时会发现自己的offset也是1,但是人家新leader是从offset = 1开始写的,自己的offset = 1怎么已经有数据了呢?
此时就会截断掉自己一条数据,然后跟人家同步保持数据一致。
5、Kafka的partition维护ISR列表的底层机制是如何设计的?
换言之kafka到底是如何维护ISR列表的,什么样的follower才有资格放到ISR列表里呢?
0.9.x之前:
1、通过参数:参数:replica.lag.max.messages管控:follower如果落后leader的消息数量超过了这个参数指定的数量之后,就会认为follower是out-of-sync,就会从ISR列表里移除了,后面如果跟上之后,将会重新将其加到ISR列表里面来。
举个例子好了,假设一个partition有3个副本,其中一个leader,两个follower,然后replica.lag.max.messages = 3(副本.滞后.最大.消息数:也就是follower的消息比leader滞后3滞后,就会从ISR列表中移除),刚开始的时候leader和follower都有3条数据,此时HW和LEO都是offset = 2的位置,大家都同步上来了
现在来了一条数据,leader和其中一个follower都写入了,但是另外一个follower因为自身所在机器性能突然降低,导致没及时去同步数据,原因主要是follower所在机器的网络负载、内存负载、磁盘负载过高,导致整体性能下降了,此时leader partition的HW还是offset = 2的位置,没动,因为HW的位置是根据follower决定的,follower没有同步数据则原来的HW就不会变动,(由于leader已经写入了5条数据,但是follower只同步了一条数据,还有4条数据没有同步过来,follower的LEO为2)但是LEO变成了offset = 6的位置;这个时候follower发送fetch请求的话。就会判断leader和follower的LEO相差了多少,如果差的数量超过了replica.lag.max.messages参数设置的一个阈值之后,就会把follower给踢出ISR列表。
什么场景下会触发ISR列表剔除呢?依托LEO来更新ISR的话,在每个follower不断的发送Fetch请求过来的时候,就会判断leader和follower的LEO相差了多少,如果差的数量超过了replica.lag.max.messages参数设置的一个阈值之后,就会把follower给踢出ISR列表。(注意:必须是leader里面ISR列表中所有的follower副本的LEO都往前推进了的话,leader的HW才会往前推进,上面图HW的offset=1,代表只有1条数据是可以读取的),但是假如kafka只有一个follower跟leader两个副本的话,一旦说把follower从leader的ISR列表里面踢出去之后,就会导致ISR列表里面只有leader一个人了,只有leader一个人的话,就会发现说,我只需要看leader我自己的LEO在哪里?然后直接把自己的HW往前面推进:
假如说在folower滞后leader超过replica.lag.max.messages 被踢出去之前,其实会影响2个地方。一个是生产者,一个是消费者。
生产者:对于生产者写HW=1之后的数据3,4,5的时候,这些数据还没有同步给ISR列表里面的所有的follower,就会导致生产者直接卡住。
消费者:消费者没有办法消费到HW后面的数据的,这个是一个问题。
依据上图我们分析出:
假如说在folower滞后leader超过replica.lag.max.messages 被踢出去之前,其实会影响2个地方。一个是生产者,一个是消费者。
生产者:对于生产者写HW=1之后的数据3,4,5的时候,这些数据还没有同步给ISR列表里面的所有的follower,就会导致生产者直接卡住。
消费者:消费者没有办法消费到HW后面的数据的,这个是一个问题。
消费者问题:但是这个时候第二个follower的LEO就落后了leader才1个offset,还没到replica.lag.max.messages = 3,所以第二个follower实际上还在ISR列表里,只不过刚才那条消息没有算“提交的”,在HW外面,所以消费者是读不到的
生产者问题:而且这个时候,生产者写数据的时候,如果默认值是要求必须同步所有follower才算写成功的,可能这个时候会导致生产者一直卡在那儿,认为自己还没写成功,这个是有可能的。
一共有3个副本,1个leaderr,2个是follower,此时其中一个follower落后,被ISR踢掉了,ISR里还有2个副本,此时一个leader和另外一个follower都同步成功了,此时就可以让那些卡住的生产者就可以返回,认为写数据就成功了
如果你一旦设置min.sync.replicas = 2,ack = -1,生产者要求你必须要有2个副本在isr里,才可以写,此外,必须isr里的副本全部都接受到数据,才可以算写入成功了,一旦说你的isr副本里面少于2了,其实还是可能会导致你生产数据被卡住的
假设这个时候,第二个follower fullgc持续了几百毫秒然后结束了,接着从leader同步了那条数据,此时大家LEO都一样,而且leader发现所有follower都同步了这条数据,leader就会把HW推进一位,HW变成offset = 3。
这个时候,消费者就可以读到这条在HW范围内的数据了,而且生产者认为写成功了
但是要是此时follower fullgc一直持续了好几秒钟,此时其他的生产者一直在发送数据过来,leader和第一个follower的LEO又推进了2位,LEO offset = 5,但是HW还是停留在offset = 2,这个时候HW后面的数据都是消费不了的,而且HW后面的那几条数据的生产者可能都会认为写未成功
现在导致第二个follower的LEO跟leader的LEO差距超过3了,此时触发阈值,follower认为是out-of-sync,就会从ISR列表里移除了
一旦第二个follower从ISR列表里移除了,世界清静了,此时ISR列表里就leader和第一个follower两个副本了,此时leader和第一个follower的LEO都是offset = 5,是同步的,leader就会把HW推进到offset = 5,此时消费者就可以消费全部数据了,生产者也认为他们的写操作成功了
那如果第二个follower后来他的fullgc结束了,开始大力追赶leader的数据,慢慢LEO又控制在replica.lag.max.messages限定的范围内了,此时follower会重新加回到ISR列表里去。
6、Kafka分段日志
1、kafka如何使用分段机制保存日志?
没生成一份日志的时候,都会生成相应的3个文件。.log
每个分区partition都有对应的目录,就是“topic-分区号”的格式,比如说有个topic叫做“order-topic”,那么假设他有3个分区,每个分区在一台机器上,那么3台机器上分别会有3个目录,“order-topic-0”,“order-topic-1”,“order-topic-2”。
每个分区里面就是很多的log segment file,也就是日志段文件,每个分区的数据会被拆分为多个段,放在多个文件里,每个文件还有自己的索引文件,大概格式可能如下所示:
00000000000000000000.index
00000000000000000000.log
00000000000000000000.timeindex
00000000000005367851.index
00000000000005367851.log
00000000000005367851.timeindex
00000000000009936472.index
00000000000009936472.log
00000000000009936472.timeindex
这个9936472之类的数字,就是代表了这个日志段文件里包含的起始offset,也就说明这个分区里至少都写入了接近1000万条数据了
kafka broker有一个参数,log.segment.bytes,限定了每个日志段文件的大小,最大就是1GB,一个日志段文件满了,就自动开一个新的日志段文件来写入,避免单个文件过大,影响文件的读写性能,这个过程叫做log rolling。
正在被写入的那个日志段文件,叫做active log segment。
2、引入索引文件如何基于二分查找快速定位数据?
每一个日志段文件:.log文件会对应一个.index和.timeindex两个索引文件,也就是kafka在写入日志文件的时候,同时会写索引文件,就是.index和.timeindex,一个是位移索引,一个是时间戳索引,是两种索引
默认情况下,有个参数log.index.interval.bytes限定了在日志文件写入多少数据,就要在索引文件写一条索引,默认是4KB,写4kb的数据然后在索引里写一条索引,所以索引本身是稀疏格式的索引,并不是来一条数据就写入一条数据的索引。不是每条数据对应一条索引的
.index是按照offset来构建索引的。.timeindex实际上是按照时间戳来构建索引,其实offset跟时间戳都是按照顺序升序排序的。
而且索引文件里的数据是按照位移和时间戳升序排序的,所以kafka在查找索引的时候,会用二分查找,时间复杂度是O(logN),找到索引,就可以在.log文件里定位到数据了
比如说.index文件里面有如下数据:
44576 物理文件(.log位置)//offset=44576代表的是.log文件的位置
57976 物理文件(.log位置)
64352 物理文件(.log位置)
比如我现在需要搜索下offset = 58892这么一条数据。我就可以在.index里面用二分查找了。发现58892比44576 大,比64352小,所以我们定位到58892数据在 => 57976这条数据对应的.log文件的位置。接着就可以从.log文件里的57976这条数对应的位置开始查找,去找offset = 58892这条数据在.log里的完整数据。所以根据.index文件用二分查找法去查找数据的话,其时间复杂度是O(logN)。
.timeindex是时间戳索引文件,如果要查找某段时间范围内的时间,先在这个文件里二分查找找到offset,然后再去.index里根据offset二分查找找对应的.log文件里的位置,最后就去.log文件里查找对应的数据。
3、磁盘上日志文件是按照什么策略定时清理腾出空间?
不停地往里面写数据,系统磁盘空间迟早会塞满。所以Kafka一定有一个数据清理的策略的。大家可以想,不可能说每天涌入的数据都一直留存在磁盘上,本质kafka是一个流式数据的中间件,不需要跟离线存储系统一样保存全量的大数据,所以kafka是会定期清理掉数据的,这里有几个清理策略。
kafka broker会在后台启动线程异步的进行日志清理的工作。
参数:log.retention.day
1、kafka默认是保留最近7天的数据,每天都会把7天以前的数据给清理掉,包括.log、.index和.timeindex几个文件。
参数:log.retention.hours
1、设置保留多少小时,默认换算成天也是7天。
只要你的数据保留在kafka里,你随时可以通过offset的指定,随时可以从kafka楼出来几天之前的数据,你可以把数据回放一遍,你需要考虑下你下游的数据,有多么的重要,如果是特别核心的数据,在kafka这个层面,可以保留7天,甚至是15天的数据。
因为你可能怕下游的消费者消费了数据之后,比如说数据丢失了,你需要从kafka里楼出来3天前的数据,重新来回放处理一遍。
7、Kafka通信
1、Kafka如何自定义协议以及使用长连接通信?
通信协议:
kafka的通信主要发生于:
1、生产端和broker之间(发送数据),
2、broker和消费端之间(消费数据),
3、broker和broker之间(同步数据),
这些通信都是基于TCP协议进行的,底层基于TCP连接和传输数据,基于tcp之上Kafka通信的协议是应用层的协议,是Kafka自己自定义的协议:定好请求跟响应的传输数据的格式,请求格式、响应格式,这样大家就可以统一按照规定好的格式来封装、传输和解析数据了.
长连接
对于生产端和broker,消费端和broker来说,还会基于TCP建立长连接,也就是维护一批长连接,然后通过固定的连接不断的传输数据,避免频繁的创建连接和销毁连接的开销。
broker端会构造一个请求队列,然后不停的获取请求放入队列,后台再搞一堆的线程来获取请求进行处理
2、Kafka Broker如何基于Reactor模型进行多路复用处理请求?
Kafka跟netty有一个共同点就是都是使用Reactor模型实现了多路复用,但是kafka在此基础之上多了一个Handler线程池去处理请求,将处理完的请求放到相应队列里面,让processor线程去处理返回给acceptor
这一节我们分析下,broker是如何接收别人请求的?他的整个的请求/处理架构是怎样的?
实际上他采用的是一个Reactor模式。
1、每个broker上都有一个acceptor线程和很多个processor线程。可以用num.network.threads参数设置processor线程的数量,默认是3,client跟一个broker之间只会创建一个socket长连接,他会复用,然后broker就用一个acceptor来监听每个socket连接的接入,分配这个socket连接给一个processor线程,processor线程负责处理这个socket连接,监听socket连接的数据传输以及客户端发送过来的请求,acceptor线程会不停的轮询各个processor来分配接入的socket连接
2、一个proessor如何处理多个客户端的socket连接请求的呢?他这里使用的是Reactor的多路复用思想。它是基于selector机制。如下:proessor需要处理多个客户端的socket连接,就是通过java nio的selector多路复用思想来实现的,用一个selector监听各个socket连接,看其是否有请求发送过来,这样一个processor就可以处理多个客户端的socket连接了。processor线程会负责把请求放入一个broker全局唯一的请求队列,默认大小是500,是queued.max.requests参数控制的,所有的processor会不停的把请求放入这个请求队列中;接着就是一个KafkaRequestHandler线程池负责不停的从请求队列中获取请求来处理,这个线程池大小默认是8个,由num.io.threads参数来控制,处理完请求后的响应,会放入每个processor自己的响应队列里;,然后会监听自己的响应队列,把响应拿出来通过socket连接发送回客户端
根据上图,总结如下:
1、很多客户端会来建立请求,建立请求之后会通过acceptor来走,acceprtor拿到一个建立连接的socket之后,均匀分配给各个processor。 各个processor类似于nio的多路复用机制。处理多个socket,然后所有processor将请求放到一个请求队列里面去,然后请求队列里面的请求由一个Handler线程池来处理。默认会有8个线程,处理完请求之后,会将请求处理完的响应结果存放到每一个processor对应的响应队列里面去。然后processor从响应队列里面获取到对应的响应发送到对应的生产者/消费者里面去。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。