为什么要使用消息队列?
总结一下,主要三点原因:解耦、异步、削峰。
1、解耦。比如,用户下单后,订单系统需要通知库存系统,假如库存系统无法访问,则订单减库存将失败,从而导致订单操作失败。订单系统与库存系统耦合,这个时候如果使用消息队列,可以返回给用户成功,先把消息持久化,等库存系统恢复后,就可以正常消费减去库存了。
2、异步。将消息写入消息队列,非必要的业务逻辑以异步的方式运行,不影响主流程业务。
3、削峰。消费端慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。比如秒杀活动,一般会因为流量过大,从而导致流量暴增,应用挂掉。这个时候加上消息队列,服务器接收到用户的请求后,首先写入消息队列,如果消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。
使用了消息队列会有什么缺点
系统可用性降低。引入消息队列之后,如果消息队列挂了,可能会影响到业务系统的可用性。
系统复杂性增加。加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。
Kafca基本概念
名称 | 解释 |
---|---|
Producer | 消息⽣产者,向 Kafka Broker 发消息的客户端。 |
Consumer | 消息消费者,从 Kafka Broker 取消息的客户端。Kafka支持持久化,生产者退出后,未消费的消息仍可被消费。 |
Consumer Group | 消费者组(CG),消费者组内每个消费者负责消费不同分区的数据,提⾼消费能⼒。⼀个分区只能由组内⼀个消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的⼀个订阅者 |
Broker | ⼀台 Kafka 机器就是⼀个 Broker。⼀个集群(kafka cluster)由多个 Broker 组成。⼀个 Broker 可以容纳多个 Topic。 |
Controller | 由zookeeper选举其中一个Broker产生。它的主要作用是在 Apache ZooKeeper 的帮助下管理和协调整个 Kafka 集群。 |
Topic | 可以理解为⼀个队列,Topic 将消息分类,⽣产者和消费者⾯向的是同⼀个 Topic。 |
Partition | 为了实现扩展性,提⾼并发能⼒,⼀个⾮常⼤的 Topic 可以分布到多个 Broker上,⼀个 Topic 可以分为多个 Partition,同⼀个topic在不同的分区的数据是不重复的,每个 Partition 是⼀个有序的队列,其表现形式就是⼀个⼀个的⽂件夹。不同Partition可以部署在同一台机器上,但不建议这么做。 |
Replication | 每⼀个分区都有多个副本,副本的作⽤是做备胎。当主分区(Leader)故障的时候会选择⼀个备胎(Follower)上位,成为Leader。在kafka中默认副本的最⼤数量是10个,且副本的数量不能⼤于Broker的数量,follower和leader绝对是在不同的机器,同⼀机器对同⼀个分区也只可能存放⼀个副本(包括⾃⼰)。 |
Message | 每⼀条发送的消息主体。 |
Leader | 每个分区多个副本的“主”副本,⽣产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。 |
Follower | 每个分区多个副本的“从”副本,使用发布订阅模式主动拉取Leader的数据(与redis不同),实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发⽣故障时,某个 Follower 还会成为新的 Leader。 |
Offset | 消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从消费位置继续消费。 |
ZooKeeper | Kafka 集群能够正常⼯作,需要依赖于 ZooKeeper,ZooKeeper 帮助 Kafka存储和管理集群信息。 |
High Level API 和Low Level API | 高水平API,kafka本身定义的行为,屏蔽细节管理,使用方便;低水平API细节需要自己处理,较为灵活但是复杂。 |
生产者
( 1 )acks=0: 表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢消息。
( 2 )acks=1: 至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一条消息。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。
( 3 )acks=-1或all: 需要等待 min.insync.replicas(默认为 1 ,推荐配置大于等于2) 这个参数配置的副本个数都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。
设计一个不丢数据的方案:数据不丢失的方案:
1)分区副本 >=2
2)acks = -1
3)min.insync.replicas >=2。
分区机制
分区原因:
⽅便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,⽽⼀个 Topic ⼜可以有多个 Partition 组成,因此可以以 Partition 为单位读写了。
可以提⾼并发,避免两个分区持久化的时候争夺资源。
备份的问题。防止一台机器宕机后数据丢失的问题。
分区原则:
我们需要将 Producer 发送的数据封装成⼀个 ProducerRecord 对象。该对象需要指定⼀些参数:
topic:string 类型,NotNull。
partition:int 类型,可选。
timestamp:long 类型,可选。
key:string 类型,可选。
value:string 类型,可选。
headers:array 类型,Nullable。
指明 Partition 的情况下,直接将给定的 Value 作为 Partition 的值;没有指明 Partition 但有 Key 的情况下,将 Key 的 Hash 值与分区数取余得到 Partition 值;既没有 Partition 又没有 Key 的情况下,第⼀次调⽤时随机⽣成⼀个整数(后⾯每次调⽤都在这个整数上⾃增),将这个值与可⽤的分区数取余,得到 Partition 值,也就是常说的 Round-Robin轮询算法。
存储机制
每个分片目录中,kafka 通过分段的方式将 数据 分为多个 LogSegment,一个 LogSegment 对应磁盘上的一个日志文件(.log)和一个索引文件(.index),其中日志文件是用来记录消息的。索引文件是用来保存消息的索引。每个LogSegment 的大小可以在server.properties 中log.segment.bytes=107370 (设置分段大小,默认是1gb)选项进行设置。
指定offset读取
通过每个segment文件的命名为当前segment中offset起始位查找对应segment,然后在index中二分查找索引,最后到log中顺序查找。稀疏索引比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。
LEO&HW更新原理
LEO(last end offset):该副本底层日志文件上的数据的最大偏移量的下一个值
HW(highwater mark):水位,它一定会小于LEO的值。这个值规定了消费者仅能消费HW之前的数据。follower在和leader同步数据的时候,同步过来的数据会带上LEO的值。leader partition就会记录这些follower同步过来的LEO,然后取最小的LEO值作为HW值。follower获取数据时也带上leader partition的HW的值,然后和自身的LEO值取一个较小的值作为自身的HW值
ISR
follower如果超过10秒没有到leader这里同步数据,就会被踢出ISR。它的作用就是帮助我们在leader宕机时快速再选出一个leader,因为在ISR列表中的follower都是和leader同步率高的,就算丢失数据也不会丢失太多。
当follower的LEO值>=leader的HW值,就可以回到ISR。
消费者
Consumer 采⽤ Pull(拉取)模式从 Broker 中读取数据。Pull 模式则可以根据 Consumer 的消费能⼒以适当的速率消费消息。Pull 模式不⾜之处是,如果Kafka 没有数据,消费者可能会陷⼊循环中,⼀直返回空数据。因为消费者从 Broker 主动拉取数据,需要维护⼀个⻓轮询,针对这⼀点, Kafka 的消费者在消费数据时会传⼊⼀个时⻓参数 timeout。如果当前没有数据可供消费,Consumer 会等待⼀段时间之后再返回,这段时⻓即为 timeout。
分区分配策略
⼀个 Consumer Group 中有多个 Consumer,⼀个 Topic 有多个 Partition。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费。不同组间的消费者是相互独立的,相同组内的消费者才会协作,这就必然会涉及到Partition 的分配问题,即确定哪个 Partition 由哪个 Consumer 来消费。
Kafka 有三种分配策略:
- Range,默认为Range:Range是对每个Topic而言的,尽量顺序的均分,前面的会多分几个,如4个partition 2个consumer则为0-1,2-3
- RoundRobin:依次每个订阅了的消费者一个一个分,如4个partition2个consumer则为02,13
- Sticky:与RoundRobin相似,但是再分配会尽可能的与上次的分配结果保持一致
当消费者组内消费者发⽣变化时,会触发分区分配策略(⽅法重新分配),在分配完成前,kafka会暂停对外服务。注意为了尽量确保消息的有序执行,一个分区只能对应一个消费者,这也说明消费者的数量不能超过分区的数量。
为啥kafca快
- Kafka 利用了一种分段式的、只追加 (Append-Only) 的日志,基本上把自身的读写操作限制为顺序 I/O
- 消息先被写入页缓存,由操作系统负责刷盘任务。
- 零拷贝降低数据拷贝次数和内核切换次数
- Kafka 的 message 是按 topic分 类存储的,topic 中的数据又是按照一个一个的 partition ,每个 partition 对应了操作系统上的一个文件夹,partition 实际上又是按照segment分段存储的提高并行写性能
如何做到可靠
- 发送确认机制,副本写入才成功
- 多partion副本存在多brocker
- 消费端offset确认
如何保证消息的顺序性?
- 全局有序:只创建1个Partition(分区),这样生产者的所有数据都发送到了一个Partition(分区),保证了消息的消费顺序。或者消费端通过顺序编排,或者依赖检测保证
- 局部消费顺序:比如3个直播间同时发消息,局部顺序就是直播间1先发,直播间2后发,但是可能直播间2的消息先到,这个顺序是不保证的。但是直播间1先发了“消息1”,再发了“消息2”,这个顺序是能保证的,也就是在直播间内是有序的,但是直播间之间的消息顺序不能保证。实现方式:生产者在发送消息的时候指定要发送到哪个Partition(分区)
大量消息在 MQ 里长时间积压,该如何解决?
一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:
先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉;
新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量;
然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue;
接着临时用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据;
等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
如何做到高可用?
Kafka 的基础集群架构,由多个broker组成,每个broker都是一个节点。当你创建一个topic时,它可以划分为多个partition,而每个partition放一部分数据,分别存在于不同的 broker 上。也就是说,一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据。
每个partition放一部分数据,如果对应的broker挂了,那这部分数据是不是就丢失了?那不是保证不了高可用吗?
Kafka 0.8 之后,提供了复制多副本机制来保证高可用,即每个 partition 的数据都会同步到其它机器上,形成多个副本。然后所有的副本会选举一个 leader 出来,让leader去跟生产和消费者打交道,其他副本都是follower。写数据时,leader 负责把数据同步给所有的follower,读消息时,直接读 leader 上的数据即可。如何保证高可用的?就是假设某个 broker 宕机,这个broker上的partition 在其他机器上都有副本的。如果挂的是leader的broker呢?其他follower会重新选一个leader出来。
如何处理消息积压
事前可以建立批处理能力,如汇总账单下发。
能导致积压突然增加,最粗粒度的原因,只有两种:要么是发送变快了,要么是消费变慢了。
大部分消息队列都内置了监控的功能,只要通过监控数据,很容易确定是哪种原因。如果是单位时间发送的 消息增多,比如说是赶上大促或者抢购,短时间内不太可能优化消费端的代码来提升消费性能,唯一的方法 是通过扩容消费端的实例数来提升总体的消费能力。
如果短时间内没有足够的服务器资源进行扩容,没办法的办法是,将系统降级,通过关闭一些不重要的业 务,减少发送方发送的数据量,最低限度让系统还能正常运转,服务一些重要业务。
如果是Kafka消费能力不足,则可以考虑增加Topic的分区数(一般一个Topic分区数为3-10个),并且同时提升消费组的消费者数量,消费者数==分区数。两者缺一不可
如果监控到消费变慢了,你需要检查你的消费实例,分析一下是什么原因导致消费变慢。优先检查一下日志 是否有大量的消费错误,如果没有错误的话,可以通过打印堆栈信息,看一下你的消费线程是不是卡在什么 地方不动了,比如触发了死锁或者卡在等待某些资源上了。
大部分消息队列都内置了监控的功能,只要通过监控数据,很容易确定是哪种原因。如果是单位时间发送的 消息增多,比如说是赶上大促或者抢购,短时间内不太可能优化消费端的代码来提升消费性能,唯一的方法 是通过扩容消费端的实例数来提升总体的消费能力。
消息回溯
消费过程中因为消费失败、上线bug或其他原因,会存在重新消费旧数据的需求。
kafka支持两种方式回溯。一种是基于消息偏移量回溯,一种是基于时间点的消息回溯。
基于消息偏移量回溯
在kafka的每个分区中,每条消息都有一个唯一的offset值 ,即消息偏移量,用来表示消息在partition分区中的位置。消费者每次消费了消息,都会把消费的此条消息的offset提交到broker(消息节点),用于记录消费到分区中的位置,下条消息从这个位置之后开始消费。所以基于消息偏移量回溯很简单,只需要重置offset,然后消费者会从该offset之后开始消费
基于时间点的消息回溯
时间索引文件找到时间对应的offset偏移量,在通过偏移量索引文件找到消息位置。