图片来自抱图网。消息队列主要解决应用耦合、异步处理、流量削减等问题。目前最常用的消息队列有RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ等。Redis、MySQL、phxsql等一些数据库也可以实现消息队列的功能。消息队列在实际应用场景中:应用解耦:多个应用通过消息队列处理同一个消息,避免了接口调用失败导致整个流程的失败。异步处理:多个应用程序处理消息队列中的同一条消息,应用程序并发处理消息,相比串行处理减少了处理时间。限流削峰:广泛应用于限时抢购或抢购活动,避免因流量过大导致应用系统挂掉的情况。今天给大家分享一篇经典的Kafka设计分析文章。Kafka是顶级的消息中间件。据Confluent称,超过三分之一的财富500强公司使用ApacheKafka。Kafka性能快,吞吐量高,比其他消息队列更上一层楼。即使在消息量巨大的情况下也能保持高性能。它在互联网公司中很受欢迎。希望大家能够理解Kafka设计的核心原则。Kafka架构Kafka是一个精心设计的东西,我只能这么说。这里所谓的一丝不苟,并不是指完全按照某种规范执行。像学生一样完成某项作业,比如JMS,反之,Kafka像JMS一样,突破规范约束,优秀,但又是一个JMS。当我用yet……来称呼这样的技术,就代表这东西进入了我的视野。好了,现在是Kafka和Storm的时候了,本文先说说Kafka。什么是卡夫卡?看官方文档,是一个Apache项目。它是一个消息队列。消息队列怎么样:消息队列是生产者和消费者之间的信使,避免了两者的直接接触。实际上,它可能具有与缓存相同的作用,平滑生产者和消费者之间代谢率的差异,但其根本目的是使生产者和消费者解耦。如果它让您感到困惑,请保持简单。Fireandforget,这句话的意思可以简单的说,就是真正的男人从不看爆炸,把烟头扔进油箱,把风衣的领子翻起来,把手插在裤兜里,走路头也不回地离开。消息队列,以下简称MQ,造就了这种真汉子。它允许生产者将消息扔到MQ中并忽略它,然后消费者可以在不与生产者交互的情况下从MQ中获取消息。在接下来的几页中,我将以自己的方式逐步演化出Kafka的原型。为了把握整体脉络,难免会隐藏很多细节。当然,这些细节很容易在它的官方文档和其他人的博客中找到。我的目的只是整理一个上下文。在设计类似的系统时,可以参考技巧。MQ向“正确”的方向进化Kafka就一定是正确的吗?客观的说,肯定不是,但它是本文的主角,所以肯定是没错的。我们先来看看最简单的MQ作为通用的形式。一般来说,这是大家第一次接触MQ后的课后作业。现在有个问题,如果有两个或两个以上的消费者需要消费消息怎么办?很简单,广播:消费者是上帝,不好对付,你推给他们的东西,不是所有人都想要,如果只需要一部分怎么办?好吧,消费者一定会责怪MQ服务不好,但是MQ有什么问题,它不理解消息的语义,面对刁难消费者,它只能要求生产者对消息进行细分,所以就有了是多个主题:这是一个显而易见的想法,即在消息队列中区分消息的主题,然后消费者可以从消息队列中获取他感兴趣的消息。但是仍然存在潜在的情况多个消费者对同一个Topic消息感兴趣:如果采用广播,那么还是会存在冗余传播的问题。如果是单播,那么消费者获取消息后,是否应该删除消息?什么?如果删除了,另一个消费者该怎么办?广播会浪费带宽,不广播不行……这好像进入了一个死循环,必须一劳永逸地从根本上解决问题。很明显的思路是下面的解决方案(至少我自己设计会做):问题解决了,但是我的天啊,仔细想想之前的架构,画出简化图后,你会发现事情会这样一瞬间就发生了失控,MQ本身的逻辑就太复杂了:回到UNIX哲学,遇到新的问题,需要新建一个程序,而不是在已有的程序上增加一个功能。基于这种想法,为什么不把消费者造成的复杂事情交给消费者呢?它有点依赖于卡夫卡。如果把MQ中的所有数据都持久化存储,消费者不就可以得到他们需要的东西了吗?这是一个根本性的变化。如果之前的方法是限定业务包——包是强行推给你的,你不要的可以自己扔掉。如果你放弃它,那么现在的方式是无限量的自助餐——你想要什么就拿什么。消息的自取,消息一直在MQ中,消费者可以随意取,任何消息都可以取,随时可以取。消费者只需要告诉MQ他们想要哪一条消息,所以需要传递一个消息的offset参数:但是,当自助餐打烊的时候,有的还会限制用餐时长。这是Kafka的策略存储的问题,详见文档。简化一下,现在看下图:一切OK。嗯,是的,这就是Kafka的原始模型。但卡夫卡远不止于此。并看到下面继续发展。集群、容错首先看目前的情况:这在逻辑上是一个类似Kafka的MQ应该有的结构。但就物理实现而言,又如何呢?经常听人说,Kafka从一开始就是为分布式而生的。在不同的机器上。让我们先看看扩展。类似于高速公路,一般听到广深高速,我们就知道这是广州到深圳的高速公路。这是一个合乎逻辑的陈述,类似于我们目前讨论的MQ主题。但是,这条高速公路的外观以及沿途的路线是物理实现的。此外,所有道路都被分成多条车道以实现平行。严格来说,每条车道都会被细分,比如小车道、客运车道、大货车车道、超车车道等等,所有这些车道上的车都到达同一个目的地(属于同一个Topic),但是它们的种类不同细分确实。将一个概念partition比作车道,如下图所示:注意keyhash模块,这里是区分汽车应该进入哪条车道的逻辑。在Kafka的术语里,lane就是partition,也就是分区。在同一个topic下分发消息时,需要自己设计一个hash函数。散列函数是一种分配策略,它确定将消息按顺序放在哪个分区中。温州皮鞋厂老板说,类比和举例不好,但这是技术散文,不是技术文档,主要是给自己看的,所以需要类比。TopicRouting所做的是决定去哪条高速公路,而keyhash决定你是乘汽车、公共汽车还是卡车去。值得注意的是,Kafka只保证同一个主题下同一个partition的消息顺序,并不能实现全局顺序。这不是缺陷,也不是两全其美。完整的订单需要序列化,但是序列化不能并行化,简直是扯淡!现在,在Topic下,我们有一个名为分区的新单元,这是Kafka中最基本的单元。请务必记住,它与您如何组织集群有关。好吧,我们来看看这些topic及其分区在M1和M2两台机器上是如何部署的:上面是两朵花开,一人一朵,现在该说说消费者了。消费者应该如何应对MQ本身已经进化到如此细粒度的程度呢?事实上,消费者也有横向扩展的需求。如果消费者对应分区,那么对应的主题就是消费者的上级。因此,额外增加一个层次,引入消费者群体的概念来解决问题:从CPU缓存到Kafka,设计思路一致。这是一个典型的全能组关联结构:至此,所有的图都画完了,是时候演示一下集群的部署了。我们知道,所谓的Kafka集群就是将各个topic的分区部署在不同的机器上,达到两个目的。一是负载均衡,即提供访问并行,二是提供高可用,即做热备份。我希望这两个功能可以用一张图来表示:整体结构如下:上面的持久化存储/查询机制我在两个小节中一步步展示了Kafka是如何工作的。虽然不知道作者是怎么设计的,但如果是我的话,我肯定是上面的想法……前面的描述毕竟是一个概览,不是很过瘾。这一节会稍微细说一下,Jin会讲解Kafka存储的半视图。我们知道,为了卸载MQ本身的复杂性,为了真正的无状态设计,Kafka将状态维护机制的锅完全甩给了消费者。因此,取消息的问题转化为消费者在Kafka存储中取一个偏移量索引取消息的问题,涉及到性能问题。但是我怎样才能更快地检查呢?如何?我们先给出最简单的场景。假设Kafka的每一个partition都是一个完整的独立文件,那么如果这个文件很大,其实也很大(可能达到T级别甚至P级别。。。)。那么在一个大文件中检索特定的消息本身就是一件令人头疼的事情,而且文件还在磁盘上,这就更糟了。我们都知道磁盘的随机读写是硬伤,顺序读写也好不了多少,这个怎么做?遍历?如果每个分区只是一个独立的文件,那么就只能遍历了:面对这种遍历问题,一般的解决办法是建立索引,并将索引数据存储在内存中,很多数据库都是这样做的。当然Kafka也可以这样做。Kafka最酷的地方之一就是它不依赖于任何特殊的文件系统,它的数据存在于普通文件中。但是,它将一个分区划分为一系列大小相等的小文件,所以在物理上,没有完整的分区文件,分区只是一个目录。我们知道,文件系统管理几个大小相等的文件是非常方便的:在上面的例子中,一个partiton被分成了100M的文件,这样的小文件被称为segments。在Kafka中存储时,每一个segment文件都会被打开,直到下一个文件满为止。由于报文的长度不一定统一,每个小段文件包含的报文数量也不一定相同。但无论如何,提取每个segment文件的第一个和最后一个消息偏移量并保存为元数据是一劳永逸的事情,便于构建常驻内存索引:通过这个区间搜索树,您可以快速定位到特定的段文件,但事情并没有就此结束。在Kafka中,每个partition段文件都配备了一个index索引文件。这个文件有什么作用?是段文件内部信息的稀疏索引,如下图所示:最后经过两次区间树搜索,最多再简单遍历一次就可以完成偏移定位工作。诚然,最后的遍历可能是少不了的,但是Kafka尽量避免了长时间耗时的遍历计算,而是将遍历压缩到很小的量级。这是一个权衡!与谁权衡?为什么不为段文件中的所有消息创建索引?很简单,把所有的索引都建起来,会导致索引很大,如果还想驻留在内存中,内存占用会很大,确实又费时费力。空间之间的权衡消失了。稀疏索引说到稀疏索引是很有用的。除了本文列举的Kafka的segmentindexsparseindex,还有两个比较常见的例子(我不是应用程序员,我是搞内核网络协议栈的,所以在我看来Kafka就更不常见了)到索引整个内存地址空间。稀疏的方法是分页,即将内存有规律地分成大小相等的块,称为内存页,然后可以对这些内存页进行索引,页表代替地址表稀疏索引,减少大小索引。另外,IP地址在地理上是聚合的,所以对于路由器物理设备来说,对于每个接口的方向,其IP地址集在很大程度上是可以聚合的。起初,路由表采用地址分类的方法,后来采用前缀匹配的方法来稀疏索引。地址分类有点像内存地址分页,只是页面有多种大小,而不是只有一种。这里的地址前缀更像是Kafka使用的两个索引。第一个是段索引,它是规则的,第二个是消息索引,它是不规则的。因为消息不定长。两种说法总结如下:OS内存页表:从虚拟地址定位到物理地址时,是否需要一一对应定位到各个地址?如果是这样的话,页表项会消耗多少管理内存呢?你算过吗?虚拟地址和物理地址将完全关联。使用稀疏索引后,只需要定位一个4K的页,将大大减少内存页表的内存占用。从而更有效率。路由表:是否每个IP地址都映射到路由器设备上的接口?这是不现实的。该解决方案从基于分配权限的分类地址稀疏索引开始,后来采用基于无类子网结构的前缀系数索引,无论哪种方式都大大减少了路由表条目的数量。UNIX哲学没有出路!为什么?因为只有复杂性才能反映出你自己的工作量。人们希望制造壁垒,把程序弄得很复杂,以显示自己的能力。毕竟,每个人都知道简单的事情。他们想要表现出不同,只能让自己的事情变得更复杂。如果你用几行Bash脚本完成一项艰巨的工作,经理很可能会认为你在做一些魔术,与C++解决方案相比什么都没有。Python更好,Java更好。4或5年前,我们有一个编程道场活动。其中一个题目是拼接字符串,也就是join操作。当时的经理和主持人强调,我们应该尽可能使用现成的接口。然而……有多少优秀的极简方案没有被表扬,但是最后被表扬的方案的特点你知道吗?它的特点是复杂。记得这个项目的作者上台介绍他的方案时说,“我的设计很简单……”结果,唉,技术上来说,是过度设计了,用大白话来说,就是自命不凡。楼主明显是半瓶了,哈哈。事情一定要做得越复杂越好,这就是能力的体现。如果你能在2行中完成某件事,你必须补上30行才算牛逼。UNIX哲学显然不适合我们。作者:极客重生编辑:陶佳龙来源:转载自公众号极客重生(ID:geek__coding)
