消息队列概述
消息队列是什么?
如图所示,我们将消息从客户端A发送到客户端B,本身是个1对1的过程,可以直接对接,消息是一次性发送的。但有些场景下我们需要让客户端A发出的数据同时发送给多个客户端,或者想让消息保存一段时间,以防止数据丢失,这时候我们就需要用到一个新的组件——消息队列。消息队列是在消息的传输过程中保存消息的容器。
消息队列的优势
既然我们选择使用消息队列,那么消息队列必然有着一些优势。比如:
-
解耦:允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。就是说发送方与接收方的客户端并不需要直接对接,降低组件之间的耦合度,使两端相对独立。
-
冗余:消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。这就是我们前面提到的,消息队列会保存一定的数据,如果接收端发生问题,数据会保存在消息队列中,确保数据不会丢失。
-
扩展性:因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。
-
销锋:这是消息队列非常重要的一个作用,使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。尤其是对于C端程序或网站,消息的高峰往往与业务高峰成正比,在业务高峰时消息量会激增,对于突然增大的消息量,下游组件可能会超负荷,导致整个系统的崩溃。而消息队列可以作为一个缓冲区,避免消息量过大对系统产生过量的负载。
-
顺序保证:这也是消息队列的一个重要作用,在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。
-
异步通信:消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它,然后在需要的时候再去处理它们。
常见消息队列
接下来我们认识几种常见的消息队列。消息队列的性能我们往往从以下几个维度进行考察:吞吐量、时效性、可用性。
ActiveMQ | RabbitMQ | RocketMQ | Kafka | |
---|---|---|---|---|
开发语言 | Java | erlang | Java | Scala |
单机吞吐量 | 万级 | 万级 | 十万级 | 十万级 |
时效性 | 毫秒级 | 微秒级 | 毫秒级 | 毫秒级 |
可用性 | 高(主从架构) | 高(主从架构) | 非常高(分布式架构) | 非常高(分布式架构) |
功能特性 | 成熟产品,对各种协议支持较好。 | 并发能力强,延迟低,管理界面丰富。 | MQ功能较完备,扩展性好。 | 只支持主要的MQ功能,对消息查询、消息回溯等功能没有提供,但性能更好,在大数据领域应用甚广。 |
![]() |
![]() |
![]() |
![]() |
常见的消息队列有ActiveMQ、RabbitMQ、RocketMQ、Kafka等。
- ActiveMQ是最成熟的产品,对各种协议的支持都比较好,用户基础大。但性能中规中矩。
- Rabbitmq以速度快著称,有着高并发、低延迟的特性。
- RocketMQ则是以更大的吞吐量大以及分布式架构带来的更高的可用性而闻名。
- Kafka同样是个高吞吐量的分布式消息队列,相较于前面提到的几种消息队列,kafka只保留了主要的消息队列功能,不提供消息查询、消息回溯等功能,但kafka的性能要比前面的同类产品更好,而且对于大数据组件的兼容性是所有消息队列中最好的。
Kafka概述
Kafka的定义
经过前面的比较,我们对kafka已经有了一个大体的认识,接下来我们来学习Kafka的定义。
Apache Kafka 是一个开源分布式事件流平台,被数千家公司用于高性能数据管道、流分析、数据集成和任务关键型应用程序。这段话是来自于kafka官网对它的介绍。
Kafka最初是由LinkedIn公司开发,并于2011年初开源。2012年10月从Apache 孵化器毕业,成为顶级项目。该项目的目标是为处理实时数据提供一个统一、高通量、低等待的平台。
通过前面的学习我们知道,Kafka是一个分布式消息队列。Kafka对消息保存时根据Topic进行归类,topic就是消息的主题,发送消息者称为Producer,消息接受者称为Consumer,此外kafka集群有多个kafka实例组成,每个实例(server)称为broker。这里大家先有对这些概念有一个大体的印象,后面我们会详细介绍。

