消息队列是我们平时生产中常用的中间件,使用消息队列有以下几点好处
- 异步:投递到Kafka中的消息可以异步处理,提高系统响应速度
- 解耦:减少系统间的耦合,通过增加一层中间件,系统不需要再感知上下游系统的变动,只负责往消息队列推/拉消息,责任界限更加分明
- 流量削峰:其实就是由异步处理带来的好处,在流量激增的时候,系统可以先快速响应,却不用立即处理。通过消息队列慢慢消费处理,起到削峰填谷的作用。
常用的消息队列有 kafka 、rabbitMq 、 rocketMq等
kafka & RocketMQ 优劣
消息队列选型的时候时常会对比 kafka 和RocketMQ 两种队列
Kakfa 设计之初就是处理海量日志文件,它的一些特性使得它能处理高吞吐的流式数据,支持集群部署;但不支持什么高级功能
RocketMQ 由阿里基于Kafka改造,更适合一些业务场景
| Kafka | RocketMQ |
|---|---|
| 适用高吞吐,低延迟的场景 | |
| 支持事务,重试,可靠性更高 |
有兴趣可以看下这篇文章:消息队列的发展历史
本篇文章主要总结下kafka的基本知识和面试考点🐶
Kafka 系统架构
Kafka 的系统架构主要如上图所示,主要可以分为三个组成部分
- 生产者 Producer
- Broker
- 消费者 Consumer
基于发布/订阅模式,生产者将消息投递到broker,消费者到broker拉取消息
其中,broker 是服务代理节点;一般来说 kafka集群中的一台物理服务器就是一个broker,可以进行水平扩展,增加broker来提高性能。
除了这三个组成部分,还有几个重要的概念
-
Topic : Kafka 中的消息是以Topic为单位进行归类的,我们可以认为一个Topic 是一个 Queue,Producer 生产的每一条消息都必须指定一个 Topic,然后 Consumer 会根据订阅的 Topic 到对应的 broker 上去拉取消息。
- 分区 Partition :队列太长容易导致各种问题,阻塞,延迟等,partition解决了这个问题
- 每个Topic 可以划分为多个分区,它们的作用就是提高吞吐量,同时方便了扩展(一个topic可以有多个partition,所以我们可以通过扩展机器去轻松的应对日益增长的数据量),提高了并发(以partition为读写单位,可以多个消费者同时消费数据,提高了消息的处理效率。)
- 每个分区的数据是不同的
- 生产者根据不同规则把消息投递到不同分区,提高生产速度
- 一个消费者组的不同消费者订阅不同分区,也可以提高消费速度
- 分区内的数据是有序的,但全局的数据不一定是有序的
- 线上一般partion可以增加但是不能减少
- 副本 Replication : 为了提高系统的可靠性,需要提高数据冗余
- 每个分区都会有多个副本,当主分区挂了的时候,会选择一个备分区成为主
- kafka默认最大副本数是10 , 且副本的数量不能大于Broker的数量(副本肯定得在不同broker上)
- 分区 Partition :队列太长容易导致各种问题,阻塞,延迟等,partition解决了这个问题
-
消费者组(consumer group)
- 多个消费者可以组成一个整体来消费一个Topic
- 一个消费者可以消费多个partition
- 一个partion不可以被多个消费者(同一个组)消费
- 增加/减少消费者的时候需要进行rebalance
-
zooKeeper:一个kv数据库,用于保存元信息
-
Rebalance: Kafak重新分配分区的过程
-
本质就是让某一个消费者组中的成员可以均衡的消费Topic,所以这是一个分配过程,引发这个rebalance的条件通常有3个:
- 消费者组成员变化(新加入成员,已有消费者崩溃,消费者在一定时间内没有完成消息处理也会被视为崩溃)
- 消费者组所消费的主题分区增加
- 组订阅的主题数量发生变化
-
Kafka 高性能原因
- 利用了 PageCache 缓存
PageCache是操作系统级别的缓存,它把尽可能多的空闲内存当作磁盘缓存使用来进一步提高IO效率,同时当其他进程申请内存,回收PageCache的代价也很小。
同时服务挂了后,缓存数据不会丢失,落盘由操作系统处理(同时kafka也支持通过参数控制数据是写到PageCache时返回还是落盘时返回。)
- 磁盘顺序写
kafka在写入数据时,只允许向后追加数据,不允许修改已有数据(符合硬盘的读写的特征)。所以在每个partition当中,数据的顺序是能保证的。
- 零拷贝技术
kafka主要通过两种零拷贝技术:磁盘读写使用mmap 和 网络IO使用sendfile
- 一般来说,数据拷贝要产生4次拷贝:
- 1、磁盘 -> 内核buffer
- 2、内核buffer -> 用户buffer
- 3、用户buffer -> 到目标内核buffer (比如socket 缓存)
- 4、内核buffer -> 目标磁盘位置(比如 网卡 ;2,3这两步操作是可以省去的) 通过零拷贝技术,可以省去2,3这两步操作
在利用通过网络传输数据时,Kafka 利用SendFile 零拷贝技术提高性能
- sendfile:解决的是磁盘到网络数据的传输。操作系统读取磁盘数据到内存缓存后,直接发送给网卡缓存,然后发送网络数据。
而 使用pageCache,实际上是利用mmap的技术来提高性能
- mmap : 将磁盘文件映射到内存中,之后通过修改内存来修改文件内容。
-
消费者pull 拉模式(性能由consumer决定而不是broker决定)
-
文件分段(把Topic中一个Partition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完成的文件,减少磁盘占用)
-
批量发送
-
数据压缩。
Kafka 可靠性如何保证
作为一个消息中间件,可靠性主要体现在如何确保不丢消息
从生产者角度看,Kafka的ACK确认机制提高了其可靠性
- ACK 确认机制
-
在生产者向Broker写入数据的时候可以设置参数来确定是否确认kafka接收到数据,这个参数可设置的值为0、1、all。
-
0代表producer往集群发送数据后,不需要等到集群的返回,不确保消息发送成功。安全性最低但是效率最高。
-
1代表producer往集群发送数据后,只要leader应答ACK就可以发送下一条,只确保leader发送成功。
-
all代表producer往集群发送数据后,需要所有的follower都完成从leader的同步才会发送下一条,确保leader发送成功和所有的副本都完成备份。安全性最高,但是效率最低。
-
- 同时ACK还分为 同步确认和异步确认,
同步 - sync和异步 - async。默认是同步的方式;同步确认可以进行重试,如果使用的是异步确认则无法进行重试,会丢消息
而从消费者的角度来看,主要是通过Offset机制,来判断自己从哪开始消费消息,不漏/重复消费
消费者提交Offset分为 自动提交和手动提交
-
自动提交:客户端会每隔一段时间提交一次位移;自动提交不会丢失消息(也就是消息都会被消费)但是会发生重复消费的情况。
-
手动提交:灵活度高,分为同步提交和异步提交。两种各有利弊
- 同步提交会阻塞但是可以实现重试机制;
- 异步提交虽然不会阻塞但是无法重试,因为重试时的位移可能已经不是之前的位移了。
这里要说一下语义保证:
- 最多一次:消息可能会丢失,但是不会出现重复消费的情况
- 最少一次:消息不会丢失,但是可能会出现重复消费情况
- 精确一次:消息一定会被处理且只会被处理一次
- Kafka 默认提供的交付可靠性保障是第二种,即
至少一次。(自动提交)
Offset 偏移主要分为
-
生产者offset:即这个分区的最新最大的offset
-
消费者offset: - 消费者本地会有个消费者offset - broker会记录一个消费者offset
-
默认情况下,消费者消费消息后会提交offset,这个offset在broker测也是保存在一个topic里 **
__consumer_offsets,**数据的内容大概包括下面这些- Group : 消费者组
- Topic : topic的名字
- Pid : partition的ID
- Offset : kafka消费者在对应分区上已经消费的消息数【位置】
- logSize : 已经写到该分区的消息数【位置】
- Lag : 还有多少消息未读取(Lag = logSize - Offset)
- Owner : 分区创建在哪个broker
broker测主要还是通过落盘,备份,pagecache 来保证数据可靠性。
以上是Kafka提供的可靠性保证,当然在实际使用中,业务上还会加入一些重试逻辑和重试组件来进一步提高可靠性。
Kafka 数据一致性
Kafka 集群模式,有可能会发生leader 崩溃,重新选举;一致性说的就是不论是老的 Leader 还是新选举的 Leader,Consumer 都能读到一样的数据。
- 高水位(High Water Mark) & LEO(Log End Offset)
在 Kafka 中,高水位是一个位置信息标记,它是用消息位移来表征的,比如某个副本中HW=8,就是这个副本的高水位在offset=8那个位置上。
Log End Offset,缩写是LEO。它表示副本写入下一条消息的位移值。上图中LEO是15,即下一条新消息的位移是15,8-14这些位置上的消息就是未提交消息。同一个副本对象,其高水位值不会大于LEO值。
高水位HW的作用主要有2个
- 定义消息的可见性,即用来标识分区下的哪些消息是可以被消费的,比如某个分区的HW(leader的HW)是8,那么这个分区只有 < 8 这些位置上消息可以被消费。即高水位之前的消息才被认为是已提交的消息,才可以被消费。
- 帮助Kafka完成副本同步。
假设分区的副本为3,其中副本0是 Leader,副本1和副本2是 follower,并且在 ISR(in-sync replica)列表里面。
虽然副本0(Leader)已经写入了 Message4,但是 Consumer 只能读取到 Message2(HW = 2)。因为所有的 ISR 都同步了 Message2,只有 High Water Mark 以上的消息才支持 Consumer 读取,而 High Water Mark 取决于 ISR 列表里面偏移量最小的分区,类似于木桶原理。
Kafka 这样做的原因是还没有被足够多副本复制的消息被认为是“不安全”的。
如果此时 Leader 发生崩溃,另一个副本成为新 Leader,那么这些消息很可能丢失了。如果我们允许消费者读取这些消息,可能就会破坏一致性。试想,一个消费者从当前 Leader(副本0) 读取并处理了 Message4,这个时候 Leader 挂掉了,选举了副本1为新的 Leader,这时候另一个消费者再去从新的 Leader 读取消息,发现这个消息其实并不存在,这就导致了数据不一致性问题。
当然,引入了 High Water Mark 机制,会导致 Broker 间的消息复制因为某些原因变慢,那么消息到达消费者的时间也会随之变长(因为我们会先等待消息复制完毕)。
延迟时间可以通过参数 replica.lag.time.max.ms 参数配置,它指定了副本在复制消息时可被允许的最大延迟时间。