前言
Kafka 是一个分布式
、高吞吐量
、高扩展性
的消息队列
系统。它最初由 LinkedIn 公司开发,后来于 2010 年贡献给 Apache 基金会,成为一个开源项目。Kafka 主要应用于日志收集系统和消息系统,类似于其他消息队列中间件(例如 RabbitMQ、ActiveMQ),但 Kafka 的优点在于其稳定性、高效性以及丰富的功能。
概念
简单了解 Kafka 的相关概念:
- Topic(主题) :
- 每个发送到 Kafka 的消息都有一个主题,也可以看作是一个类别,类似于
数据库中的表名
。例如,如果发送一个主题为 "order" 的消息,那么该主题下就会有多条关于订单的消息。
- 每个发送到 Kafka 的消息都有一个主题,也可以看作是一个类别,类似于
- Producer(生产者) :
- 生产者
负责发送
消息到 Kafka 服务器。每条消息
必须属于一个 Topic(主题)
,类似MySQL 中的 insert 语句
。生产者不断地向 Kafka 发送消息。 - 注意上面说的是每条消息必须属于一个主题,并没有说一个生产者只能生产一个主题的消息,可以生产多个主题的消息
- 生产者
- Consumer(消费者) :
- 消费者
负责订阅 Kafka
中的主题(Topic)并从中拉取消息,类型 MySQL 的 select,不同的是,Kafka 只能消费一次,如想重复消费
则要使用偏移量特殊设置
。 - 每个消费者都属于一个特定的消费组(Consumer Group),如果没有指定消费组,则会默认一个消费组。
- 消费者从分区中获取消息,处理它们并执行相应的业务逻辑。
- 例如你订阅了某个公众号,即这个公众号被多个消费者订阅,公众号发送消息到消息队列时,订阅这个公众号的消费者就去这个公众号中消费数据。
- 消费者
- Consumer Group (消费组)
- 消费组是一组
具有相同目标的消费者
(具有处理相同逻辑的业务,比如拿到订单消息,处理相关的订单业务,如果同一组多个消费者拿到这个订单,那这个订单就被处理了多次,造成重复消费)。 - 当消息发布到主题后,只会被投递给订阅它的每个消费组中的一个消费者,也就是说,
同一个主题
下的同一个分区
的数据只会给同一个消费组
的一个消费者消费
,这样设计是为了避免重复消费。 - 每个消费者在消费组中负责处理不同分区的消息,也就是说,
同一个主题
下的不同一个分区
的数据可以给同一个消费组
的不同消费者消费
。 - 消费组的存在确保了消息的
负载均衡
和高可用性
。
- 消费组是一组
- Partition(分区) :
- 生产者发送的
消息存储在分区中
。分区的概念类似于Redis 中的槽
,目的是将数据分成多个块,实现负载均衡,相当于MySQL 分库
。 - 每个 Topic 可以有
多个分区
,但至少需要一个分区
。 - 每个分区存储的数据都是有序的,不同分区之间的数据不保证有序性,也就是说同一个分区的数据是按顺序消费的,但是不同分区之间就不一定了
- 比如有 1、2、3、4、5 的数据,1、3、4 在分区A,2、5 在分区 B,能保证的是分区 A 是按 1、3、4 的顺序消费、分区 B 是 2、5 的顺序消费。但是不能保证 1、2、3、4、5 这个顺序。
- 生产者发送的
- Replica(副本) :
- 副本用于保护分区中的数据完整性,防止数据丢失或服务器宕机。
- Kafka 将副本分布在
不同的服务器
上,实现负载均衡
。 - 副本分区包括一个
主分区
(leader)和多个从分区
(follower),确保数据的完整性。 - 熟悉的药方熟悉的味道,这难道不是 Redis 的主从复制吗(狗头)
- Broker(实例或节点) :
- Kafka 集群由多个 Broker 组成,每个 Broker 是一个 Kafka 实例,也就是说,每启动一个 Kafka 就创建了一个 Broker。
- 多个 Broker 构成分布式 Kafka 集群,提高吞吐率和效率。
单机消息发送与消费
集群消息发送与消费
结合上面的那些概念,看明白这两张图,就知道 Kafka 的消息发送与消费大概是怎么个流程了
如何保证消息不丢失不重复消费
保证写入数据不丢失
acks=all
或acks=-1
:生产者在所有同步副本收到数据后收到确认。此设置提供最高的数据持久性。即确认的的确确写入了本地日志,并且 follower 也已经保存了副本,即使 leader 宕机了,数据也不会丢失
概念
- 分区级别的复制 :
- 复制因子:这个数是多少,则说明 Kafka 有多少个副本。如果复制因子为 1,则只有一个副本 Leader,没有 follower。所以一般需要大于 1
- 这允许 Kafka 在集群服务器发生故障时
自动切换到这些副本
,以便在出现故障时消息仍然可用
- Leader 和 Followers :
每个分区
的副本
分别在不同
的 Kafkabrokers
上。- 在 n 个副本中,一个副本作为 leader ,其他副本成为 followers。
producer 只能往 leader 分区上写数据(读也只能从 leader 分区上进行)
,followers 只按顺序从 leader 上复制日志。
- 复制协议 :
- Kafka 的复制协议
确保了消息的一致性和可靠性
。 - 当 producer 往 leader 上发送消息时,leader 将消息复制到所有 followers。
- 只有将消息成功复制到所有同步副本(ISR)后,这条消息才算被提交(acks=all),
消费者只会消费已经提交的数据
。
- Kafka 的复制协议
- 同步副本列表(ISR) :
- 每个分区的 leader 维护一个 in-sync replica(同步副本列表,也称为 ISR)。
- 只有在 ISR 中的副本才能被
认为是跟上 leader 写进度的
,如果有副本处于落后状态,则将其移除 ISR。 - Kafka 密切监控 ISR 中的副本,以确保数据的完整性和可用性。
- 当 leader 发生故障时,
只能在 ISR 中的 follower 才能晋升为 leader
- 偏移量
- 偏移量是一个连续的整数值,用于唯一标识分区中的每一条消息。
- 每个分区都有自己的偏移量序列,从0开始递增。
- 消费者通过维护自己的偏移量来记录已经消费的消息。
- ISR 维护自己的偏移量来记录同步到哪条记录了
- 偏移量一般使用
**HW(High Watermark)**
来表示
- 参数
replication.factor
:确定将创建每个分区的多少个副本。min.insync.replicas
:指定当acks 设置为"all"
时必须
确认写入才能被视为成功的最小副本数。unclean.leader.election.enable
:确定是否可以将不同步的副本选举为领导者(不推荐)。replica.lag.time.max.ms
:这个参数定义了一个时间阈值,用于判断副本是否落后于 Leader 太久。replica.lag.max.messages
:这个参数定义了一个消息数量阈值,用于判断副本是否落后于 Leader 太多。
复制过程
复制过程涉及到一个参数
acks=0
:生产者不等待broker的任何确认,即我把消息发给leader 了,我不管有没有成功写入本地日志,有没有成功复制,直接返回提交成功。此设置提供最低的延迟,但数据丢失的风险最高。有可能我生产者说我已经写入数据了,但是消费者拿不到数据,导致数据丢失。
acks=1
:在broker将数据写入其本地日志之后但在任何followers复制数据之前,生产者收到确认。这提供了延迟和持久性之间的平衡。可能会导致的问题,我没来得及复制,但是生产者又收到消息已经提交,消费者可以去消费了。但是此时 leader 挂了,新从 follower 晋升上来的 leader 表示我不知道有这回事,就导致了数据丢失。
acks=all
或acks=-1
:生产者在所有同步副本收到数据后收到确认。此设置提供最高的数据持久性。
acks=0生产者向 broker 发送消息,broker 收到消息之后立刻返回消息已经提交,此时消费者就可以尝试消费该数据
acks=1
- 生产者向 broker 发送消息,broker 收到消息之后,先将消息同步到本地日志,同步完成之后返回给 producer 消息已经提交,此时消费者就可以尝试消费该数据
acks=-1 或者 acks=all
- 生产者向 broker 发送消息,broker 收到消息之后,先将消息同步到本地日志,同步完成后
- leader 副本 会发送从上一次同步的偏移量之后的所有消息,以确保 follower 副本能够追赶到当前的数据状态
- follower 同步完成之后,向 leader 发送 acks 确认
- 待所有的 follower 副本都同步完成之后向 producer 发送acks 确认,此时消费者就可以尝试消费该数据
上图有几处说明的地方
- 两个 broker 组成一个 Kafka 集群
- broker1 有 partitionA leader 跟 partitionB follower,当然有条件的这里也可以分两台服务器
- 这样是为了任务一台服务器挂掉,另一台都可以顶上,并且A、B 两个分区的数据都有,实现了高并发与高可用
举个例子(acks=all)
- 假如
replica.lag.max.messages:3,min.insync.replicas:2
,这两个参数说明,每当有消息进行复制时,至少有 2 个副本复制完成才算已完成复制,如果ISR 有副本落后超过超过 3 个消息,则移除 ISR - 有三个副本,一个 leader partitionA,两个 follower partitionA,里面分别有 0,1,2 的消息,此时他们的HW=5
- 此时有个消息为 5 的发送过来,在同步时,有个 follower 同步失败,此时消息会被视为未同步,消费者不能消费
- 之后又有 6、7消息过来,同样一个 follower 同步失败,一样上述的流程,此时 5、6、7 均未同步完成视为未提交,消费者不能消费
- 之后 8 这个消息过来了,最后那个 follower 同步失败时,因为超过了
replica.lag.max.messages
:3,所以被移除 ISR
-
疑问来了,既然 follower2 移除了 ISR,那么 ISR 就剩一个同步follower 了,并且这个 follower 已经完全跟 leader 同步了,为什么 5、6、7、8 还是视为为同步完成(未提交)?那是
min.insync.replicas:2
,必须有至少 2 个副本完成同步之后,才视为已提交。 -
之后 follower2 重启了一下,恢复正常了。此时就会从 follower 的偏移量开始同步,HW=5开始,直到跟 leader 一样,视为跟上了 leader,否则还是视为落后状态。此时 5、6、7、8 视为已提交,消费者可以进行消费
- 当然,如果上述例子有 3 个或更多 follower 时,每次有 2 个以上复制完成即可表示消息已提交。即可以使用 HW来判断 follower 是否已经落后于 leader。
保证消费者不会重复消费
-
Offset 偏移量
-
Kafka 中的每条消息都有一个唯一的 offset,代表消息的序号。消费者消费消息后,会定期提交已消费的消息的 offset。这样,即使消费者重启,也能从上次消费的位置继续。
-
如上面的写入例子,HW 表示最大偏移量 9,每添加一条数据偏移量加一,所以每条数据都有其对应的偏移量。比如 0 对应 1,8 对应 9
-
如图是消费过程的 offset 的提交过程
-
如果在消费者消费完消息时,提交 offset 的过程中失败了。Kafka 没有收到 offset 的提交,下次读取数据还是从上一个偏移量读取,还是会造成重复消费
-
幂等性 :重复消费不可怕,
但必须保证幂等性
。幂等性是指对
同一数据或请求的多次操作不会改变其状态
。- 比如一个查询按钮,无论查询多少次,也不会改变其状态,所以查询就是一个天生的幂等性
- 比如一个新增按钮,如果点击保存时,由于网络原因没有及时响应,多点了几次,导致同样的数据连续创建了几条记录,所以这就是非幂等性
为实现幂等性,可以考虑以下方法:
- 数据写入数据库前先判断是否已存在,如果存在则执行更新操作或者不操作。
- 对于写入 Redis,直接使用
SET
操作即可,因为 Redis 具有天然的幂等性。 - 在生产者发送数据时,添加全局唯一的 ID,消费者根据 ID 查询是否已处理过。
保证消费的顺序性:
如果有多个消费者,如何保证消费顺序?可以使用以下方法:
-
在上面的概念有提到过,Kafka 无法保证不同分区之间数据的有序性,但分区内肯定是按顺序消费的
-
为每个消息指定一个 key,可以对 key 使用 hash 方法,判断需要存放到那个分区,例如订单 ID。Kafka 会将具有相同 key 的消息分配到同一个 partition 中,保证顺序。
-
使用多线程并发处理消息,但要注意处理顺序。
kafka 实现百万级并发的底层技术原理
Kafka 消息队列,并不是消费一条消息就把该消息给删除的,而是会持久化的。既然需要持久化,那就需要跟磁盘打交道,实现磁盘 IO。为什么都需要磁盘 IO 了,并发还能这么快?可以实现百万级的并发量?
页缓存技术 + 磁盘顺序写:
-
Kafka 每次接收到数据都会写入磁盘,但它巧妙地利用操作系统的页缓存(Page Cache)来实现文件写入。
-
Page Cache 是操作系统内存中的缓存,
Kafka 将数据直接写入这个缓存,而不是直接写入磁盘文件
。 -
接下来,操作系统自行决定何时将缓存中的数据真正刷入磁盘文件。这样,磁盘写性能得到了显著提升。
-
此外,Kafka 采用磁盘顺序写的方式,只追加数据到文件末尾,而不是随机位置修改数据。这进一步提高了性能。
什么是也缓存
-
总所周知,操作系统分用户态跟内核态,用户态的数据需要保存到磁盘,必须经过内核态,内核态来将数据写入磁盘。但是并不是每进来一条数据我就进行一次磁盘 IO 的,就像 MySQL 查询一样,并不是一条一条的查,而是一页一页得查。
-
所以当用户对文件中的某个数据块进行读写操作时,内核首先会申请一个内存页(称为
页缓存(以 Linux 为例 大小为 4kb)
)与文件中的数据块进行绑定 -
所以,虽然 Kafka 的数据会进行持久化,但实际是将数据写入了页缓存,所以 Kafka 的瓶颈并不是磁盘 IO
什么是顺序读写,与随机读写有什么区别
- 顺序读写 :
- 顺序读写是按照数据的先后顺序进行工作的方式。当
处理大型文件
时,顺序读写通常能获得较理想的速度。 - 具体表现为读写时间较短且具备连续性,适用于大文件拷贝等场景。
- 顺序读写速度通常以每秒读取和写入的数据量(MB/s)来衡量。
- 顺序读写是按照数据的先后顺序进行工作的方式。当
- 随机读写 :
- 随机读写的特点在于
读写具有随机性
,不遵循文件的先后顺序进行数据的读取和写入。 - 适用于
处理大量小文件
的读写,例如电脑开机、系统文件更新、网页缓存写入、图片拷贝等。 - 随机读写速度通常以每秒读写操作的次数(IOPS)来衡量。
- 随机读写的特点在于
- 为什么顺序读写会提高Kafka 的性能
-
在高并发的情况下,比如每秒百万的并发,这一秒产生的数据量就很庞大了,使用顺序读写就不用频繁
寻址-->写入
了,而是直接写在文件后面 -
读取数据进行消费也是一样,按顺序进行读取,就不用每次读取一条消息都要寻址换道了
-
页缓存技术 + 磁盘顺序写 基本实现了写是基于内存写,并且实现了持久化,但是读还是基于磁盘 IO 读的呀,并且还要经过内核态与用户态的数据转换,也就是数据拷贝,并且不止一次,如下图
-
- 读取数据时先看看要读的数据在不在
os cache
里,如果不在的话就从磁盘文件里读取数据后放入os cache。 - 接着从操作系统的
os cache
里拷贝数据
到应用程序进程
的缓存里 - 再从
应用程序进程
的缓存里拷贝数据
到操作系统层面的Socket
缓存里 - 最后从
Socket
缓存里提取数据后发送到网卡
,最后发送出去给下游消费 想一想,如果是你,向提高上面的读性能,会从哪里入手?
- 首先肯定不会在磁盘文件读数据到 cache 缓存啦,如果这里没了,数据就有可能读不到了
- 所以我能不能把两次拷贝数据的过程省略掉,直接把数据从 cache 发送到网卡?
零拷贝技术
- 在消费数据时,Kafka 引入了零拷贝技术,避免了不必要的数据拷贝。
- 零拷贝允许操作系统的
缓存中的数据直接发送到网卡
,跳过了应用程序缓存的拷贝步骤。 - 这大大提高了数据消费时读取文件数据的性能。
-
通过零拷贝技术,就不需要把os cache里的数据拷贝到应用缓存,再从应用缓存拷贝到Socket缓存了,
两次拷贝都省略了
,所以叫做零拷贝
。 -
对Socket缓存仅仅就是拷贝数据的描述符过去,然后数据就直接从os cache中发送到网卡上去了,这个过程大大的提升了数据消费时读取文件数据的性能。
-
而且大家会注意到,在从磁盘读数据的时候,会先看看os cache内存中是否有,如果有的话,其实读数据都是直接读内存的。
-
如果kafka集群经过良好的调优,大家会发现大量的数据都是直接写入os cache中,然后读数据的时候也是从os cache中读。
-
相当于是Kafka完全基于内存提供数据的写和读了,所以这个整体性能会极其的高。
所以 Kafka 的上限最主要的不是 CPU、不是内存、而是带宽,带宽有多猛,你的 Kafka 就有多猛。不过前提也是 CPU 和内存不要太拉跨