消息队列(Message Queue)是一种跨进程的通信机制,用于在分布式系统中发送、存储和接收消息。它允许不同的应用程序或服务以异步的方式进行数据交换。以下是消息队列的一些基本概念:
消息:消息是通信的基本单位,通常包含两部分:头部(metadata),如发送者信息、时间戳等;以及主体(payload),即实际要传输的数据内容。
生产者/发布者:指的是发送消息的应用程序或服务。生产者创建消息并将其发送到消息队列中。
消费者/订阅者:指的是接收消息的应用程序或服务。消费者从消息队列中读取消息,并根据消息的内容执行相应的操作。
队列:是一个临时存储消息的地方,用于保存生产者发送的消息直到这些消息被消费者处理。队列遵循先进先出(FIFO, First In First Out)原则,除非特别配置。
Broker(代理):在某些消息队列实现中,会有一个中间件称为broker,负责消息的传递、存储及路由。它作为生产者和消费者之间的桥梁,确保消息能够正确地从生产者发送到消费者。
持久化与非持久化:消息队列中的消息可以配置为持久化的或非持久化的。持久化消息即使在系统崩溃后也能恢复,而非持久化消息则不会在系统故障时保留。
广播与点对点:消息队列支持两种主要的消息分发模式。广播模式下,每条消息会被发送给所有订阅了该主题的消费者;而在点对点模式下,每条消息只会被一个消费者处理。
事务支持:一些消息队列系统提供了事务支持,确保一组消息要么全部成功提交,要么全部不提交,以此来保证数据的一致性和完整性。
使用消息队列有助于解耦应用组件、提高系统的可扩展性、增强系统的容错能力,并且能够平衡负载,使得不同速度的服务之间能够高效协作。常见的消息队列产品包括RabbitMQ、Apache Kafka、ActiveMQ等。
解耦服务:通过消息队列,生产者和消费者可以独立地部署、扩展和开发。例如,在电子商务系统中,订单服务生成订单后,可以通过消息队列通知库存服务更新库存,而无需直接调用库存服务的接口。
异步处理:对于一些不需要立即响应的操作,可以采用异步的方式进行处理,从而提高系统的响应速度。比如用户注册成功后发送欢迎邮件,这个操作就可以通过消息队列异步执行,而不必等待邮件发送完成后再返回给用户注册成功的提示。
流量削峰:在高并发的情况下,如秒杀活动或节假日促销,短时间内大量请求可能会导致系统过载。使用消息队列可以在流量高峰期缓冲这些请求,平滑地将任务分配给后台处理,保护系统免受瞬间高峰流量的影响。
Kafka 是一个分布式的基于发布/订阅模式的消息队列(Message Queue),主要应用与大数据实时处理领域。
Kafka 存储的消息来自任意多被称为 Producer 生产者的进程。数据从而可以被发布到不同的 Topic 主题下的不同 Partition 分区。
在一个分区内,这些消息被索引并连同时间戳存储在一起。其它被称为 Consumer 消费者的进程可以从分区订阅消息。
Kafka 运行在一个由一台或多台服务器组成的集群上,并且分区可以跨集群结点分布。
下面给出 Kafka 一些重要概念,让大家对 Kafka 有个整体的认识和感知:
Producer: 消息生产者,向 Kafka Broker 发消息的客户端。
Consumer:消息消费者,从 Kafka Broker 取消息的客户端。
Consumer Group:消费者组(CG),消费者组内每个消费者负责消费不同分区的数据,提高消费能力。一个分区只能由组内一个消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
Broker:一台 Kafka 机器就是一个 Broker。一个集群由多个 Broker 组成。一个 Broker 可以容纳多个 Topic。
Topic:可以理解为一个队列,Topic 将消息分类,生产者和消费者面向的是同一个 Topic。
Partition:为了实现扩展性,提高并发能力,一个非常大的 Topic 可以分布到多个 Broker (即服务器)上,一个 Topic 可以分为多个 Partition,每个 Partition 是一个 有序的队列。
Replica:副本,为实现备份的功能,保证集群中的某个节点发生故障时,该节点上的 Partition 数据不丢失,且 Kafka 仍然能够继续工作,Kafka 提供了副本机制,一个 Topic 的每个分区都有若干个副本,一个 Leader 和若干个 Follower。
Leader:每个分区多个副本的“主”副本,生产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。
Follower:每个分区多个副本的“从”副本,实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个 Follower 还会成为新的 Leader。
Offset:消费者采用pull的方式从broker上拉取消息进行消费,该值记录消费的位置信息,当消费者挂掉再重新恢复的时候,可以从消费位置继续消费。
Zookeeper:Kafka 集群能够正常工作,需要依赖于 Zookeeper,Zookeeper 帮助 Kafka 存储和管理集群信息。
Consumer Group:Kafka按照消费组来消费消息,一个消费组下面的机器组成一个Consumer Group,每条消息只能被该Consumer Group一个Consumer消费。不同的Consumer Group可以消费同一条消息。
消息状态:在Kafka中,消息是否被消费的状态保存在Consumer中,Broker不会关心是否被消费或被谁消费,Consumer会记录一个offset值(指向partition中吓一跳将要被消费的消息位置)。
消息持久化:Kafka会把消息持久化到本地文件,并且具有极高性能(零拷贝)。
批量发送:Kafka支持以消息集合为单位进行批量发送,以提高效率。
Push-and-Pull: Producer向Broker push消息,Consumer从Broker pull消息。
分区机制(Partition):
Kafka 的 Topic 被划分为多个分区(Partition),每个分区是一个有序、不可变的消息队列。分区的主要作用包括:
提供并行处理能力:不同的分区可以分布在不同的 Broker 上,从而支持高吞吐量。
保证消息顺序:在一个分区内,消息是严格按顺序存储和消费的。
支持水平扩展:通过增加分区数,可以提升系统的并发处理能力。
提供数据备份:支持数据容灾能力,支持服务高可用
在 Kafka 中,每条消息(也称为记录)都有一个明确的格式和组成部分。以下是 Kafka 消息的基本结构:
Offset:每个消息在它所属的分区中都有一个唯一的序列号,称为 Offset。这个数字用于标识消息在分区中的位置。
Message Size:表示消息体的大小,以字节为单位。这有助于消费者知道需要读取多少数据。
Key(可选):消息键,可以为空。如果定义了 Key,它可以用来确定消息如何被路由到特定的分区。Kafka 使用 Key 的哈希值来决定消息应该放在哪个分区,从而保证具有相同 Key 的消息总是被发送到同一个分区。
Value:这是实际的消息内容,即要传输的数据。它可以是任何类型的序列化后的数据,如字符串、JSON、Avro 等。
Headers(可选):从 Kafka 0.11.0 版本开始引入,允许用户为消息添加元数据信息。这些元数据是以键值对的形式存在,可以用来携带关于消息的额外信息而不影响消息的实际内容。
Timestamp:表示消息创建的时间或当消息到达 broker 时的时间戳。时间戳对于日志压缩、基于时间的查询等功能非常重要。
分区选择策略
如果消息没有指定 Key(即 key == null)
轮询(Round-Robin)策略(默认选用),将消息均匀地分配到所有可用分区中。
随机策略:从分区中随机选择一个
如果消息指定了 Key,则根据 Key 的哈希值计算目标分区:
partition = hash(key) % numPartitions
自定义策略
Kafka 允许用户通过实现 org.apache.kafka.clients.producer.Partitioner 接口来自定义分区逻辑。例如,可以根据业务规则将某些类型的消息路由到特定的分区。
Consumer Group保存了自己的位移信息,只需要一个简单的整数表示位置就可以了。
老版本的位移是提交到Zookeeper中,但是Zookeeper不适合记性大批量的读写操作,尤其是写操作。
从0.9版本开始kafka增加了__consumer_offsets这个Topic,将Offset这个信息写入Topic,这样就不需要以来Zookeeper。
Consumer Group采用pull的方式来消费消息,那么每个Consumer该消费哪个Partition的消息需要一套严格的机制来保证,防止各Consumer间重复消费消息。而且,Partition是可以水平无限扩展的,随着Partition的扩展,Consumer消费的Partition也会重新分配,在Kafka内部有两种默认的消费分区分配策略:Range和RoundRobin。
当发生以下事件时,Kafka会重新进行分配:
同一个Consumer Group内新增消费者
消费者离开当前所属的Group,比如机器Shutdown或者Crash
订阅的主题新增Partition
Range策略
工作原理:
RangeAssignor 是 Kafka 的默认分区分配策略。
它按照主题的分区范围(Range)将分区分配给消费者。
具体步骤如下:
将每个主题的所有分区按顺序编号(例如 P0, P1, P2, …, Pn)。
按照消费者的字典顺序排序(例如 C0, C1, C2)。
根据消费者数量计算每个消费者应分配的分区范围。
分区总数 ÷ 消费者总数 = 每个消费者分到的分区数(整数部分)。
剩余的分区会依次分配给前面的消费者。
最终,每个消费者得到一个连续的分区范围。
示例
假设有一个 Topic 包含 8 个分区(P0-P7),消费者组中有 3 个消费者(C0, C1, C2)。使用 RangeAssignor 策略时,分区分配如下:
8 / 3 = 2 ,每个消费者分到2个分区
按顺序分配分区:
C0: P0, P1(基本分区)+ P2(剩余分区的第一个)
C1: P3, P4(基本分区)+ P5 (剩余分区的第二个)
C2: P6, P7(基本分区)
最终分配结果:
C0: P0, P1, P2
C1: P3, P4, P5
C2: P6, P7
RoundRobin策略
工作原理:
RoundRobinAssignor 使用轮询的方式将所有主题的分区均匀地分配给消费者。
具体步骤如下:
将所有主题的所有分区按顺序排列。
按照消费者的字典顺序排序。
使用轮询的方式依次为每个消费者分配分区。
直到所有分区都被分配完毕。
示例:
假设有一个 Topic 包含 8 个分区(P0-P7),消费者组中有 3 个消费者(C0, C1, C2)。使用 RoundRobinAssignor 策略时,分区分配如下:
将所有分区按顺序排列:P0, P1, P2, P3, P4, P5, P6, P7。
按照消费者的字典顺序排序:C0, C1, C2。
使用轮询方式分配分区:
第一轮:C0 -> P0, C1 -> P1, C2 -> P2。
第二轮:C0 -> P3, C1 -> P4, C2 -> P5。
第三轮:C0 -> P6, C1 -> P7。
最终分配结果:
C0: P0, P3, P6
C1: P1, P4, P7
C2: P2, P5
总结:
策略 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
RangeAssignor | 实现简单,适合分区数量与消费者数量接近的场景 | 可能导致负载不均 | 分区数量与消费者数量接近 |
RoundRobinAssignor | 分区分配更加均匀 | 实现稍复杂 | 分区数量较多或涉及多主题的场景 |
对分布式系统来说,当集群规模上升到一定程度后,一台或多台机器宕机的风险会增加,Kafka采用多机备份和消息ACK应答机制,解决数据丢失问题,并通过一套失败恢复机制解决服务不可用问题
Kafka 每个主题(Topic)可以被划分为多个分区(Partition),而每个分区又可以有多个副本(Replica)。这些副本分布在不同的 Broker 上,其中一个副本作为 Leader 副本,其余的作为 Follower 副本。生产者总是向 Leader 副本写入数据,Follower 副本则从 Leader 异步拉取数据进行同步。
acks=0
:生产者不等待任何确认,消息可能丢失。acks=1
:生产者只需 Leader 副本确认收到消息即可。acks=all
或 acks=-1
:生产者需要所有同步副本(ISR)确认收到消息。acks=all
提供了最高的数据安全性,但也会增加延迟。假设有一个 Topic 包含三个副本(复制因子为 3),并且设置了 min.insync.replicas=2
和 acks=all
。在这种情况下:
当一个或多个 Broker 发生故障时,Kafka 依靠其内置的副本(Replication)和选举机制来恢复服务。
session.timeout.ms
控制),ZooKeeper 将删除该 Broker 的临时节点,从而触发故障检测。min.insync.replicas
:决定了必须有多少个副本确认接收到消息才能被视为已提交。这影响了系统在部分副本失败时的容错能力。unclean.leader.election.enable
:控制是否允许非 ISR 成员成为 Leader。默认情况下禁用此选项以避免潜在的数据丢失风险。Kafka 集群中的控制器(Controller)负责管理主题、分区、副本等资源的状态变化,如 Leader 选举、分区分配等任务。控制器本身也是一个 Broker,但它承担了额外的责任。
假设在一个 Kafka 集群中有三个 Broker(B1, B2, B3),其中 B1 是当前的控制器。现在考虑以下两种场景:
Kafka 通过批量发送消息的方式显著提高了吞吐量,减少了网络传输的开销。
batch.size
:控制每个批次的最大字节数。当累积的消息达到这个大小时,生产者会立即发送该批次。linger.ms
:指定生产者等待更多消息加入当前批次的时间。即使批次未满,也会在 linger.ms
时间后发送消息。
linger.ms
设置为 0,则只要消息生成就会立即发送。Kafka 使用顺序写入磁盘和操作系统缓存来实现高效的消息持久化,确保数据的安全性和高性能。
acks
参数:
acks=0
:生产者不等待任何确认,消息可能会丢失。acks=1
:生产者只需 Leader 副本确认收到消息即可。acks=all
或 acks=-1
:生产者需要所有同步副本(ISR)确认收到消息。flush.messages
和 flush.ms
:
零拷贝技术是 Kafka 实现高性能的关键之一,它通过减少数据在不同内存区域之间的复制次数来提高吞吐量。
在传统的数据传输过程中,数据需要经过多次拷贝:
Kafka 使用了 Linux 的 sendfile
系统调用来实现零拷贝。以下是零拷贝的工作流程:
sendfile
系统调用,数据直接从内核空间的页缓存发送到网络接口,而无需经过用户空间。优化点 | 实现方式 | 性能优势 |
---|---|---|
批量发送消息 | 将多条消息打包成一个批次发送,减少网络请求次数 | 减少网络开销,提高吞吐量,增强压缩效率 |
持久化消息 | 使用顺序写入磁盘和操作系统页缓存,结合灵活的刷盘策略 | 高吞吐量、低延迟、可靠的数据存储 |
零拷贝 | 利用 Linux 的 sendfile 系统调用,避免数据在用户空间和内核空间之间的拷贝 |
减少 CPU 开销,降低延迟,提高吞吐量 |