Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)

嘟嘟鱼啊鱼
发布于 2023-6-27 16:49
浏览
0收藏

你好,我是码哥,可以叫我靓仔

作者:mo

引言

在探究 Kafka 核心知识之前,我们先思考一个问题:什么场景会促使我们使用 Kafka?  说到这里,我们头脑中或多或少会蹦出异步解耦削峰填谷等字样,是的,这就是 Kafka 最重要的落地场景。

  • 异步解耦:同步调用转换成异步消息通知,实现生产者和消费者的解耦。想象一个场景,在商品交易时,在订单创建完成之后,需要触发一系列其他的操作,比如进行用户订单数据的统计、给用户发送短信、给用户发送邮件等等。如果所有操作都采用同步方式实现,将严重影响系统性能。针对此场景,我们可以利用消息中间件解耦订单创建操作和其他后续行为。
  • 削峰填谷:利用 broker 缓冲上游生产者瞬时突发的流量,使消费者消费流量整体平滑。对于发送能力很强的上游系统,如果没有消息中间件的保护,下游系统可能会直接被压垮导致全链路服务雪崩。想象秒杀业务场景,上游业务发起下单请求,下游业务执行秒杀业务(库存检查,库存冻结,余额冻结,生成订单等等),下游业务处理的逻辑是相当复杂的,并发能力有限,如果上游服务不做限流策略,瞬时可能把下游服务压垮。针对此场景,我们可以利用 MQ 来做削峰填谷,让高峰流量填充低谷空闲资源,达到系统资源的合理利用。

通过上述例子可以发现交易、支付等场景常需要异步解耦削峰填谷功能解决问题,而交易、支付等场景对性能、可靠性要求特别高。那么,我们本文的主角 Kafka 能否满足相应要求呢?下面我们来探讨下。

Kafka 宏观认知

在探究 Kafka 的高性能、高可靠性之前,我们从宏观上来看下 Kafka 的系统架构:

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

