1.什么是消息队列?
消息队列Message Queue,简称MQ。 是一种应用间的通信方式,主要由三个部分组成。
生产者:Producer,是消息的产生者与调用端,主要负责消息所承载的业务信息的实例化是一个队列的发起方。
代理:Broker,主要的处理单元,负责消息的存储、投递、及各种队列附加功能的实现,是消息队列最核心的组成部分。
消费者:Consumer,一个消息队列的终端也是消息的调用端,具体是根据消息承载的信息,处理各种业务逻辑。
消息队列的应用场景较多,常用的可以分为三种:
- 异步处理
主要应用于对实时性要求不严格的场景,比如:用户注册发送验证码、下单通知、发送优惠券等等。服务方只需要把协商好的消息发送到消息队列,剩下的由消费消息的服务去处理,不用等待消费服务返回结果。
2.应用解耦
应用解耦可以看作是把相关但耦合度不高的系统联系起来。比如订单系统与 WMS、EHR 系统,有关联但不那么紧密,每个系统之间只需要把约定的消息发送到 MQ,另外的系统去消费即可。解决了各个系统可以采用不同的架构、语言来实现,从而大大增加了系统的灵活性。
3.流量削峰
流量削峰一般应用在大流量入口且短时间内业务需求处理不完的服务中心,为了权衡高可用,把大量的并行任务发送到 MQ 中,依据MQ 的存储及分发功能,平稳的处理后续的业务,起到一个大流量缓冲的作用。
消息队列技术选型:目前市面上常见的消息队列中间件主要有ActiveMQ、RabbitMQ、Kafka、RocketMQ 这几种,在架构技术选型的时候一般根据业务的需求选择合适的中间件:比如中小型公司,低吞吐量的一般用 ActiveMQ、RabbitMQ 较为合适,大数据高吞吐量的大型公司一般选用 Kafka 和RocketMQ。
2. Kafka中消费组的概念及作用
消费组的概念
消费者分组是一组共同消费某一主题(或多个主题)的消费者集合。每个消费者都通过配置 group.id 属性归属到一个特定的分组中。
消费组的作用
分区分配: 当一个主题拥有多个分区时,Kafka 确保同一分组内的消费者不会同时消费同一个分区的数据。也就是说,一个分区在同一时刻只会被分组内的一个消费者消费。Kafka 使用一种分区分配策略(默认为基于范围或基于轮询的策略),确保分组内部的分区负载均衡。
负载均衡与伸缩性: 当分组内的消费者数量发生变化(例如增加或减少消费者实例)时,Kafka 可以通过消费者组协调机制重新分配分区给消费者,从而实现水平扩展或收缩。新的消费者加入时会接管一部分分区,离开的消费者所负责的分区则会被其他存活的消费者接管。
消息消费的唯一性与幂等性: 对于同一分组内的消费者来说,每条消息只会被其中一个消费者消费一次,这是由于每个分区的消息只能由一个消费者消费,从而实现了消息的 exactly-once 或 at-least-once 语义(取决于具体配置)。不同分组的消费者可以消费同样的消息,这适用于多份数据处理或备份的需求。
消费者偏移量管理: 消费者组还负责管理每个消费者在各分区的消费进度(偏移量)。Kafka 会跟踪每个消费者组在各个分区的消费位置。当消费者断开连接并重新连接时,可以根据其所在分组的偏移量信息继续从上次停止的地方开始消费。
3. Kafka 如何保证消息不丢失?
由于Kafaka由Producer、 Consumer、Broker 组成,所以保证消息不丢失这个问题,有从这三部分来考虑和实现。
生产端
首先是生产端,需要确保消息能够到达服务端并实现消息存储,在这个层面,有可能出现网络问题,导致消息发送失败,所以,针对生产端,可以通过 2 种方式来避免消息丢失:
- Producer 默认是异步发送消息,这种情况下要确保消息发送成功,可以添加异步回调函数来监听消息发送的结果,如果发送失败,可以在回调中重试。
- Producer 本身提供了一个重试参数retries,如果因为网络问题或者 Broker 故障导致发送失败,Producer 会自动重试。
服务端
服务端需要确保生产端发送过来的消息不会丢失,也就是只需要把消息持久化到磁盘就可以了。(如图)
但是,Kafka 为了提升性能,采用了异步批量刷盘的实现机制,也就是说按照一定的消息量和时间间隔来刷盘,而最终刷新到磁盘的这个动作,是由操作系统来调度的,所以如果在刷盘之前系统崩溃,就会导致数据丢失。
针对这个问题,需要通过Partition的副本机制和acks 机制来一起解决:
-
Partition 副本机制,它是针对每个数据分区的高可用策略,每个 partition 副本集包含唯一的一个 Leader 和多个 Follower,Leader 专门处理事务类的请求,Follower 负责同步Leader 的数据。在这样的一种机制的基础上,kafka 提供了一个acks 的参数,Producer 可以设置acks参数再结合Broker 的副本机制来个共同保障数据的可靠性。
-
acks参数控制了数据持久化到Kafka集群后的确认级别,不同的配置会影响到消息的可靠性以及潜在的吞吐量和延迟。以下是acks参数的几种不同配置及其含义和区别:-
acks=0:
-
生产者在发送完消息后不需要等待任何来自Kafka Broker的确认就认为消息已经发送成功。
-
这种配置提供了最低的延迟,但也是最不可靠的,因为即使消息实际上并没有被任何Broker成功接收,生产者也会认为消息发送成功。
-
acks=1:
-
生产者只需等待集群中分区的领导者(Leader)节点确认消息就已经被接收即可。
-
这意味着只要消息被写入到了分区的首领副本,生产者就会收到确认并认为消息发送成功。
-
这种配置保证了消息至少到达了Kafka集群中的一个节点,但如果在这之后首领节点崩溃并且消息尚未完全复制到其他同步副本(ISR,In-Sync Replicas),那么这部分消息可能会丢失。
-
acks=all 或 acks=-1:
- 生产者需等待ISR列表中的所有副本都成功地接收到消息才会认为消息发送成功。
- 这是最安全的配置,因为它确保了消息在发生故障时仍然可用,因为消息已经被所有同步副本接收。
- 但是,这种配置会引入更高的延迟,因为生产者必须等待所有副本都完成了数据同步,而且这也会影响系统的总体吞吐量。
-
综上所述,acks参数的选择是在消息传递的可靠性、延迟和吞吐量之间做权衡的过程。在需要极高数据完整性的场景下,通常会选择acks=all,而在对延迟敏感且能容忍一定程度数据丢失的情况下,则可能选择acks=1甚至acks=0。
消费端
消费端用offset来记录消息消费的进度,每次消费完消息都会更新并提交offset,因此的通过offset机制可以保证消费消息不会丢失。
4. Kafka如何保证消息不会重复消费
生产端
生产端发送消息后,服务端已经收到消息了,但是假如遇到网络问题,无法获得响应,生产端就无法判断该消息是否成功提交到了 Kafka,而我们一般会配置重试次数,但这样会引发生产端重新发送同一条消息,从而造成消息重复的发送。从 0.11.0 的版本开始,Kafka 给每个生产端生成一个唯一的 ID,并且在每条消息中生成一个 sequence num,sequence num 是递增且唯一的,这样服务端就能对消息去重,达到一个生产端不重复发送一条消息的目的。
消费端
幂等性设计:计消费者端的业务处理逻辑为幂等操作,即使消息被重复消费,也不会对系统状态产生负面影响。这样,即使消息在某些异常情况下被重新消费,系统也能正确处理。
5. Kafka如何保证消息顺序消费
Kafka服务端本身已经保证了在单个分区内的消息顺序,因为新消息总是追加到分区尾部。这是Kafka作为分布式消息队列的一个核心特性。因此不需要考虑服务端。
分区内顺序消费
生产端
-
基于Key的分区策略:Kafka生产者可以根据消息的key计算哈希值,并根据分区数确定目标分区。这样,具有相同key的消息将被路由到同一分区,从而保证了这部分消息在分区内的顺序消费。
-
生产者唯一id+消息序列号+幂等性:从 Apache Kafka 0.11.0 版本开始引入了幂等性和事务性生产者功能,确实增强了消息发送的可靠性与有序性。
- Producer ID (PID) : 生产者客户端在初始化时会请求服务器分配一个全局唯一的Producer ID(PID)。这个ID与生产者的实例相关联,并用于标识一系列相关的消息。
- Sequence Number: 对于每个PID,生产者会在发送消息时为每条消息生成一个递增的序列号(Sequence Number或说是Producer Epoch + Base Offset)。这些序列号用于确保消息按照严格的顺序到达并且不会重复。
- 幂等性:启用幂等性后,即使由于网络问题或其他原因导致消息重复发送,Kafka也能确保消息仅被写入日志一次。Kafka服务器端会依据PID和序列号来识别重复的消息并丢弃它们。
消费端
独占消费者策略:消费者独占策略通常指的是确保消费者组内的每个消费者实例只消费主题的一个分区,这样可以确保消费者组内的消息顺序消费(在分区级别)。通过设置合理的消费者组和分区分配策略来实现:
-
消费者组内消费者数量设置:
- 确保消费者组内的消费者实例数量等于主题的分区数。
-
使用合适的分区分配策略:
- Range 和 RoundRobin 分配策略在消费者组内的消费者实例数量等于主题的分区数条件下可以实现每个消费者分配一个分区的效果。
- 补充1:Range策略是Kafka的默认分区分配策略之一(在较早版本中)。该策略会根据分区的序号(分区ID)和消费者ID进行分配。对于每个主题,Kafka会将所有分区按序号排序,然后将分区列表按消费者数量平均切分成几段。接着,这些分区段将按照消费者ID的字典序分配给消费者。因此,在消费者数量和主题分区数量相等的情况下,每个消费者会分配到一个且仅一个分区,从而实现类似独占的效果。
- 补充2:RoundRobin策略按照轮询的方式来分配分区给消费者。在消费者数量不多于主题分区数量的情况下,该策略可以保证每个消费者至少分配到一个分区。当消费者数量和分区数量相等时,理论上可以实现每个消费者只分配到一个分区的效果。
全局顺序消费
在Kafka的原生设计中,全局顺序消费是不被直接支持的,因为不同分区之间可能存在并行生产和消费,每个分区有自己的顺序,全局范围内无法保证所有的消息严格按照生产顺序消费。
生产端:
- 单分区策略:在创建Topic时仅设置一个分区。这样一来,所有消息都将被发送到这个唯一的分区中,而每个消费者组内的任何一个消费者也只能消费这个分区的消息,从而确保了消息按照发送顺序进行消费。
总结来说,在生产端通过合理的分区策略和事务性生产者可以尽量保证消息的发送顺序;而在服务端,Kafka默认保证了单个分区内的消息顺序;在消费端,合理设置消费者组与分区的关系,以及在消费逻辑中采取同步处理方式,可以实现消息的顺序消费。需要注意的是,全局严格顺序消费往往意味着牺牲并发能力和伸缩性,因此在实践中需要根据业务需求权衡利弊。