因为工作中常常用到 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 的,举荐大家应用版本反对的最好的策略。