因为工作中常常用到kafka,然而对kafka的一些外部机制不是很相熟,所以最近在看kafka相干的常识,咱们晓得kafka十分经典的音讯引擎,它以高性能、高可用著称。那么问题来了,它是怎么做到高性能、高可用的?它的音讯是以什么样的模式长久化的?既然写了磁盘,为何速度还那么快?它是如何保障音讯不失落的...?带着这一系列的问题,咱们来扒开kafka的面纱。
首先咱们思考这样一个问题:为什么须要音讯引擎?为什么不能间接走rpc? 以一个订单零碎为例:当咱们下了一个订单的时候,应该是要先减商品库存,而后用户领取扣钱,商家账户加钱...,最初可能还要发推送或者短信通知用户下单胜利,通知商家来订单了。
这整个下单过程,如果全副同步阻塞,那么耗时会减少,用户期待的工夫会加长,体验不太好,同时下单过程依赖的链路越长,危险越大。为了放慢响应,缩小危险,咱们能够把一些非必须卡在主链路中的业务拆解进来,让它们和主业务解耦。下单的最要害外围就是要保障库存、用户领取、商家打款的一致性,音讯的告诉齐全能够走异步。这样整个下单过程不会因为告诉商家或者告诉用户阻塞而阻塞,也不会因为它们失败而提醒订单失败。
接下来就是如何设计一个音讯引擎了,宏观来看一个音讯引擎反对发送、存储、接管就行了。
那么如上图一个繁难音讯队列模型呈现了,Engine把发送方的音讯存储起来,这样当接管方来找Engine要数据的时候,Engine再从存储中把数据响应给接管放就ok了。既然波及到长久化的存储,那么迟缓的磁盘IO是要思考的问题。还有接管方可能不止一个,以上述订单为例,下单实现之后,通过音讯把实现事件收回去,这时候负责用户侧推送的开发须要生产这条音讯,负责商户侧推送的开发也须要生产这条音讯,能想到的最简略的做法就是copy出两套音讯,然而这样是不是显得有点节约?高可用也是一个须要思考的点,那么咱们的engine是不是得正本,有了正本之后,如果一个engine节点挂掉,咱们能够选举出一个新副原本工作。光有正本也不行,发送方可能也是多个,这时候如果所有的发送方都把数据打到一个Leader(主)节点上仿佛也不合理,单个节点的压力太大。可能你会说:不是有正本吗?让接管方间接从正本读取音讯。这样的话又带来另一个问题:正本复制Leader的音讯提早了咋办?读不到音讯再读一次Leader?如果这样的话,引擎的设计的貌似更加简单了,仿佛不太正当。那就得想一种既能不通过正本又能扩散单节点压力就行了,答案就是分片技术,既然单个Leader节点压力太大,那么就分成多个Leader节点,咱们只须要一个好的负载平衡算法,通过负载平衡把音讯平均分配到各个分片节点就好了,于是咱们能够设计出一套大略长这样的生产者-消费者模型。
然而这些只是简略的想法,具体如何实现还是很简单的,带着这一系列问题和想法,咱们来看看kafka是如何实现的。
思考与实现
首先咱们还是从kafka的几个名词动手,次要介绍下音讯、主题、分区和消费者组。
一条音讯该怎么设计
音讯是服务的源头,所有的设计都是为了将音讯从一端送到另一端,这外面波及到音讯的构造,音讯体不能太大,太大容易造成存储成本上升,网络传输开销变大,所以音讯体只须要蕴含必要的信息,最好不要冗余。音讯最好也反对压缩,通过压缩能够在音讯体自身就精简的状况下变的更小,那么存储和网络开销能够进一步升高。音讯是要长久化的,被生产掉的音讯不能始终存储,或者说十分老的音讯被再次生产的可能性不大,须要一套机制来清理老的音讯,开释磁盘空间,如何找出老的音讯是要害,所以每个音讯最好带个音讯生产时的工夫戳,通过工夫戳计算出老的音讯,在适合的时候进行删除。音讯也是须要编号的,编号一方面代表了音讯的地位,另一方面消费者能够通过编号找到对应的音讯。大量的音讯如何存储也是个问题,全副存储在一个文件中,查问效率低且不利于清理老数据,所以采纳分段,通过分段的形式把大的日志文件切割成多个绝对小的日志文件来晋升维护性,这样当插入音讯的时候只有追加在段的最初就行,然而在查找音讯的时候如果把整个段加载到内存中一条一条找,仿佛也须要很大的内存开销,所以须要一套索引机制,通过索引来减速拜访对应的Message。
总结:一条kafka的音讯蕴含发明工夫、音讯的序号、反对消息压缩,存储音讯的日志是分段存储,并且是有索引的。
为什么须要Topic
宏观来看音讯引擎就是一发一收,有个问题:生产者A要给消费者B发送音讯,同时也要给消费者C发送音讯。那么消费者B和消费者C如何只生产到本人须要的数据?能想到的简略的做法就是在音讯中加Tag,消费者依据Tag来获取本人的音讯,不是本人的音讯间接跳过,然而这样仿佛不太优雅,而且存在cpu资源节约在音讯的过滤上。所以最无效的方法就是对于给B音讯不会给C,给C的音讯不会给B,这就是Topic。通过Topic来辨别不同的业务,每个消费者只须要订阅本人关注的Topic即可,生产者把消费者须要的音讯通过约定好的Topic发过来,那么简略的了解就是音讯依照Topic分类了。
总结:Topic是个逻辑的概念,Topic能够很好的做业务划分,每个消费者只须要关注本人的Topic即可。
分区如何保障程序
通过上文咱们晓得分区的目标就是扩散单节点的压力,再联合Topic和Message,那么音讯的大略分层就是Topic(主题)->Partition(分区)->Message(音讯)。兴许你会问,既然分区是为了升高单节点的压力,那么干嘛不必多个topic代替多个分区,在多个机器节点的状况下,咱们能够把多个topic部署在多个节点上,仿佛也能实现分布式,简略一想仿佛可行,认真一想,还是不对。咱们最终还要服务业务的,这样的话,原本一个topic的业务,要拆解成多个topic,反而把业务的定义打散了。
好吧,既然有多个分区了,那么音讯的调配是个问题,如果topic上面的数据过于集中在某个分区上,又会造成散布不平均,解决这个问题,一套好的调配算法是很有必要的。
kafka反对轮询法,即在多分区的状况下,通过轮询能够平均地把音讯分给每个分区,这里须要留神的是,每个分区里的数据是有序的,然而整体的数据是无奈保障程序的,如果你的业务强依赖音讯的程序,那么就要慎重考虑这种计划,比方生产者顺次发了A、B、C三个音讯,它们别离散布在3个分区中,那么有可能呈现的生产程序是B、A、C。
那么如何保障音讯的程序性?从整体的角度来看,只有分区数大于1,就永远无奈保障音讯的程序性,除非你把分区数设置成1,然而这样的话吞吐就是问题。从理论的业务场景来说,个别咱们可能须要某个用户的音讯、或者某个商品的音讯有序就能够了,用户A和用户B的音讯谁先谁后没关系,因为它们之间没什么关联,然而用户A的音讯咱们可能要放弃有序,比方音讯形容的是用户的行为,行为的先后顺序是不能乱的。这时候咱们能够思考用key hash的形式,同一个用户id,通过hash始终能放弃分到一个分区上,咱们晓得分区外部是有序的,所以这样的话,同一个用户的音讯肯定是有序的,同时不同的用户能够调配到不同的分区上,这样也利用到了多分区的个性。
总结:kafka整体音讯是无奈保障有序的,然而单个分区的音讯是能够保障有序的。
如何设计一个正当的消费者模型
既然是设计音讯模型,那么消费者必不可少,实现消费者最简略的形式就是起一个过程或者线程间接去broker外面拉取音讯即可,这很正当,然而如果生产的速度大于以后的生产速度怎么办?第一工夫想到的就是再起一个消费者,通过多个消费者来晋升生产速度,这里仿佛又有个问题,两个消费者都生产到了同一条音讯怎么办?加锁是个解决方案,然而效率会升高,兴许你会说生产的实质就是读,读是能够共享的,只有保障业务幂等,反复生产音讯也没关系。这样的话,如果10个消费者都争抢到了同样的音讯,后果有9个消费者都是白白浪费资源的。因而在须要多个消费者晋升生产能力的同时,还要保障每个消费者都生产到没被解决的音讯,这就是消费者组,消费者组上面能够有多个消费者,咱们晓得topic是分区的,因而只有消费者组内的每个消费者订阅不同的分区就能够了。现实的状况下是每个消费者都调配到雷同数据量分区,如果某个消费者取得的分区数不均匀(较多或者较少),呈现数据歪斜状态,那么就会导致某些消费者十分忙碌或者轻松,这样就不合理,这就须要一套平衡的调配策略。
kafka消费者分区调配策略次要有3种:
- Range:这种策略是针对topic的,会把topic的分区数和消费者数进行一个相除,如果有余数,那就阐明多余的分区不够平均分了,此时排在后面的消费者会多分得1个分区,乍看其实挺正当,毕竟原本数量就不平衡。然而如果消费者订阅了多个topic,并且每个topic均匀算下来都多几个个分区,那么对于排在后面的消费者就会多生产很多分区。
因为是依照topic维度来划分的,所以最终:
- c1生产 Topic0-p0、Topic0-p1、Topic1-p0、Topic1-p1
- c2生产 Topic0-p2、Topic1-p2
最终能够发现消费者c1比消费者c2整整多两个分区,齐全能够把c1的分辨别一个给c2,这样就能够平衡了。
- RoundRobin:这种策略的原理是将生产组内所有消费者以及消费者所订阅的所有topic的partition依照字典序排序,而后通过轮询算法一一将分区以此调配给每个消费者。假如当初有两个topic,每个topic3个分区,并且有3个消费者。那么大抵生产情况是这样的:
- c0生产 Topic0-p0、Topic1-p0
- c1生产 Topic0-p1、Topic1-p1
- c2生产 Topic0-p2、Topic1-p2
看似很完满,然而如果当初有3个topic,并且每个topic分区数是不统一的,比方topic0只有一个分区,topic1有两个分区,topic2有三个分区,而且消费者c0订阅了topic0,消费者c1订阅了topic0和topic1,消费者c2订阅了topic0、topic1、topic2,那么大抵生产情况是这样的:
- c0生产 Topic0-p0
- c1生产 Topic1-p0
- c2生产 Topic1-p1、Topic2-p0、Topic2-p1、Topic2-p2
这么看来RoundRobin并不是最完满的,在不思考每个topic分区吞吐能力的差别,能够看到c2的生产累赘显著很大,齐全能够将Topic1-p1分区分给消费者c1。
- Sticky:Range和RoundRobin都有各自的毛病,某些状况下能够更加平衡,然而没有做到。
Sticky引入目标之一就是:分区的调配要尽可能平均。以下面RoundRobin 3个topic别离对应1、2、3个分区的case来说,因为c1齐全能够生产Topic1-p1,然而它没有。针对这种状况,在Sticky模式下,就能够做到把Topic1-p1分给c1。
Sticky引入目标之二就是:分区的调配尽可能与上次调配的放弃雷同。这里次要解决就是rebalance后分区重新分配的问题,假如当初有3个消费者c0、c1、c2,他们都订阅了topic0、topic1、topic2、topic3,并且每个topic都有两个分区,此时生产的情况大略是这样:
这种调配形式目前看RoundRobin没什么区别,然而如果此时消费者c1退出,消费者组内只剩c0、c2。那么就须要把c1的分区从新分给c0和c2,咱们先来看看RoundRobin是如何rebalance的:
能够发现原来c0的topic1-p1分给了c2,原来c2的topic1-p0分给了c0。这种状况可能会造成反复生产问题,在消费者还没来得及提交的时候,发现分区曾经被分给了一个新的消费者,那么新的消费者就会产生反复生产。然而从实践的角度来说,在c1退出之后,能够没必要去动c0和c2的分区,只须要把本来c1的分区瓜分给c0和c2即可,这就是sticky的做法:
须要留神的是Sticky策略中,如果分区的调配要尽可能平均和分区的调配尽可能与上次调配的放弃雷同发生冲突,那么会优先实现第一个。
总结:kafka默认反对以上3种分区调配策略,也反对自定义分区调配,自定义的形式须要本人去实现,从成果来看RoundRobin要好于Range的,Sticky是要好于RoundRobin的,举荐大家应用版本反对的最好的策略。