消息队列,即是“消息 + 队列”(Message Queue),简称 MQ。消息队列本质上就是一个FIFO队列,只不过队列中存放的内容是message,所以叫“消息队列”。
消息队列的主要用途:用于不同服务、进程、线程 之间的通信。
消息队列的使用场景可分为 “异步处理、流量控制、服务解耦、发布订阅、高并发缓冲” 等五个方面:
以下图中的秒杀系统为例,如不使用消息队列,一个正常的业务处理流程需要顺次经过“订单、短信、统计”三个模块的处理,“库存”组件等待返回结果的时间就是三者处理耗时之和;而如果引入消息队列,库存”组件将消息发送到消息队列中,消息队列同时将这条消息分别发送给下游的 “订单”、“短信”、“统计” 等组件去异步并行处理。
此处使用消息队列的目的是:
更快返回结果;减少等待,实现并发处理,提升系统总体性能。
例如秒杀场景下的下单状态,使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。
当出现突发瞬时流量时,消息队列可以缓存部分消息,等待后端组件空闲时处理。
假设现有系统中由A、B、C、D 四个子系统构成,在不使用消息队列时,A系统需要同时实现与 B系统、C系统、D系统的下游系统的通信所用到的接口,此时如果其中某个子系统的消息接口修改,则A需要进行同步修改适配,多个子系统强耦合在了一起。
如果引入消息队列,则A只需要实现向消息队列发送消息的接口即可,其他系统组件如果需要使用A发布的消息则自行去消息队列中消费即可,并且更方便实现下游模块的增加和删减(例如新增一个消费模块E)。
例如网络游戏中跨服的广播消息,多个服务器订阅同一个消息队列上的消息。
例如Kafka在日志服务、监控上报 等方面的应用。
“broker” 的概念来自于 Apache ActiveMQ,通俗的讲就是 MQ的服务器。
生产者(Producer):发送消息到消息队列;
消费者(Consumer):从消息队列接收消息。
消息生产者向一个特定的队列发送消息,消息消费者从该队列中接收消息;
消息的生产者和消费者可以不同时处于运行状态。
每一个成功处理的消息都由消息消费者签收确认(Acknowledge)。
发布订阅消息模型中,支持向一个特定的 “主题”(Topic) 发布消息,0个或多个订阅者接收来自这个消息主题的消息。
在这种模型下,发布者和订阅者彼此不知道对方。
基于Queue消息模型,利用FIFO先进先出的特性,可以保证消息的顺序性。
为了 保证消息不丢失,消息队列提供了消息Acknowledge机制,即ACK机制,当Consumer确认消息已经被消费处理,发送一个ACK给消息队列,此时消息队列可以删除这个消息了;
如果Consumer宕机/关闭,没有发送ACK,消息队列将认为这个消息没有被处理,会将这个消息重新发送给其他的Consumer重新消费处理。
(疑问: 对于Kafka,未被ACK确认的重传次数如何设置? )
消息的持久化,对于一些关键的核心业务来说非常重要,启用消息持久化后,当消息队列宕机重启后,消息可以持久化存储恢复,消息不丢失,可以继续消费处理。
(疑问: 对于Kafka,消息的“持久化”是如何实现的?)
Kafka的特点是: “高吞吐、可持久化、可水平扩展、支持流数据处理” 等。
Kafka可充当三种角色:
(1)消息系统:(消息中间件)
与传统的消息中间件一样,Kafka被用作消息中间件时具备 系统解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性 等功能。
与此同时,Kafka还提供了大多数消息中间件难以实现的 消息顺序性保障 及 回溯消费 的功能。
(2)存储系统:
Kafka把消息持久化到 磁盘,相比于其他基于内存存储的系统而言,有效的降低了数据丢失的风险。
(3)流式处理平台:
(流失处理平台指的是对分布式大数据的处理)Kafka不仅为每个流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流失处理类库,比如窗口、连接、交换和聚合等各类操作。
一个典型的Kafka体系架构包括:若干 Producer、若干 Broker、若干 Consumer,以及一个ZooKeeper集群。
其中:
(1)Producer:生产者,即消息的发送方,负责生产消息,然后将其投递到Kafka中;
(2)Customer:消费者,即消息的接收方,消费者连接Kafka上并接收消息,进而进行相应的业务逻辑处理。
(3)Broker:服务代理节点。大多数情况下可以将Broker看做一台Kafka服务器,前提是这台服务器上只部署了一个Kafka实例。 一个或多个Broker组成一个Kafka集群。
(4)ZooKeeper:ZooKeeper是Kafka用来负责群元数据的管理、控制器的选举等操作
Kafka中的消息以 “主题”(Topic) 为单位进行归类。在正式处理消息之前,生产者、消费者分别向Broker注册一个同名的Topic实例,之后生产者负责将消息发送到这个特定的Topic(生产者发送到Kafka集群中的每一条消息都要指定一个主题),消费者负责订阅主题并进行消费。
主题是一个逻辑上的概念,一个主题可以横跨多个broker,以此来提供比单个broker更强大的性能。
一个主题可以划分为多个 “分区”(Partition),很多时候也会把分区称为 “主题分区”(Topic-Partition)。
如果一个主题只对应一个文件,那么这个文件所在机器的I/O将会成为这个主题的性能瓶颈(I/O读写速度往往是很慢的,如果Kafka上的一个主题只为一个文件服务,则会造成存储资源的浪费),而分区恰好解决了这个问题。
每一条消息被发送到broker之前,会根据分区规则选择存储到哪个具体的分区。如果分区设定的合理,所有的消息都可以均匀的分配到不用的分区中。
分区在存储层面可以看做一个 可追加的日志文件(Log),消息在被追加到分区日志文件的时候,都会分配一个特定的偏移量 offset。offset是消息在分区的唯一标识,Kafka通过它来保证消息在分区内的顺序性。
也就是说,Kafka保证的是分区有序而不是主题有序。
Kafka为分区引入了多副本(Replica)机制,通过增加副本的数量可以提升容灾能力。
同一分区的不同副本是 “一主多从” 的关系,其中:leader副本负责处理 读写请求,follower副本只负责与leader副本的 消息同步。(在同一时刻,副本之间并非完全一样,follower副本中的消息相对leader会有一定的滞后)
(注意:Kafka不支持读写分离。)
副本处于不同的broker中,当leader副本出现故障时,从follower副本中重新选举新的leader副本对外提供服务。Kafka通过多副本机制实现了故障的自动转移,当Kafka集群中某个broker失效时仍然能保证服务可用(高可用)。
Kafka的消费端也具备一定的容灾能力。 Consumer使用拉模式(Pull)从服务端拉取消息,并且保存消费的具体位置,当消费者宕机后恢复上线时可根据之前保存的消费位置重新拉取需要的消息进行消费,这样就不会造成消息丢失。
Kafka的多副本架构:
Kafka中主题、分区、副本和Log之间的关系:
同一主题的不同分区可以创建在同一个broker上,但同一分布的不同副本不允许创建在同一个broker上,否则在创建时将出错。
例如,在只有1个borker的服务器上创建一个含有两个副本的主题“topic-demo”:
# sh kafka-topics.sh --zookeeper localhost:2181/kafka --create --topic topic-demo --partitions 4 --replication-factor 2
//控制台打印:
Error while executing topic command : Replication factor: 2 larger than available brokers: 1.
AR、ISR、OSR 描述的是 “同一个分区的不同副本” 之间的关系:
AR:一个分区中的所有副本,统称AR(Assigned Replicas);
ISR:所有与leader副本保持“一定程度”同步的副本(包括leader副本在内)组成ISR(In-Sync Replicas);
OSR:与leader副本同步滞后过多的副本(显然不包括leader副本)组成OSR(Out-Sync Replicas)。
三者关系: AR = ISR + OSR
,这个“一定程度”的同步可以通过参数配置。
leader副本会维护和跟踪 ISR、OSR集合中的副本的滞后状态:如果ISR集合中的follower副本滞后过多时,leader负责将其移除到OSR集合中;如果OSR集合中的follower副本“追上”了leader副本时,leader负责将其移入到ISR集合中。
当leader副本发生宕机时,只有ISR集合中的follower副本有资格被选举为新的leader。(这个原则也可以通过修改参数配置)
LEO:LEO(Log End Offset) 表示一个副本对应的日志的下一个可写入的位置(相当于写指针相对于log起始位置的偏移量offset),LEO 是副本级的变量;
HW:HW (High Watermark)俗称“高水位”,是分区级的变量,每个分区都有一个HW高水位值,用于标识一个特殊的消息偏移量offset,它是这个分区中所有副本的LEO的最小值(HW=min(LEO)
),消费者只能拉取这个HW之前的消息。
假设某个分区的ISR集合中有3个副本(1个leader副本+2个follower副本),此时分区HW = 3:
当新消息 3 和 4 写入到leader副本后,follower副本会发送拉取请求来拉取消息3和4以进行消息同步。
在同步过程中,不同的follower副本的同步效率也不尽相同。 假设在某一时刻 follower1完全跟上了leader副本,而follower2只完成了消息3的同步,则此时HW = 4,消费者只能消费offset 为 0 到 3 之间的消息:
当所有副本都完成了同步,HW = 5, 消费者可以消费 offset = 4 的消息了:
由上面的Kafka复制机制的实现可知,Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。
事实上,同步复制要求所有能工作的follower副本都复制完,这条消息才会被确认为已成功提交,这种复制方式极大的影响了性能;
而对于异步复制的方式,follower副本异步的从leader副本中复制数据,数据只要被leader副本写入就被认为已经成功提交。在这种情况下,如果follower副本都还没有复制而落后于leader副本,突然leader副本宕机,则会造成数据丢失。
Kafka 使用的这种ISR的方式,则有效的权衡了数据可靠性和性能之间的关系。