购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.4 Kafka消息中间件

Apache Kafka是一种分布式的、基于发布/订阅的消息系统,由Scala语言编写而成。它具有快速、可扩展、可持久化的特点。Kafka最初由LinkedIn开发,并于2011年年初开源,2012年10月从Apache孵化器毕业,成为Apache基金会的顶级项目。

Kafka作为一款优秀的消息中间件,具有近乎实时的消息处理能力。它虽然基于磁盘进行消息的存储,但是基于顺序读写的方式访问磁盘,实现了数据最优的读写;此外它还对消息进行批量压缩,降低了高并发情况下的数据量,提高了网络传输的效率;Kafka同样支持消息分区,实现分区内部的消息顺序传输(跨分区不能保证),进一步提高了并发的能力。

由于以上特性,Kafka被广泛用于各种高并发、实时的应用场景中,例如日志收集、大数据实时计算等。

4.4.1 Kafka基础概念

Kafka作为非常流行的消息中间件,与市面上主流的消息中间件产品具有很多类似的概念,了解这些概念能够帮助我们更好地理解Kafka的设计思想,也能够帮助我们理解其他类似的消息中间件产品。

总的来看,任何消息中间件从逻辑上主要分为3个角色,分别是生产者(Producer)、数据传输以及消费者(Consumer)。特定的生产者产生的消息经过特定的通道(Topic)传输到指定的消费者中,并且在这个过程中尽可能地保证消息传输的时序性。

Kafka需要部署在具体的服务器上(物理主机或者云服务器),当启动该服务器上部署的Kafka服务之后,从逻辑上来看这台服务器就构成了一个Broker,即接收生产者推送过来的消息,并按照某种规则推送到某个消费者中。如果这个Kafka是集群,那么就存在多个Broker,这个时候Broker也要处理集群内其他Broker的请求。

生产者在消息队列中的主要作用就是生产消息,并按照特定的规则推送到具体的Topic中;消费者主要负责订阅该Topic中的消息,然后进行消费;在Kafka中多个消费者可以构成一个消费者组(Consumer Group),一个消费者只能属于一个消费者组,同时一个消费者群只能订阅一个Topic,但是一个Topic可以被多个消费者组订阅。

Topic可以理解为消息的集合,一个或者多个生产者可以往同一个Topic推送消息,同理,一个Topic也可以被多个消费者同时消费。然而Topic作为Kafka中核心的概念之一,它主要是由多个分区(Partition)构成的,每个分区之间的消息是不重复的,并且Kafka只能保证同一个分区内部消息的时序性,即不同分区之间的消息并不存在统一的时序性,换句话说,局部有序而非全局有序。

分区具有跨Broker的特性,即同一个Topic下的不同分区可以属于不同的Broker。分区是Kafka实现并行处理能力的基础,即可以通过增加Broker提高整个Kafka集群的并发能力。同时每个分区都存在多个副本,来保证Kafka的高可用。

4.4.2 Kafka架构概述

基于上述内容,一个典型的Kafka架构如图4-9所示,可以看到每个分区中都存在多个副本。由于存在多个副本就会涉及副本之间的数据同步的问题,因此Kafka引入日志(Log)的概念。一个分区对应一个Log,一个Log由多个段(Segment)构成,段对应的是磁盘上的数据文件以及索引文件。Kafka按照顺序读写的方式对Segment进行读写,并限制其大小。

图4-9 典型的Kafka架构

同时,在集群环境中,需要引入一个新的组件ZooKeeper来存储Kafka元数据信息,例如消费者消费状态、消费者组的管理等。但是考虑到ZooKeeper存在一定的单点故障,在最新的Kafka版本中已逐步弱化该作用。

4.4.3 Kafka高性能原理

Kafka作为高并发消息中间件的代表,探究背后的原理可以帮助我们更加清楚地了解Kafka的特性,它在设计层面、消息存储以及传输过程中的优化是其达到高性能的根本原因。

1.采用NIO模式

传统的IO都是阻塞的I/O模型,即在读写的过程中,线程是无法完成其他事情的,进而导致一些资源的浪费。而在Java NIO(Non-blocking I/O)中,一个线程从某通道发送请求读取或者写入数据时,如果暂时无数据可读或者无通道可以写入,那么它不会继续等待,而会进行其他任务,直到数据可以继续读取或者写入为止。

这里涉及一个问题,线程如何知道之前的数据可以继续读取。为了避免频繁的轮询状态,NIO采用React模式,即以事件驱动的方式去通知线程,并且通知的方式是非阻塞行为。具体的NIO模型如图4-10所示。