如上图所示,Kafka 由 Producer、Broker、Consumer 以及负责集群管理的 ZooKeeper 组成,各部分功能如下:

  • Producer:生产者,负责消息的创建并通过一定的路由策略发送消息到合适的 Broker;
  • Broker:服务实例,负责消息的持久化、中转等功能;
  • Consumer:消费者,负责从 Broker 中拉取(Pull)订阅的消息并进行消费,通常多个消费者构成一个分组,消息只能被同组中的一个消费者消费;
  • ZooKeeper:负责 broker、consumer 集群元数据的管理等;(注意:Producer 端直接连接 broker,不在 zk 上存任何数据,只是通过 ZK 监听 broker 和 topic 等信息

上图消息流转过程中,还有几个特别重要的概念—主题(Topic)、分区(Partition)、分段(segment)、位移(offset)。

  • topic:消息主题。Kafka 按 topic 对消息进行分类,我们在收发消息时只需指定 topic。
  • partition:分区。为了提升系统的吞吐,一个 topic 下通常有多个 partition,partition 分布在不同的 Broker 上,用于存储 topic 的消息,这使 Kafka 可以在多台机器上处理、存储消息,给 kafka 提供给了并行的消息处理能力和横向扩容能力。另外,为了提升系统的可靠性,partition 通常会分组,且每组有一个主 partition、多个副本 partition,且分布在不同的 broker 上,从而起到容灾的作用。
  • segment:分段。宏观上看,一个 partition 对应一个日志(Log)。由于生产者生产的消息会不断追加到 log 文件末尾,为防止 log 文件过大导致数据检索效率低下,Kafka 采取了分段和索引机制,将每个 partition 分为多个 segment,同时也便于消息的维护和清理。每个 segment 包含一个.log 日志文件、两个索引(.index、timeindex)文件以及其他可能的文件。每个 Segment 的数据文件以该段中最小的 offset 为文件名,当查找 offset 的 Message 的时候,通过二分查找快找到 Message 所处于的 Segment 中。
  • offset:消息在日志中的位置,消息在被追加到分区日志文件的时候都会分配一个特定的偏移量。offset 是消息在分区中的唯一标识,是一个单调递增且不变的值。Kafka 通过它来保证消息在分区内的顺序性,不过 offset 并不跨越分区,也就是说,Kafka 保证的是分区有序而不是主题有序

Kafka 高可靠性、高性能探究

在对 Kafka 的整体系统框架及相关概念简单了解后,下面我们来进一步深入探讨下高可靠性、高性能实现原理。

Kafka 高可靠性探究

Kafka 高可靠性的核心是保证消息在传递过程中不丢失,涉及如下核心环节:

  • 消息从生产者可靠地发送至 Broker;-- 网络、本地丢数据;
  • 发送到 Broker 的消息可靠持久化;-- Pagecache 缓存落盘、单点崩溃、主从同步跨网络;
  • 消费者从 Broker 消费到消息且最好只消费一次 -- 跨网络消息传输 。
消息从生产者可靠地发送至 Broker

为了保障消息从生产者可靠地发送至 Broker,我们需要确保两点;

  1. Producer 发送消息后,能够收到来自 Broker 的消息保存成功 ack;
  2. Producer 发送消息后,能够捕获超时、失败 ack 等异常 ack 并做处理。

ack 策略

针对问题 1,Kafka 为我们提供了三种 ack 策略,

  • Request.required.acks = 0:请求发送即认为成功,不关心有没有写成功,常用于日志进行分析场景;
  • Request.required.acks = 1:当 leader partition 写入成功以后,才算写入成功,有丢数据的可能;
  • Request.required.acks= -1:ISR 列表里面的所有副本都写完以后,这条消息才算写入成功,强可靠性保证;

为了实现强可靠的 kafka 系统,我们需要设置 Request.required.acks= -1,同时还会设置集群中处于正常同步状态的副本 follower 数量 min.insync.replicas>2,另外,设置 unclean.leader.election.enable=false 使得集群中 ISR 的 follower 才可变成新的 leader,避免特殊情况下消息截断的出现。

消息发送策略

针对问题 2,kafka 提供两类消息发送方式:同步(sync)发送和异步(async)发送,相关参数如下:

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

以 sarama 实现为例,在消息发送的过程中,无论是同步发送还是异步发送都会涉及到两个协程--负责消息发送的主协程和负责消息分发的 dispatcher 协程。

异步发送

对于异步发送(ack != 0 场景,等于 0 时不关心写 kafka 结果,后文详细讲解)而言,其流程大概如下:

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

  1. 主协程中调用异步发送 kafka 消息的时候,其本质是将消息体放进了一个 input 的 channel,只要入 channel 成功,则这个函数直接返回,不会产生任何阻塞。相反,如果入 channel 失败,则会返回错误信息。因此调用 async 写入的时候返回的错误信息是入 channel 的错误信息,至于具体最终消息有没有发送到 kafka 的 broker,我们无法从返回值得知。
  2. 当消息进入 input 的 channel 后,会有另一个dispatcher 的协程负责遍历 input,来真正发送消息到特定 Broker 上的主 Partition 上。发送结果通过一个异步协程进行监听,循环处理 err channel 和 success channel,出现了 error 就记一个日志。因此异步写入场景时,写 kafka 的错误信息,我们暂时仅能够从这个错误日志来得知具体发生了什么错,并且也不支持我们自建函数进行兜底处理,这一点在 trpc-go 的官方也得到了承认。

同步发送

同步发送(ack != 0 场景)是在异步发送的基础上加以条件限制实现的。同步消息发送在 newSyncProducerFromAsyncProducer 中开启两个异步协程处理消息成功与失败的“回调”,并使用 waitGroup 进行等待,从而将异步操作转变为同步操作,其流程大概如下:

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

通过上述分析可以发现,kafka 消息发送本质上都是异步的,不过同步发送通过 waitGroup 将异步操作转变为同步操作。同步发送在一定程度上确保了我们在跨网络向 Broker 传输消息时,消息一定可以可靠地传输到 Broker。因为在同步发送场景我们可以明确感知消息是否发送至 Broker,若因网络抖动、机器宕机等故障导致消息发送失败或结果不明,可通过重试等手段确保消息至少一次(at least once) 发送到 Broker。另外,Kafka(0.11.0.0 版本后)还为 Producer 提供两种机制来实现精确一次(exactly once) 消息发送:幂等性(Idempotence)和事务(Transaction)。

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

小结

通过 ack 策略配置、同步发送、事务消息组合能力,我们可以实现exactly once 语意跨网络向 Broker 传输消息。但是,Producer 收到 Broker 的成功 ack,消息一定不会丢失吗?为了搞清这个问题,我们首先要搞明白 Broker 在接收到消息后做了哪些处理。

发送到 Broker 的消息可靠持久化

为了确保 Producer 收到 Broker 的成功 ack 后,消息一定不在 Broker 环节丢失,我们核心要关注以下几点:

  • Broker 返回 Producer 成功 ack 时,消息是否已经落盘;
  • Broker 宕机是否会导致数据丢失,容灾机制是什么;
  • Replica 副本机制带来的多副本间数据同步一致性问题如何解决;

Broker 异步刷盘机制

kafka 为了获得更高吞吐,Broker 接收到消息后只是将数据写入 PageCache 后便认为消息已写入成功,而 PageCache 中的数据通过 linux 的 flusher 程序进行异步刷盘(刷盘触发条:主动调用 sync 或 fsync 函数、可用内存低于阀值、dirty data 时间达到阀值),将数据顺序写到磁盘。消息处理示意图如下:

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

由于消息是写入到 pageCache,单机场景,如果还没刷盘 Broker 就宕机了,那么 Producer 产生的这部分数据就可能丢失。为了解决单机故障可能带来的数据丢失问题,Kafka 为分区引入了副本机制。

Replica 副本机制

Kafka 每组分区通常有多个副本,同组分区的不同副本分布在不同的 Broker 上,保存相同的消息(可能有滞后)。副本之间是“一主多从”的关系,其中 leader 副本负责处理读写请求,follower 副本负责从 leader 拉取消息进行同步。分区的所有副本统称为 AR(Assigned Replicas),其中所有与 leader 副本保持一定同步的副本(包括 leader 副本在内)组成 ISR(In-Sync Replicas),与 leader 同步滞后过多的副本组成 OSR(Out-of-Sync Replicas),由此可见,AR=ISR+OSR。


follower 副本是否与 leader 同步的判断标准取决于 Broker 端参数 replica.lag.time.max.ms(默认为 10 秒),follower 默认每隔 500ms 向 leader fetch 一次数据,只要一个 Follower 副本落后 Leader 副本的时间不连续超过 10 秒,那么 Kafka 就认为该 Follower 副本与 leader 是同步的。在正常情况下,所有的 follower 副本都应该与 leader 副本保持一定程度的同步,即 AR=ISR,OSR 集合为空。

当 leader 副本所在 Broker 宕机时,Kafka 会借助 ZK 从 follower 副本中选举新的 leader 继续对外提供服务,实现故障的自动转移,保证服务可用。为了使选举的新 leader 和旧 leader 数据尽可能一致,当 leader 副本发生故障时,默认情况下只有在 ISR 集合中的副本才有资格被选举为新的 leader,而在 OSR 集合中的副本则没有任何机会(可通过设置 unclean.leader.election.enable 改变)。

当 Kafka 通过多副本机制解决单机故障问题时,同时也带来了多副本间数据同步一致性问题。Kafka 通过高水位更新机制、副本同步机制、 Leader Epoch 等多种措施解决了多副本间数据同步一致性问题,下面我们来依次看下这几大措施。

HW 和 LEO

首先,我们来看下两个和 Kafka 中日志相关的重要概念 HW 和 LEO:

  • HW: High Watermark,高水位,表示已经提交(commit)的最大日志偏移量,Kafka 中某条日志“已提交”的意思是 ISR 中所有节点都包含了此条日志,并且消费者只能消费 HW 之前的数据;
  • LEO: Log End Offset,表示当前 log 文件中下一条待写入消息的 offset;

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

如上图所示,它代表一个日志文件,这个日志文件中有 8 条消息,0 至 5 之间的消息为已提交消息,5 至 7 的消息为未提交消息。日志文件的 HW 为 6,表示消费者只能拉取到 5 之前的消息,而 offset 为 5 的消息对消费者而言是不可见的。日志文件的 LEO 为 8,下一条消息将在此处写入。

注意:所有副本都有对应的 HW 和 LEO,只不过 Leader 副本比较特殊,Kafka 使用 Leader 副本的高水位来定义所在分区的高水位。换句话说,分区的高水位就是其 Leader 副本的高水位。Leader 副本和 Follower 副本的 HW 有如下特点:

  • Leader HW:min(所有副本 LEO),为此 Leader 副本不仅要保存自己的 HW 和 LEO,还要保存 follower 副本的 HW 和 LEO,而 follower 副本只需保存自己的 HW 和 LEO;
  • Follower HW:min(follower 自身 LEO,leader HW)。

注意:为方便描述,下面Leader HW简记为HWL,Follower HW简记为F,Leader LEO简记为LEOL ,Follower LEO简记为LEOF

下面我们演示一次完整的 HW / LEO 更新流程:

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

  1. 初始状态

HWL=0,LEOL=0,HWF=0,LEOF=0。

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

  1. Follower 第一次 fetch
  • Leader收到Producer发来的一条消息完成存储, 更新LEOL=1;
  • Follower从Leader fetch数据,  Leader收到请求,记录follower的LEOF =0,并且尝试更新HWL =min(全部副本LEO)=0;
  • eade返回HWL=0和LEOL=1给Follower,Follower存储消息并更新LEOF =1, HW=min(LEOF,HWL)=0。

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

  1. Follower 第二次 fetch
  • Follower再次从Leader fetch数据,  Leader收到请求,记录follower的LEOF =1,并且尝试更新HWL =min(全部副本LEO)=1;
  • leade返回HWL=1和LEOL=1给Follower,Leader收到请求,更新自己的 HW=min(LEOF,HWL)=1。

上述更新流程中 Follower 和 Leader 的 HW 更新有时间 GAP。如果 Leader 节点在此期间发生故障,则 Follower 的 HW 和 Leader 的 HW 可能会处于不一致状态,如果 Followe 被选为新的 Leader 并且以自己的 HW 为准对外提供服务,则可能带来数据丢失或数据错乱问题。

KIP-101 问题:数据丢失&数据错乱 ^参 5^

数据丢失

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

第 1 步:

  1. 副本 B 作为 leader 收到 producer 的 m2 消息并写入本地文件,等待副本 A 拉取。
  2. 副本 A 发起消息拉取请求,请求中携带自己的最新的日志 offset(LEO=1),B 收到后更新自己的 HW 为 1,并将 HW=1 的信息以及消息 m2 返回给 A。
  3. A 收到拉取结果后更新本地的 HW 为 1,并将 m2 写入本地文件。发起新一轮拉取请求(LEO=2),B 收到 A 拉取请求后更新自己的 HW 为 2,没有新数据只将 HW=2 的信息返回给 A,并且回复给 producer 写入成功。此处的状态就是图中第一步的状态。

第 2 步:

此时,如果没有异常,A 会收到 B 的回复,得知目前的 HW 为 2,然后更新自身的 HW 为 2。但在此时 A 重启了,没有来得及收到 B 的回复,此时 B 仍然是 leader。A 重启之后会以 HW 为标准截断自己的日志,因为 A 作为 follower 不知道多出的日志是否是被提交过的,防止数据不一致从而截断多余的数据并尝试从 leader 那里重新同步。

第 3 步:

B 崩溃了,min.isr 设置的是 1,所以 zookeeper 会从 ISR 中再选择一个作为 leader,也就是 A,但是 A 的数据不是完整的,从而出现了数据丢失现象。

问题在哪里?在于 A 重启之后以 HW 为标准截断了多余的日志。不截断行不行?不行,因为这个日志可能没被提交过(也就是没有被 ISR 中的所有节点写入过),如果保留会导致日志错乱。

数据错乱

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

在分析日志错乱的问题之前,我们需要了解到 kafka 的副本可靠性保证有一个前提:在 ISR 中至少有一个节点。如果节点均宕机的情况下,是不保证可靠性的,在这种情况会出现数据丢失,数据丢失是可接受的。这里我们分析的问题比数据丢失更加槽糕,会引发日志错乱甚至导致整个系统异常,而这是不可接受的。

第 1 步:

  1. A 和 B 均为 ISR 中的节点。副本 A 作为 leader,收到 producer 的消息 m2 的请求后写入 PageCache 并在某个时刻刷新到本地磁盘。
  2. 副本 B 拉取到 m2 后写入 PageCage 后(尚未刷盘)再次去 A 中拉取新消息并告知 A 自己的 LEO=2,A 收到更新自己的 HW 为 1 并回复给 producer 成功。
  3. 此时 A 和 B 同时宕机,B 的 m2 由于尚未刷盘,所以 m2 消息丢失。此时的状态就是第 1 步的状态。

第 2 步:

由于 A 和 B 均宕机,而 min.isr=1 并且 unclean.leader.election.enable=true(关闭 unclean 选择策略),所以 Kafka 会等到第一个 ISR 中的节点恢复并选为 leader,这里不幸的是 B 被选为 leader,而且还接收到 producer 发来的新消息 m3。注意,这里丢失 m2 消息是可接受的,毕竟所有节点都宕机了。

第 3 步:

A 恢复重启后发现自己是 follower,而且 HW 为 2,并没有多余的数据需要截断,所以开始和 B 进行新一轮的同步。但此时 A 和 B 均没有意识到,offset 为 1 的消息不一致了。

问题在哪里?在于日志的写入是异步的,上面也提到 Kafka 的副本策略的一个设计是消息的持久化是异步的,这就会导致在场景二的情况下被选出的 leader 不一定包含所有数据,从而引发日志错乱的问题。

Leader Epoch

为了解决上述缺陷,Kafka 引入了 Leader Epoch 的概念。leader epoch 和 raft 中的任期号的概念很类似,每次重新选择 leader 的时候,用一个严格单调递增的 id 来标志,可以让所有 follower 意识到 leader 的变化。而 follower 也不再以 HW 为准,每次奔溃重启后都需要去 leader 那边确认下当前 leader 的日志是从哪个 offset 开始的。下面看下 Leader Epoch 是如何解决上面两个问题的。

数据丢失解决

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

这里的关键点在于副本 A 重启后作为 follower,不是忙着以 HW 为准截断自己的日志,而是先发起 LeaderEpochRequest 询问副本 B 第 0 代的最新的偏移量是多少,副本 B 会返回自己的 LEO 为 2 给副本 A,A 此时就知道消息 m2 不能被截断,所以 m2 得到了保留。当 A 选为 leader 的时候就保留了所有已提交的日志,日志丢失的问题得到解决。

如果发起 LeaderEpochRequest 的时候就已经挂了怎么办?这种场景下,不会出现日志丢失,因为副本 A 被选为 leader 后不会截断自己的日志,日志截断只会发生在 follower 身上。

数据错乱解决

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

这里的关键点还是在第 3 步,副本 A 重启作为 follower 的第一步还是需要发起 LeaderEpochRequest 询问 leader 当前第 0 代最新的偏移量是多少,由于副本 B 已经经过换代,所以会返回给 A 第 1 代的起始偏移(也就是 1),A 发现冲突后会截断自己偏移量为 1 的日志,并重新开始和 leader 同步。副本 A 和副本 B 的日志达到了一致,解决了日志错乱。

小结

Broker 接收到消息后只是将数据写入 PageCache 后便认为消息已写入成功,但是,通过副本机制并结合 ACK 策略可以大概率规避单机宕机带来的数据丢失问题,并通过 HW、副本同步机制、 Leader Epoch 等多种措施解决了多副本间数据同步一致性问题,最终实现了 Broker 数据的可靠持久化。

消费者从 Broker 消费到消息且最好只消费一次

Consumer 在消费消息的过程中需要向 Kafka 汇报自己的位移数据,只有当 Consumer 向 Kafka 汇报了消息位移,该条消息才会被 Broker 认为已经被消费。因此,Consumer 端消息的可靠性主要和 offset 提交方式有关,Kafka 消费端提供了两种消息提交方式:

Kafka 核心全面总结,高可靠高性能核心原理探究(上篇)-鸿蒙开发者社区

正常情况下我们很难实现 exactly once 语意的消息,通常是通过手动提交+幂等实现消息的可靠消费。




文章转载自公众号:码哥字节

分类
标签
已于2023-6-27 16:49:51修改
收藏
回复
举报
回复
    相关推荐