无论是kafka集群,还是consumer都依赖于zookeeper集群保存一些meta信息,来保证系统可用性。所以说kafka是强依赖于Zookepper的。
接下来我们去看一下kafka官网:https://kafka.apache.org/
Kafka的基本角色
- Broker:一台kafka服务器就是一个broker,一个集群由多个broker组成。
- Producer:消息生产者,就是向kafka broker发消息的客户端。
- Consumer:消息消费者,向kafka broker取消息的客户端。
还有一个重要的组件不属于kafka内部,但kafka强依赖与它,就是zookeeper。Zookepper分布式应用程序协调服务,在 Kafka 中的作用有:Broker 注册、Topic 注册、Consumer 注册、Producer 和 Consumer 负载均衡、维护 Partition 与 Consumer 的关系、记录消息消费的进度。
Kafka原理
Kafka的内部结构
-
Topic :主题,可以理解为一个队列,每个生产者将消息发送到broker中的topic中,每个Topic中传输不同类型的数据;
-
Partition:分区,如图所示一个topic可以分为多个partition,不同partition可以分配到多个Broker上,图中topicA有两个分区,分别是partition0和partition1,两个分区又可以分配到不同broker之中;
-
Offset:消费偏移量,用来记录每个Partition中数据的消费位置。partition中的每条消息都会被分配一个有序的offset。通过offset,kafka能保证一个partition中的消息保持顺序发给consumer,要注意的是,kafka只能保证一个partition内的消息有序,不保证一个topic的整体(多个partition间)的顺序;
-
Consumer Group (CG):消费者组,消费者组是对应topic存在的,是kafka用来实现一个topic消息的广播(发给所有的consumer,1:N)和单播(发给任意一个consumer,1:1)的手段。
一个topic可以有多个消费者组,topic的消息会重复发送到所有的消费者组,但每个partition只会把消息发给该消费者组中的一个consumer。
用消费者组还可以将consumer进行自由的分组,从而实现对同一个topic中的数据进行多次消费。也就是说可以对一个topic创建多个消费者组,实现topic的多次消费。
消息生产
在了解了Kafka的内部结构之后,我们来学习一下kafka的工作流,看看kafka是怎样工作的。
推送
消息由producer产生,采用push的方法将消息推送给broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘,顺序写磁盘效率比随机写内存要高,保障kafka吞吐率。
分区
消息被发送到topic中,topic由一些partitionLog(分区日志)组织,每个partition中消息都是有序的,消息实际上是被追加到了每个PartitionLog中,其中每一消息都被赋予了一个offset值。这就是kafka的分区机制。
分区的原因:
- 每个Partition都可以根据所在机器进行灵活调整,因此整个集群都可以适应任意大小的数据。
- 数据读写的单位可以是Partition,提高了系统的并发量。
分区的原则:
- 指定了patition,则直接使用;
- 未指定patition但指定key,通过对key的value进行hash出一个patition;
- patition和key都未指定,使用轮询选出一个patition。
副本
接下来是kafka的副本机制。前面我们说到一个topic被划分为多个Partition,一个partition又可能拥有多个replication,副本的多少取决于topic创建时指定的分区数,默认分区数在kafka配置文件中的default.replication.factor
处修改。
当一个分区存在多个副本时,系统会在这些副本中选出一个作为leader,其他的副本作为follower,producer和consumer想要操作这个partition时都是与这个leader进行交互,follower从leader副本中复制数据。follower作为leader副本的备份,当leader不可用时,将从follower副本中重新选出一个leader,以保证数据的可用。
消息写入
消息的写入流程
- producer先从zookeeper的
/brokers/.../state
节点找到该partition的leader - producer将消息发送给该leader
- leader将消息写入本地log
- followers从leader pull消息,写入本地log后向leader发送ACK
- leader收到所有ISR中的replication的ACK后,增加HW(high watermark,最后commit 的offset)并向producer发送ACK
消息的保存
存储方式:
我们前面说到,消息被保存在topic中,topic在又被划分为多个partition。在磁盘上,每个partition被划分为一个目录,该目录存储该partition中所有的消息和索引文件。
存储策略:
我们说kafka可以保存消息,无论是否被消费。但消息队列终究不是数据库,不是用来长时间保存数据的,因此需要有删除策略对消息进行定期删除。Kafka支持两种删除策略,分别是基于时间与基于大小,基于时间的删除策略通过配置log.retention.hours
,基于大小的删除策略通过配置log.retention.bytes
进行具体修改。
需要注意的是,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。
消息的消费
kafka提供了两套consumer API:高级Consumer API和低级Consumer API。
高级API
优点:
- 高级API 写起来简单;
- 不需要自行去管理offset,系统通过zookeeper自行管理;
- 不需要管理分区,副本等情况,系统自动管理;
- 消费者断线会自动根据上一次记录在zookeeper中的offset去接着获取数据(默认设置1分钟更新一下zookeeper中存的offset);
- 可以使用group来区分对同一个topic 的不同程序访问分离开来(不同的group记录不同的offset,这样不同程序读取同一个topic才不会因为offset互相影响)。
缺点:
-
不能自行控制offset(对于某些特殊需求来说);
-
不能细化控制如分区、副本、zk等。
低级 API
优点:
-
能够让开发者自己控制offset,想从哪里读取就从哪里读取。
-
自行控制连接分区,对分区自定义进行负载均衡
-
对zookeeper的依赖性降低(如:offset不一定非要靠zk存储,自行存储offset即可,比如存在文件或者内存中)
缺点:
- 太过复杂,需要自行控制offset,连接哪个分区,找到分区leader 等。
消费者组
消费者是以consumer group消费者组的方式工作,由一个或者多个消费者组成一个组,共同消费一个topic。每个分区在同一时间只能由group中的一个消费者读取,但是多个group可以同时消费这个partition。
在图中,有一个由三个消费者组成的group,有一个消费者读取主题中的两个分区,另外两个分别读取一个分区。某个消费者读取某个分区,也可以叫做某个消费者是某个分区的拥有者。在这种情况下,消费者可以通过水平扩展的方式同时读取大量的消息。另外,如果一个消费者失败了,那么其他的group成员会自动负载均衡读取之前失败的消费者读取的分区。
消费方式
Consumer采用pull的模式从broker中读取数据,这一点很重要。如果由broker采用push模式进行消息推送,push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。所以由consumer自己去pull数据可以更好的适配自身的消费速率。
pull模式也不是完美的,如果kafka没有数据,消费者可能会陷入循环中,一直等待数据到达。为了避免这种情况,我们在我们的拉请求中有参数,允许消费者请求在等待数据到达的“长轮询”中进行阻塞(并且可选地等待到给定的字节数,以确保大的传输大小)。