图4-10 NIO模型

Kafka不仅采用了NIO模型,还进行了部分优化,它将读、写请求对应的线程操作都变成多线程操作,因为在单线程操作的情况下,线程阻塞将会导致读或写的请求都无法满足高并发的要求。同时,为了将内部的业务逻辑与网络读写的逻辑进行拆分设计,Kafka内部构建了一个消息队列(Message Queue)用以将业务与网络解耦,从而实现最大的并发。

此外,为了避免单点的Selector造成时间分发过程中的单点障碍导致性能瓶颈,Kafka也将Selector进行了扩展,从而使整个Kafka具有接受高并发的数据分发能力,如图4-11所示。

图4-11 Kafka内部NIO模型

2.优化日志存储

网络层面的设计使Kafka有了高并发的基础,但是作为一个消息中间件必然涉及消息的读写,所以Kafka内部也针对这部分进行了一定的优化。

传统的磁盘写入主要分为两大类:随机写和顺序写。两者的主要区别在于寻址所需花费的时间不同。根据Kafka官网的说法,寻道时间大概在10ms左右,这个时间在高并发下情况带来的性能损耗还是较大的。同时前文也已经提到,Kafka每个通道下面存在多个分区,并且每个分区下面对应的日志段都是以顺序写的方式写入日志。为了提高文件读取的效率,Kafka为每个日志段文件都创建了一个索引文件,当Kafka有消息数据产生进入分区中,日志段将会写入消息以及更新索引文件。

3.减少数据交互

如果按照传统逻辑,Kafka从磁盘中将数据读出然后通过网络传输到消费端的整体流程如图4-12所示。

图4-12 传统数据文件读取与网络传输方式

一个简单的数据发送操作,共发生了4次用户空间与内核空间的上下文切换以及4次数据拷贝,这些不必要的损耗将会影响高并发系统的数据传输的效率,所以要想提高文件传输的性能,就需要减少用户空间与内核空间的上下文切换和内存拷贝的次数。

这里就得提到零拷贝技术(3.1.1节已有相关介绍,这里不再展开),该技术可以极大地减少无谓的数据复制操作,同时减少上下文切换导致的时间损耗。Kafka内部通过该技术,将磁盘的数据复制到页缓存后,在内核空间中完成数据的传输。假设当前有100个消费者需要读取同一条消息,采用传统的数据读取方式时需要复制400次(4×100),而采用零拷贝技术时只需要复制101(1+100)次,复制次数缩减到原先的四分之一,同时减少了不必要的上下文切换操作。零拷贝技术的逻辑示意图如图4-13所示。

图4-13 零拷贝技术的逻辑示意图

4.构建时间轮

Kafka将消息发出,等待消费端处理完之后才会返回结果,同时对于部分存在超时参数的请求,如果超过指定时间消息还未处理完成,则Kafka需要发送消息给客户端通知它消息消费超时。针对上述场景,Kafka内部需要维护一些延迟的消息队列去定时处理这部分消息,维护消息的状态。

在Java内部存在两种比较常见的延迟队列,分别是Timer以及DelayedQueue,这两种队列的消息存储操作的时间复杂度是 O n log( n )),这种时间复杂度在高并发的情况下是不能满足性能的要求的,所以Kafka内部实现了时间轮(TimingWheel)算法,将定时任务的数据存储的复杂度降低到 O (1)。

Kafka内部实现的时间轮算法叫作基于Hash的层级时间轮算法(Hashed and Hierarchical),它主要由两部分构成,使用数组实现的环形队列以及环形双向链表。其中环形队列用于存储定时任务,而环形双向链表用于存储具体的可以被执行的定时任务。通过这两种巧妙的设计,Kafka将时间复杂度降低到 O (1),满足了高并发情况下的定时任务的存储要求。

4.4.4 Kafka与其他中间件对比

从上面的章节可以了解到Kafka的高并发能力是从网络、存储以及消息传输等各个环节都进行优化才达到的。但是上面的这些信息并不是Kafka作为一款优秀的消息中间件的全部,例如消息的副本机制、日志管理、延迟处理等都是它较为核心的部分,由于篇幅限制,本章主要从高性能的角度去阐述Kafka所做的优化。

接下来我们针对市面上常用的消息中间件进行对比,如表4-5所示。

表4-5 不同消息中间件对比 yjIbbm3njAcwIOVW9eVLCPU7pcG1I7gIGSc+cfPv9KbzNzZpwkeu/jWfRc8b87zI

点击中间区域
呼出菜单
上一章
目录
下一章
×