本文面试情节虚假,但知识真实,请在家人或者朋友的陪同下仔细观看,防止在观看的过程发呆、走神导致没学到知识。
性能篇
一位身穿格子衬衣,头发好似一拳超人的中年人走了过来,没错他就是面试官,他手握简历,若有所思,我当时害怕极了,然后他开口:小伙子啊,我们这边是基础架构的中间件组,既然你的简历没提到kafka,那我接下来问问你kafka的知识吧。
我:好的,kafka平时看的不多,但也还了解一点,不是特别精通所以没写了。(嘿嘿,我是故意没写的,早就知道你要来这一套,kafka其实是俺最精通的东西了)
面试官捋了捋他那稀疏的胡须:那我们开始吧,先说说kafka的Log文件存在什么地方?
我:kafka的topic可以分区,所以Log对应了一个命名形式为topic-partition的文件夹,比如对于一个有两个分区的topic来说,它的log分别存在xxx/topic-1和xxx/topic-2中。
面试官:那按照这样的说法,所以log文件的位置应该就是xxx/topic-1/data.log或者xxx/topic-2/data.log?
我:不是的,kafka的log会分段,每个分区文件夹下,其实有很多的log段,它们共同组成了log,每个日志段大小是1G,如果一个日志段写完,会自动写入一个新的段。
面试官:为什么要分段?不分段行不行?
我:分段可以很好的维护数据,首先不分段,当查找一条数据的时候会很麻烦,就像在一本没有目录的新华字典里找数据一样,如果分了段我们只要知道数据在哪个段中,然后在对应的段中查找即可。同时由于log是持久化磁盘的,磁盘的空间不可能无穷无尽的,当需要清除一些老数据,通过分段机制,只需要删除较老的数据段即可。
面试官:hold on,hold on~,你说分了段后我们只要知道数据在哪个段中即可,那么我们怎么知道数据在哪个段中的?
我:easy,easy~,kafka内部维护一个跳跃表,跳跃表的节点就是每个段的段号。这样当查询数据的时候,先根据跳跃表就可以快速定位到目标数据段。
面试官:跳跃表是可以加速访问,但是每个段的段号是咋确定的?
我:kakfa的段号其实就是根据偏移量来的,它代表当前段内偏移量最小的那条数据的offset,比如:
segment1的段号是200,segment2的段号是500,那么segment1就存储了偏移量200-499的消息。
面试官:嗯嗯,那定位到段后,如何定位到具体的消息,直接遍历吗?
我:不是直接遍历,直接遍历效率太低,kafka采用稀疏索引的方式来搜索具体的消息,其实每个log分段后,除了log文件外,还有两个索引文件,分别是.index和.timeindex,
其中.index就是我说的偏移量索引文件,它不会为每条消息创建索引,它会每隔一个范围区间创建索引,所以称之为稀疏索引。
比如我们要查找消息6的时候,首先加载稀疏文件索引.index到内存中,然后通过二分法定位到消息5,最后通过消息5指向的物理地址接着向下顺序查找,直至找到消息6。
面试官:那稀疏索引的好处是什么?
我:稀疏索引是一个折中的方案,既不占用太多空间,也提供了一定的快速检索能力。
面试官:上面你说到了.timeindex文件,它是干嘛的?
我:这和kafka清理数据有着密切的关系,kafka默认保留7天内的数据,对于超过7天的数据,会被清理掉,这里的清理逻辑主要根据timeindex时间索引文件里最大的时间来判断的,如果最大时间与当前时间差值超过7天,那么对应的数据段就会被清理掉。
面试官:说到数据清理,除了你说的根据时间来判断的,还有哪些?
我:还有根据日志文件大小和日志起始偏移量的方式,对于日志文件大小,如果log文件(所有的数据段总和)大于我们设定的阈值,那么就会从第一个数据段开始清理,直至满足条件。对于日志起始偏移量,如果日志段的起始偏移量小于等于我们设定的阈值,那么对应的数据段就会被清理掉。
面试官:你知道消息合并吗?如果知道说说消息合并带来的好处。
我:了解一点,消息合并就是把多条消息合并在一起,然后一次rpc调用发给broker,这样的好处无疑会减少很多网络IO资源,其次消息会有个crc校验,如果不合并每条消息都要crc,合并之后,多条消息可以一起crc一次。
面试官:那合并之后的消息,什么时候会给broker?
我:合并的消息会在缓冲区内,如果缓冲区快满了或者一段时间内没有生产消息了,那么就会把消息发给broker。
面试官:那你知道消息压缩吗?
我:知道一点,压缩是利用cpu时间来节省带宽成本,压缩可以使数据包的体积变得更小,生产者负责将数据消息压缩,消费者拿到消息后自行解压。
面试官:所有只有生产者可以压缩?
我:不是的,broker也可以压缩,当生产者指定的压缩算法和broker指定压缩算法的不一样的时候,broker会先按照生产者的压缩算法解压缩一下,然后再按照自己的压缩算法压缩一下,这是需要注意的,如果出现这种情况会影响整体的吞吐。还有就是新老版本的问题,如果新老版本的压缩算法不兼容,比如broker版本比较老,不支持新的压缩算法,那么也会发生一样的事情。
面试官:我们知道kafka的消息是要写入磁盘的,磁盘IO会不会很慢?
我:是这样的,kafka的消息是磁盘顺序读写的,有关测试结果表明,一个由6块7200r/min的RAID-5阵列组成的磁盘簇的线性(顺序)写入速度可以达到 600MB/s,而随机写入速度只有 100KB/s,两者性能相差6000倍。操作系统可以针对线性读写做深层次的优化,比如预读(read-ahead,提前将一个比较大的磁盘块读入内存)和后写(write-behind,将很多小的逻辑写操作合并起来组成一个大的物理写操作)技术。顺序写盘的速度不仅比随机写盘的速度快,而且也比随机写内存的速度快。
面试官:顺序读写是为了解决了缓慢的磁盘问题,那在网络方面还有其他的优化吗?
我:有,零拷贝,在没有零拷贝的时候,消息是这样交互的:
- 切到内核态:内核把磁盘数据copy到内核缓冲区
- 切到用户态:把内核的数据copy到用户程序
- 切到内核态:用户数据copy到内核socket缓冲区
- socket把数据copy给网卡
可以发现一份数据经过多次copy,最终兜兜转转又回到了内核态,实属浪费。
当有了零拷贝之后:
- 磁盘数据copy到内核缓冲
- 内核缓冲把描述符和长度发给socket,同时直接把数据发给网卡
可以发现通过零拷贝,减少了两次copy过程,大大降低了开销。
可靠篇
面试官:(关于性能方面的问的差不多了,接下来换换口味吧),kafka的多消费者模型是怎么做到的?
我:如果要支持多个消费者同时消费一个topic,最简单的方式就是把topic复制一份,但这无疑会浪费很多空间,尤其在消费者很多的情况下,
于是kafka设计出一套offset机制,即一份数据,不同的消费者根据位置来获取不同的消息即可。
面试官:那你知道消费者的offset存在哪吗?
我:很久以前,是存在zookeeper中的,但是offset需要频繁更新,zookeeper又不适合频繁更新,所以后来就把消费者位移存在了一个叫_consumer_offset的topic中,这个topic会在第一个消费者启动的时候自动创建,默认50个分区,3个副本。
面试官:那你说说这个_consumer_offset里面具体存了什么?
我:这里其实主要分为key和value,value可以简单的认为就是我们的消费者位移,关于key,这里要细说下,由于每个消费者都属于一个消费者组,并且每个消费者其实消费的是某个topic的分区,所以通过group-topic-partition就可以关联上对应的消费者了,这也就是key的组成。
面试官:那你能介绍下消费者提交位移的方式吗?
我:这里分为自动提交和手动提交。自动提交的话,就不需要我们干预,我们消费完消息后,kafka会自动帮我们提交,手动提交的话,就需要我们在消费到消息后自己主动commit。
面试官:自动提交会有什么问题?
我:自动提交的策略是consumer默认每隔5秒提交一次位移,如果consumer在接下来的很长时间内都没有数据消费,那么自动提交策略就会一直提交重复的位移,导致_consumer_offset有很多重复的消息。
面试官:那这有什么解决方案吗?
我:有,这种情况的核心问题就是可能会有大量的、重复的位移消息占用存储空间,只要把重复的去掉即可,kafka提供一种类似redis的aofrewrite的功能,叫compact策略,compact是由一个logCleaner线程来完成的,它会把重复的、并且较老的消息清除掉。
面试官:那如果consumer自动重启了,位移没来的及提交咋办?
我:这个会造成重复消费,一般业务上需要配合做幂等。
面试官:那手动提交能解决这个问题吗?
我:不能,如果我们在业务处理完之后手动提交,但是在还没得及提交的情况下,也发生了重启或者其他原因导致提交不上去,在消费者恢复后也会发生重复消费。
面试官:那如果我是先提交,后处理业务逻辑呢?
我:这种情况也不能保证100%没问题,如果提交成功,但是处理业务时出错,正常来说,这时希望重新消费这条数据是不行的,因为已经提交了,除非你重置offset。总之无论哪种方案都不能保证100%的完美,我们需要自己根据业务情况做幂等或者根据log来找到丢失的数据。
面试官:消费者提交消费位移时提交的是是当前消费到的最新消息的offset还是offset+1?
我:offset+1。
面试官:从生产者的角度谈谈消息不丢失的看法。
我:关于消息丢失问题,kafka的生产者提供了3种策略来供使用者选择,每种策略各有利弊,需要结合业务的实际状况来选择。
- 第一种就是生产者不关心消息的情况,只负责发,这种模式无疑速度是最快的,吞吐是最好的,但是可能造成大量的数据丢失,比如在borker出现问题的时候,生产者还不停的发,那么到broker恢复期间的数据都将丢失。
- 第二种就是生产者需要所有副本都写入成功,不管是Leader副本还是Follower副本,那么当Follower副本越多,吞吐理论就越差,但是这种模式下,消息是最安全的。
- 第三种就是生产者只需要收到Leader副本的ack即可,不用关心Follower副本的写入情况,它是个折中的做法,保证了一定的安全性的同时也不会太影响吞吐。
如果你不在意自己的数据丢失问题,追求吞吐,比如像log这种,可以采用第一种,如果你非常在意自己的数据安全性,那么就选第二种。如果你希望吞吐稍微好点,同时数据又能安全些,建议第三种,但是第三种在Follower副本出现的问题的时候对生产者来说是无法感知的。
面试官:那你说说一个Follower副本如何被选举成Leader的?
我:在kafka中有这样几个概念:
- AR:所有副本集合
- ISR:所有符合选举条件的副本集合
- OSR:落后太多或者挂掉的副本集合
AR = ISR + OSR,在正常情况下,AR应该是和ISR一样的,但是当某个Follower副本落后太多或者某个Follower副本节点挂掉了,那么它会被移出ISR放入OSR中,kafka的选举也比较简单,就是把ISR中的第一个副本选举成新的Leader节点。比如现在AR=[1,2,3],1挂掉了,那么ISR=[2,3],这时会选举2为新的Leader。
面试官捋了捋自己左边的刘海:你还有什么要问我的吗?
我:老师,请问你会组合拳吗?
面试官:组合拳我不会,但是等会会有很多人组合过来面你。
我:
未完待续...
作者:假装懂编程
链接:https://juejin.cn/post/7018702635544870948
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。