【Kafka】实现百万级并发的底层原理

前言

Kafka 是一个分布式高吞吐量高扩展性消息队列系统。它最初由 LinkedIn 公司开发,后来于 2010 年贡献给 Apache 基金会,成为一个开源项目。Kafka 主要应用于日志收集系统和消息系统,类似于其他消息队列中间件(例如 RabbitMQ、ActiveMQ),但 Kafka 的优点在于其稳定性、高效性以及丰富的功能。

概念

简单了解 Kafka 的相关概念:

  1. Topic(主题)
    • 每个发送到 Kafka 的消息都有一个主题,也可以看作是一个类别,类似于数据库中的表名。例如,如果发送一个主题为 "order" 的消息,那么该主题下就会有多条关于订单的消息。
  2. Producer(生产者)
    • 生产者负责发送消息到 Kafka 服务器。每条消息必须属于一个 Topic(主题)类似MySQL 中的 insert 语句。生产者不断地向 Kafka 发送消息。
    • 注意上面说的是每条消息必须属于一个主题,并没有说一个生产者只能生产一个主题的消息,可以生产多个主题的消息
  3. Consumer(消费者)
    • 消费者负责订阅 Kafka 中的主题(Topic)并从中拉取消息,类型 MySQL 的 select,不同的是,Kafka 只能消费一次,如想重复消费则要使用偏移量特殊设置
    • 每个消费者都属于一个特定的消费组(Consumer Group),如果没有指定消费组,则会默认一个消费组。
    • 消费者从分区中获取消息,处理它们并执行相应的业务逻辑。
    • 例如你订阅了某个公众号,即这个公众号被多个消费者订阅,公众号发送消息到消息队列时,订阅这个公众号的消费者就去这个公众号中消费数据。
  4. Consumer Group (消费组)
    • 消费组是一组具有相同目标的消费者(具有处理相同逻辑的业务,比如拿到订单消息,处理相关的订单业务,如果同一组多个消费者拿到这个订单,那这个订单就被处理了多次,造成重复消费)。
    • 当消息发布到主题后,只会被投递给订阅它的每个消费组中的一个消费者,也就是说,同一个主题下的同一个分区的数据只会给同一个消费组一个消费者消费,这样设计是为了避免重复消费。
    • 每个消费者在消费组中负责处理不同分区的消息,也就是说,同一个主题下的不同一个分区的数据可以给同一个消费组不同消费者消费
    • 消费组的存在确保了消息的负载均衡高可用性
  5. 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 这个顺序。
  6. Replica(副本)
    • 副本用于保护分区中的数据完整性,防止数据丢失或服务器宕机。
    • Kafka 将副本分布在不同的服务器上,实现负载均衡
    • 副本分区包括一个主分区(leader)和多个从分区(follower),确保数据的完整性。
    • 熟悉的药方熟悉的味道,这难道不是 Redis 的主从复制吗(狗头)
  7. Broker(实例或节点)
    • Kafka 集群由多个 Broker 组成,每个 Broker 是一个 Kafka 实例,也就是说,每启动一个 Kafka 就创建了一个 Broker。
    • 多个 Broker 构成分布式 Kafka 集群,提高吞吐率和效率。

单机消息发送与消费

集群消息发送与消费

结合上面的那些概念,看明白这两张图,就知道 Kafka 的消息发送与消费大概是怎么个流程了

如何保证消息不丢失不重复消费

保证写入数据不丢失

acks=allacks=-1 :生产者在所有同步副本收到数据后收到确认。此设置提供最高的数据持久性。即确认的的确确写入了本地日志,并且 follower 也已经保存了副本,即使 leader 宕机了,数据也不会丢失

