Kafka消息队列
文章目录
1、Kafka 为实时日志流而生,要处理的并发和数据量非常大。可见,Kafka 本身就是一个高并发系统,它必然会遇到高并发场景下典型的三高挑战:高性能、高可用和高扩展。
2、为了简化实现的复杂度,Kafka 最终采用了很巧妙的消息模型:它将所有消息进行了持久化存储,让消费者自己各取所需,想取哪个消息,想什么时候取都行,只需要传递一个消息的 offset 进行拉取即可。
3、从宏观角度解析 Kafka 的整体架构:为了解决存储的扩展性问题,Kafka 对数据进行了水平拆分,引出了 Partition(分区),通过 Partition 的多副本冗余机制进行故障转移,确保了高可靠。
Kafka 是什么?
我们先看看 Kafka 官网给自己下的定义:
- Apache Kafka is an open-source distributed event streaming platform.
翻译成中文就是:Apache Kafka 是一个开源的分布式流处理平台。
Kafka 不是一个消息系统吗?为什么被称为分布式的流处理平台呢?这两者是一回事吗?
一定有读者会有这样的疑问,要解释这个问题,需要先从 Kafka 的诞生背景说起。
Kafka 最开始其实是 Linkedin 内部孵化的项目,在设计之初是被当做「数据管道」,用于处理以下两种场景:
运营活动场景:记录用户的浏览、搜索、点击、活跃度等行为。
系统运维场景:监控服务器的 CPU、内存、请求耗时等性能指标。
可以看到这两种数据都属于日志范畴,特点是:数据实时生产,而且数据量很大。
Linkedin 最初也尝试过用 ActiveMQ 来解决数据传输问题,但是性能无法满足要求,然后才决定自研 Kafka。
所以从一开始,Kafka 就是为实时日志流而生的。了解了这个背景,就不难理解 Kafka 与流数据的关系了,以及 Kafka 为什么在大数据领域有如此广泛的应用?也是因为它最初就是为解决大数据的管道问题而诞生的。
接着再解释下:为什么 Kafka 被官方定义成流处理平台呢?它不就提供了一个数据通道能力吗,怎么还和平台扯上关系了?
这是因为 Kafka 从 0.8 版本开始,就已经在提供一些和数据处理有关的组件了,比如:
- 1、Kafka Streams:一个轻量化的流计算库,性质类似于 Spark、Flink。
- 2、Kafka Connect:一个数据同步工具,能将 Kafka 中的数据导入到关系数据库、Hadoop、搜索引擎中。
可见 Kafka 的野心不仅仅是一个消息系统,它早就在往「实时流处理平台」方向发展了。
这时候,再回来看 Kafka 的官网介绍提到的 3 种能力,也不难理解了:
- 1、数据的发布和订阅能力(消息队列)
- 2、数据的分布式存储能力(存储系统)
- 3、数据的实时处理能力(流处理引擎)
这样,kafka 的发展历史和定义基本缕清了。当然,这个系列仅仅关注 Kafka 的前两种能力,因为这两种能力都和 MQ 强相关。
从 Kafka的消息模型说起
理解了 Kafka 的定位以及它的诞生背景,接着我们分析下 Kafka 的设计思想。
上篇文章中我提到过:要吃透一个MQ,建议从「消息模型」这种最核心的理论层面入手,而不是一上来就去看技术架构,更不要直接进入技术细节。
所谓消息模型,可以理解成一种逻辑结构,它是技术架构再往上的一层抽象,往往隐含了最核心的设计思想。
下面我们尝试分析下 Kafka 的消息模型,看看它究竟是如何演化来的?
首先,为了将一份消息数据分发给多个消费者,并且每个消费者都能收到全量的消息,很自然的想到了广播。
紧接着问题出现了:来一条消息,就广播给所有消费者,但并非每个消费者都想要全部的消息,比如消费者 A 只想要消息1、2、3,消费者 B 只想要消息4、5、6,这时候该怎么办呢?
这个问题的关键点在于:MQ 不理解消息的语义,它根本无法做到对消息进行分类投递。
此时,MQ 想到了一个很聪明的办法:它将难题直接抛给了生产者,要求生产者在发送消息时,对消息进行逻辑上的分类,因此就演进出了我们熟知的 Topic 以及发布-订阅模型。
这样,消费者只需要订阅自己感兴趣的 Topic,然后从 Topic 中获取消息即可。
但是这样做了之后,仍然存在一个问题:假如多个消费者都对同一个 Topic 感兴趣(如下图中的消费者 C),那又该如何解决呢?
如果采用传统的队列模式(单播),那当一个消费者从队列中取走消息后,这条消息就会被删除,另外一个消费者就拿不到了。
这个时候,很自然又想到下面的解决方案:
也就是:当 Topic 每增加一个新的消费者,就「复制」一个完全一样的数据队列。
这样问题是解决了,但是随着下游消费者数量变多,将引发 MQ 性能的快速退化。尤其对于 Kafka 来说,它在诞生之初就是处理大数据场景的,这种复制操作显然成本太高了。
这时候,就有了 Kafka 最画龙点睛的一个解法:它将所有消息进行了持久化存储,由消费者自己各取所需,想取哪个消息,想什么时候取都行,只需要传递一个消息的 offset 即可。
这样一个根本性改变,彻底将复杂的消费问题又转嫁给消费者了,这样使得 Kafka 本身的复杂度大大降低,从而为它的高性能和高扩展打下了良好的基础。(这是 Kafka 不同于 ActiveMQ 和 RabbitMQ 最核心的地方)
最后,简化一下,就是下面这张图:
这就是 Kafka 最原始的消息模型。
这也间接解释了第二章节中:为什么官方会将 Kakfa 同时定义成存储系统的原因。
Kafka架构设计的任督二脉
下面我们再接着分析下:Kafka 究竟是如何解决存储问题的?
面对海量数据,单机的存储容量和读写性能肯定有限,大家很容易想到一种存储方案:对数据进行分片存储。这种方案在我们实际工作中也非常常见:
比如数据库设计中,当单表的数据量达到几千万或者上亿时,我们会将它拆分成多个库或者多张表。
比如缓存设计中,当单个 Redis 实例的数据量达到几十个 G 引发性能瓶颈时,我们会将单机架构改成分片集群架构。
类似的拆分思想在 HDFS、ElasticSearch 等中间件中都能看到。
Kafka 也不例外,它同样采用了这种水平拆分方案。在 Kafka 的术语中,拆分后的数据子集叫做 Partition(分区),各个分区的数据合集即全量数据。
我们再来看下 Kafka 中的 Partition 具体是如何工作的?举一个很形象的例子,如果我们把「Kafka」类比成「高速公路」:
当大家听到京广高速的时候,知道这是一条从北京到广州的高速路,这是逻辑上的叫法,可以理解成 Kafka 中的 Topic(主题)。
一条高速路通常会有多个车道进行分流,每个车道上的车都是通往一个目的地的(属于同一个Topic),这里所说的车道便是 Partition。
这样,一条消息的流转路径就如下图所示,先走主题路由,然后走分区路由,最终决定这条消息该发往哪个分区。
其中分区路由可以简单理解成一个 Hash 函数,生产者在发送消息时,完全可以自定义这个函数来决定分区规则。如果分区规则设定合理,所有消息将均匀地分配到不同的分区中。
通过这样两层关系,最终在 Topic 之下,就有了一个新的划分单位:Partition。先通过 Topic 对消息进行逻辑分类,然后通过 Partition 进一步做物理分片,最终多个 Partition 又会均匀地分布在集群中的每台机器上,从而很好地解决了存储的扩展性问题。
因此,Partition 是 Kafka 最基本的部署单元。本文之所以将 Partition 称作 Kafka 架构设计的任督二脉,基于下面两点原因:
1、Partition 是存储的关键所在,MQ「一发一存一消费」的核心流程必然围绕它展开。
2、Kafka 高并发设计中最难的三高问题都能和 Partition 关联起来。
因此,以 Partition 作为根,能很自然地联想出 Kafka 架构设计中的各个知识点,形成可靠的知识体系。
下面,请大家继续跟着我的思路,以 Partition 为线索,对 Kafka 的宏观架构进行解析。
Kafka的宏观架构设计
接下来,我们再看看 Partition 的分布式能力究竟是如何实现的?它又是怎么和 Kafka 的整体架构关联起来的?
前面讲过 Partition 是 Topic 之下的一个划分单位,它是 Kafka 最基本的部署单元,它将决定 Kafka 集群的组织方式。
假设现在有两个 Topic,每个 Topic 都设置了两个 Partition,如果 Kafka 集群是两台机器,部署架构将会是下面这样:
可以看到:同一个 Topic 的两个 Partition 分布在不同的消息服务器上,能做到消息的分布式存储了。但是对于 Kafka 这个高并发系统来说,仅存储可扩展还不够,消息的拉取也必须并行才行,否则会遇到极大的性能瓶颈。
那我们再看看消费端,它又是如何跟 Partition 结合并做到并行处理的?
从消费者来看,首先要满足两个基本诉求:
1、广播消费能力:同一个 Topic 可以被多个消费者订阅,一条消息能够被消费多次。
2、集群消费能力:当消费者本身也是集群时,每一条消息只能分发给集群中的一个消费者进行处理。
为了满足这两点要求,Kafka 引出了消费组的概念,每个消费者都有一个对应的消费组,组间进行广播消费,组内进行集群消费。此外,Kafka 还限定了:每个 Partition 只能由消费组中的一个消费者进行消费。
最终的消费关系如下图所示:假设主题 A 共有 4 个分区,消费组 2 只有两个消费者,最终这两个消费组将平分整个负载,各自消费两个分区的消息。
如果要加快消息的处理速度,该如何做呢?也很简单,向消费组 2 中增加新的消费者即可,Kafka 将以 Partition 为单位重新做负载均衡。当增加到 4 个消费者时,每个消费者仅需处理 1 个 Partition,处理速度将提升两倍。
到这里,存储可扩展、消息并行处理这两个难题都解决了。但是高并发架构设计上,还遗留了一个很重要的问题:那就是高可用设计。
在 Kafka 集群中,每台机器都存储了一些 Partition,一旦某台机器宕机,上面的数据不就丢失了吗?
此时,你一定会想到对消息进行持久化存储,但是持久化只能解决一部分问题,它只能确保机器重启后,历史数据不丢失。但在机器恢复之前,这部分数据将一直无法访问。这对于高并发系统来说,是无法忍受的。
所以 Kafka 必须具备故障转移能力才行,当某台机器宕机后仍然能保证服务可用。
如果大家去分析任何一个高可靠的分布式系统,比如 ElasticSearch、Redis Cluster,其实它们都有一套多副本的冗余机制。
没错,Kafka 正是通过 Partition 的多副本机制解决了高可用问题。在 Kafka 集群中,每个 Partition 都有多个副本,同一分区的不同副本中保存的是相同的消息。
副本之间是 “一主多从” 的关系,其中 leader 副本负责读写请求,follower 副本只负责和 leader 副本同步消息,当 leader 副本发生故障时,它才有机会被选举成新的 leader 副本并对外提供服务,否则一直是待命状态。
现在,我假设 Kafka 集群中有 4 台服务器,主题 A 和主题 B 都有两个 Partition,且每个 Partition 各有两个副本,那最终的多副本架构将如下图所示:
很显然,这个集群中任何一台机器宕机,都不会影响 Kafka 的可用性,数据仍然是完整的。
理解了上面这些内容,最后我们再反过来看下 Kafka 的整体架构:
1、Producer:生产者,负责创建消息,然后投递到 Kafka 集群中,投递时需要指定消息所属的 Topic,同时确定好发往哪个 Partition。
2、Consumer:消费者,会根据它所订阅的 Topic 以及所属的消费组,决定从哪些 Partition 中拉取消息。
3、Broker:消息服务器,可水平扩展,负责分区管理、消息的持久化、故障自动转移等。
4、Zookeeper:负责集群的元数据管理等功能,比如集群中有哪些 broker 节点以及 Topic,每个 Topic 又有哪些 Partition 等。
很显然,在 Kafka 整体架构中,Partition 是发送消息、存储消息、消费消息的纽带。吃透了它,再去理解整体架构,脉络会更加清晰。
总结
本文以 Partition 为切入点,从宏观角度解析了 Kafka 的整体架构,再简单总结下本文的内容:
- Kafka 通过巧妙的模型设计,将自己退化成一个海量消息的存储系统。
- 为了解决存储的扩展性问题,Kafka 对数据进行了水平拆分,引出了 Partition(分区),这是 Kafka 部署的基本单元,同时也是 Kafka 并发处理的最小粒度。
- 对于一个高并发系统来说,还需要做到高可用,Kafka 通过 Partition 的多副本冗余机制进行故障转移,确保了高可靠。