概念

  1. 分区级别的复制
    • 复制因子:这个数是多少,则说明 Kafka 有多少个副本。如果复制因子为 1,则只有一个副本 Leader,没有 follower。所以一般需要大于 1
    • 这允许 Kafka 在集群服务器发生故障时自动切换到这些副本,以便在出现故障时消息仍然可用
  2. Leader 和 Followers
    • 每个分区副本分别在不同的 Kafka brokers 上。
    • 在 n 个副本中,一个副本作为 leader ,其他副本成为 followers
    • producer 只能往 leader 分区上写数据(读也只能从 leader 分区上进行),followers 只按顺序从 leader 上复制日志。
  3. 复制协议
    • Kafka 的复制协议确保了消息的一致性和可靠性
    • 当 producer 往 leader 上发送消息时,leader 将消息复制到所有 followers。
    • 只有将消息成功复制到所有同步副本(ISR)后,这条消息才算被提交(acks=all),消费者只会消费已经提交的数据
  4. 同步副本列表(ISR)
    • 每个分区的 leader 维护一个 in-sync replica(同步副本列表,也称为 ISR)。
    • 只有在 ISR 中的副本才能被认为是跟上 leader 写进度的,如果有副本处于落后状态,则将其移除 ISR。
    • Kafka 密切监控 ISR 中的副本,以确保数据的完整性和可用性。
    • 当 leader 发生故障时,只能在 ISR 中的 follower 才能晋升为 leader
  5. 偏移量
    • 偏移量是一个连续的整数值,用于唯一标识分区中的每一条消息。
    • 每个分区都有自己的偏移量序列,从0开始递增。
    • 消费者通过维护自己的偏移量来记录已经消费的消息。
    • ISR 维护自己的偏移量来记录同步到哪条记录了
    • 偏移量一般使用**HW(High Watermark)** 来表示
  6. 参数
    • 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=allacks=-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。

保证消费者不会重复消费

  1. Offset 偏移量

    • Kafka 中的每条消息都有一个唯一的 offset,代表消息的序号。消费者消费消息后,会定期提交已消费的消息的 offset。这样,即使消费者重启,也能从上次消费的位置继续。

    • 如上面的写入例子,HW 表示最大偏移量 9,每添加一条数据偏移量加一,所以每条数据都有其对应的偏移量。比如 0 对应 1,8 对应 9

    • 如图是消费过程的 offset 的提交过程

如果在消费者消费完消息时,提交 offset 的过程中失败了。Kafka 没有收到 offset 的提交,下次读取数据还是从上一个偏移量读取,还是会造成重复消费

  1. 幂等性 :重复消费不可怕,但必须保证幂等性

    幂等性是指对同一数据或请求的多次操作不会改变其状态

    • 比如一个查询按钮,无论查询多少次,也不会改变其状态,所以查询就是一个天生的幂等性
    • 比如一个新增按钮,如果点击保存时,由于网络原因没有及时响应,多点了几次,导致同样的数据连续创建了几条记录,所以这就是非幂等性

    为实现幂等性,可以考虑以下方法:

    • 数据写入数据库前先判断是否已存在,如果存在则执行更新操作或者不操作。
    • 对于写入 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

    什么是顺序读写,与随机读写有什么区别

    1. 顺序读写
      • 顺序读写是按照数据的先后顺序进行工作的方式。当处理大型文件时,顺序读写通常能获得较理想的速度。
      • 具体表现为读写时间较短且具备连续性,适用于大文件拷贝等场景。
      • 顺序读写速度通常以每秒读取和写入的数据量(MB/s)来衡量。
    2. 随机读写
      • 随机读写的特点在于读写具有随机性,不遵循文件的先后顺序进行数据的读取和写入。
      • 适用于处理大量小文件的读写,例如电脑开机、系统文件更新、网页缓存写入、图片拷贝等。
      • 随机读写速度通常以每秒读写操作的次数(IOPS)来衡量。
    3. 为什么顺序读写会提高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 和内存不要太拉跨

相关推荐
哎呦没13 分钟前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
2401_8576009516 分钟前
SpringBoot框架的企业资产管理自动化
spring boot·后端·自动化
m0_571957582 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
Chrikk6 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*6 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue6 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man6 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang