1. 你想让你所在的世界毁灭吗?------谈谈Kafka的高可用
1. 1 三峡大坝级工程------消息队列
我们知道MQ(Message Queue,消息队列)主要解决的是是解耦、削峰填谷、异步消息、和持久化等问题,同时需要保证高性能、高可用、可伸缩和最终一致性架构。这些概念理解起来都很抽象,但是如果我们将这些概念与自然界的水循环系统类比起来可能会更好理解。实际上一般的计算机系统可以看作是数据流动的过程。例如:淘宝的订单信息流向阿里的后台服务器集群,进行处理之后的数据再流回消费者客户端。
这样一个过程很容易让我们想起自然界的水循环过程,山川溪流收集雨水,汇集成河流、江川,最后流向海洋,最后再以雨水的形式循环起来,计算机应用中的数据流动过程其实就类似于自然界的水循环过程。而作为消息队列或者缓存队列的MQ其实就类似于三峡大坝的概念,上游的小溪流河流就类似于生产者的概念,下游的河道类似于消费者概念,而我们的海就类似于数据库集群的概念。接下来我们将从这个新奇的角度来理解MQ所提供的这些功能,以及为什么要有这些功能。
解耦:
首先是解耦的功能,我们不妨想想水库修建后的效果,上游的流量不再时刻影响着下游的流量,下游可以时刻根据自己的需求通过开闭闸门的方式控制流量的大小。这样一来,不需要担心上游流量暴涨而导致下游无法承受导致的洪水,也不需要担心当上游流量降低导致下游无水可用。所以解耦实际上来说,就是将消息生产者和消费者的事务互相独立开来,提升消息的处理速度和系统的并发量,维护系统整体稳定性。
削峰填谷:
应用系统跟气候一样,不会总是一成不变的,有雨季也有旱季。当雨季来临,上流河水流量暴涨,如果不加以处理就有可能给下流造成严重的洪涝灾害,对应计算机系统而言就是导致服务崩溃。因此,修建水库的作用,就是再雨季削减上流的流量,防止过度的流量导致下游的崩溃;旱季来临,当上游流量骤减,则利用存储的水进行合理分发。这一过程其实和MQ的作用可以说是一摸一样,最终目的都是有利于保障系统整体的稳定性。
持久化:
水库顾名思义就是将水积攒起来,这个过程水没有消失,既没有被蒸发也没有被消费,只是存储在我们的水库当中等待下游需要的时候进行消费。而MQ的作用也是一样,消息到MQ之后不是说没有人消费就把消息丢弃掉了,而是在MQ内部有相应的持久化机制,当消费者再次上线的时候可以继续进行消费。
1.2 三峡大坝与元宇宙------MQ的持久性和可靠性
1.2.1 可靠性
虽然我们的MQ在计算系统中的作用和我们的三峡大坝一样是战略级级工程,为我们系统提供了稳定性和可靠性的保障。但是我们的MQ实际上并没那么可靠,而这不可靠性的来源倒不是因为MQ本身不可靠,而是MQ所处于的那个世界是不怎么可靠的。 为什么说MQ所在的世界不稳定呢?其实像三峡大坝所在的现实世界是贼稳定的,不能说突然之间我们的现实世界都给干没了或者宕机,导致三峡大坝不能工作或者里面的水消失吧。所以三峡大坝所处环境本身的可靠性和稳定性是无需多虑的。然而,身处于我们计算机服务世界中的MQ可不太一样,随时面临着不可预知的风险,导致MQ所在的世界毁灭。
我们的MQ一般是部署在服务器上的,服务器一般就是一台台的非常可靠的电脑,但是这些电脑虽然非常可靠也逃不过随时可能出现的意外。例如:断电、自然灾害和系统异常等等,这些事情是随时可能发生从而导致MQ所在的世界毁灭的。但是好像听起来这个事情不是特别严重,总会来电吧?重启不就行了吗?很聪明,重启大法的确能解决计算机系统中99%的问题,但是显然不能解决我们MQ所要解决的问题。
实际上来说,MQ和三峡大坝一样是时时刻刻都在处理上流的数据流的,就像你很难接受三峡大坝突然消息1s一样,MQ哪怕宕机1s也是难以忍受的。首先最直接的后果会是下游的消费者系统的全面瘫痪,无事可做,而且对于整个数据系统来说,哪些消费者消费到什么状态了也是一个棘手的问题。因此来说,MQ系统的宕机哪怕只有1s对于大型的计算机应用系统来说都是不可接受的事情,严重的如银行系统肯能会导致各种存款订单系统的消息丢失,最终导致用户的金融账户出现异常造成巨大事故。
那么,能保证可靠性的最简单的办法是不是很容易想到,我有平行宇宙,我在我的平行宇宙中多建几个三峡大坝,每个大坝都是完整的全量水数据。当本宇宙的三峡大坝不行了,或者是被核弹攻击了或者是被三体人二向箔拍扁了这种极端情况发生之后。对吧,咋们快速的通过多元宇宙的备份还原技术,把其它宇宙的三峡大坝的备份给还原过来,或者说,咋们直接采用多元宇宙的选主模型重新选一个主宇宙出来,所有消费者用咱们新主宇宙的三峡大坝吧。
这样一来是不是三峡大坝的可靠性问题就得到了解决,虽然说多元宇宙的技术咱们还不成熟,但是在计算机领域分布式的技术还是挺成熟的,咋们的MQ有福咯,你的可靠性问题咱只能说不是问题!
1.2.2 持久性
接着上面说,其实吧数据和水还是有点儿区别的,水就是一氧化二氢,所有的水分子都是一样,一坨水分子组成的水咱们也看不出来啥子区别的嘛。但是数据可不一样,虽然咱都是0和1,但是不同的排列顺序组成的0和1可都是带有信息的啊,类比起来就是分子之间的排列顺序对于三峡大坝来说是重要的,而且必须维护成正确状态。
这也是我们说MQ所在的服务器宕机之后不能直接重启的原因,就像你说在三峡大坝被二向薄拍扁之后,咱们不还原一个有水的三峡大坝,而是还原一个三峡坝大的建筑行不行,水咋们以后再慢慢攒?当然不行,因为里面的水是怎么排列的信息也丢了,那要你这个MQ还有啥用?总不能你宕机一次人家银行卡里面的money就要少上几百万吧,那还不得把你家们都堵了。
因此,我们说持久性其实也是MQ一大重要的机制,对应三峡大坝来说,咋们不仅要保障三峡大坝的主体工程的完整性和可靠性,里面的水的可靠性咱也要保障。当然,水的持久性是好保障的,三峡大坝不久是一个坑吗?水流进来了自然就跑不掉了。不过对于计算机里面的数据来说可不是这样滴~计算机里面的数据一般是存在内存里面的,内存这玩意儿哪哪儿都好,就是断电数据就没了。
所以说,咱们MQ的内部还需要一套持久化的机制来保障数据能够及时的被持久化到磁盘里面去,完成咱们三峡大坝里面水资源的备份工作。
3. 多元宇宙详解------Kafka中的数据副本机制
3.1 分布式三峡大坝------Kafka多副本机制
Kafka本身是一个分布式的消息队列,能够便捷的支持横向拓展(可伸缩)和高可用。那么这句话我汤师爷给翻译翻译是什么意思,咱们先不说消费者和生产者,就是咱这个三峡大坝本身是可以随时在多元宇宙创建副本的,所有生产者的数据可以push到我多元宇宙中叫做Leader的三峡大坝中,同样消费者也可以从Leader中消费数据。同时,还存在着若干个三峡大坝的平行宇宙备份,他们被称作Follower,这些Follower可以在需要的时候被选举升级成Leader。
可伸缩:
那么这样一样,当上游的水的流量不断增加的时候怎么办?不要慌,我们直接new一个三峡大坝出来,接入到上游就可以了。这就是我们所说的可伸缩性,就是当系统流量不断增加的时候,可以方便且快速的增加我们Kafka集群的规模,来提升集群的处理能力。
高可用:
那三体人进攻导致三峡大坝消失咋办?不要慌,我们的多元宇宙三峡大坝集群可以快速的帮你选出一个Leader节点,咱继续从这个主节点来消费就行了,这就保障了咋们Kafka集群的高可用性。
那么问题就来到了咱们这个多元宇宙的三峡大坝集群该如何去实现了。首先我们不妨有个这样的假设,上游流到三峡大坝的水是有颜色的,而且每种颜色的水只和自己的同类玩耍(水的颜色就类似于topic的概念)。对于同一种颜色(如绿色)的水会被存储在三峡大坝里面的一个池子里面(partition),消费者想要消费这种颜色的水也是从这个池子里面拿。不同宇宙中的池子被分为了Leader和Follower两种类型,Leader的池子负责数据的写入和消费。Follower的池子不直接负责数据的消费和写入而是作为Leader的备份存在的,负责定期与Leader同步数据。如下图所示:
为了提高咱们三峡大坝的吞吐量,每种颜色的水会有多个池子(partition),为了保障可靠性每个池子会有多个副本(Follower)来作为备份,副本会被均匀的散落在多元宇宙中(broker)。同一种编号的池子,比如"池子-1"保存的消息的内容是一致的(可能由于消息同步机制的问题在某些时刻并不完全一致)。池子之间是"一主多从"的关系,当Leader池子出现异常的时候,能够快速的从Follower池子中选出新的Leader来对外提供服务,也就实现了故障的自动转移。
-
Replica(包括Leader和Follower) :副本,同一分区的不同副本保存的是相同的消息,为保证集群中的某个节点发生故障时,该节点上的 partition 数据不丢失,且 kafka 仍然能够继续工作,kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,一个 leader 和若干个 follower。
-
Leader (主池子):每个分区的多个副本中的"主副本",生产者以及消费者只与 Leader 交互。
-
Follower (从池子):每个分区的多个副本中的"从副本",负责实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,从 Follower 副本中重新选举新的 Leader 副本对外提供服务。
总结一下,在上面的例子中我们把Kafka看成三峡大坝,这个大坝很神奇在不同的多元宇宙中有不同的备份(broker),而且大坝中管理着来自上游生产者不同颜色的水(topic),相同颜色的水会流到同种类型的池子里面(partition),为了实现分布式三峡大坝的高性能,每种颜色的水(topic)会对应多个池子(partition 1、partition 2、...),而且这些池子本身在不同的多元宇宙中有备份(partition-leader,partition-follower)。这种听起来有点儿离谱的结构实际上支持了Kafka的高可用和高性能。
3.2 多元宇宙池子间的关系------AR、LSR、OSR、LEO、HW
AR (Assigned Replica): 分区中的所有副本统称为AR(某个池子在所有多元宇宙中的所有副本),AR=ISR+OSR。
ISR (In-Sync Replica): 所有与Leader副本保持一定程度同步的副本(包括Leader副本在内)组成ISR,ISR中的副本是可靠的,它们跟随Leader副本的进度。
OSR (Out-of-Sync Replica): 与Leader副本同步滞后过多的副本组成了OSR,这些副本可能由于网络故障或其他原因而无法与Leader保持同步。
LEO (Log End Offset): 每个副本都有内部的LEO,代表当前队列消息的最后一条偏移量offset,LEO是该副本消息日志的末尾,LEO的大小相当于当前日志分区中最后一条消息的offset值加1。分区ISR集合中的每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区的HW,对消费者而言只能消费HW之前的消息。(将要流进池子的最新消息的offset,也就是说LEO所对应的消息还并未写入)。
HW (High Watermark): 高水位代表所有ISR中的LEO最低的那个offset,它是消费者可见的最大消息offset,表示已经被所有ISR中的副本复制,HW表示数据在整个ISR中都得到了复制,是目前来说最可靠的数据。
解释上图的意思,在Kafka中消息会先被发送到Leader中,然后Follower再从Leader中拉取消息进行同步,同步期间内Follower副本消息相对于Leader会有一定程度上的滞后。前面所说的"一定程度上的之后"是可以通过参数来配置的,在正常情况下所有的Follower都应与Leader保持一定程度的同步,AR=ISR,OSR为空。
3.3 多元宇宙池子间消息复制流程------Partition副本消息复制流程
如图所示,假设ISR集合中有3个副本,Leader、Follower-1和Follower-2,此时分区LEO和HW都为2,消息3和4从生产者出发之后会先存入Leader副本。
消息写入Leader副本之后,由于Follower副本消费还未更新,所以HW依旧为2,而LEO为5说明下一条待写入的消息的offset为5。当Leader中3和4消息写入完毕之后,等待Follower-1和Follower-2来Leader中拉取增量数据。
在消息同步过程中,由于网络环境和硬件因素等不同,副本同步效率也不尽相同。如上图在某一时刻follower的消息只同步到了3,而Leader和Follower-1的消息最新为4,那么当前的HW则为3。
当所有的副本都成功写入了消息3和消息4,整个分区的HW=4,LEO=5。总的来说,Kafka的Partition之间的复制机制既不是完全同步的复制,也不是单纯的异步复制。同步复制要求所有能够工作的副本都完成复制这条消息才算已经成功提交,这种方式极大的影响了性能。而在异步复制的方式下,Follower副本异步的从Leader副本中复制数据,数据只要被Leader写入副本就认为已经提交成功,这种情况下就存在如果Follower副本还没完全复制而落后于Leader副本而此时Leader副本突然宕机,则会造成数据丢失的情况发生。
Kafka中使用的ISR机制则有效的权衡了数据的可靠性和性能之间的关系。ISR机制中由于存在HW和LEO的概念,Kafka集群中始终可以知道哪些数据目前而言是有效的,既避免了同步复制所导致的性能低下问题,也避免了异步复制所导致的数据丢失的问题。因为只要HW得到了更新,那么Kafka集群就可以知道当前Partition中最新的有效数据,可以进行消费。而如果此时集群Leader节点出现了宕机,那么数据恢复的时候也可以从HW的位置进行恢复,从而避免数据的丢失。
- 多元宇宙管理者------zookeeper 从前面的描述中可以看出,我们的三峡大坝多元宇宙实际上是非常复杂的,不仅仅宇宙(broker)、三峡大坝(kafka)、池子(partition),甚至其中的水(topic)都是分不同的颜色,池子(partition)之间的主从关系等等都会非常复杂。那么,这么多的复杂信息该怎么来管理呢?zookeeper为我们提供的就是分布式系统中的管理功能,能够快速的帮我们进行分布式系统元数据的管理和推选主节点等等功能。
Kafka中涉及到了多处的选举机制,很容易混淆,主要有以下三个方面:
- Broker选举(多元宇宙"主宇宙"选举)
- Replica选举(三峡大坝"主池子"选举)
- 消费者leader选举
这里我们主要讲解broker选举和分区副本选举,因为这两个选举直接关系到我们Kafka集群的高可用。
4.1 主宇宙选举------Broker Leader选举
咱们上面说了,三峡大坝存在多个平行宇宙之间,其实这些平行宇宙也存在一个叫做"主宇宙"的概念来作为我们broker的leader。这个主宇宙的功能比较强大,可以创建、删除水的颜色(topic);增加、减少、池子(partition)的数量和主池选举;管理其它多元宇宙,包括宇宙级别的新增、关闭和故障处理;分区重分配(auto.leader.rebalance.enable=true)
多元宇宙管理者的选主模型我们可以去zookeeper相关的原理进行进一步的了解,目前而言我们只需要知道zookeeper那里有一个叫做controller的tiltle,我们所有的多元宇宙都需要去竞争这个title,竞争到了之后当前宇宙就成为了"主宇宙",掌握了其它平行宇宙的生杀大权。
每个broker都有唯一的brokerId,他们在启动后会去竞争注册zookeeper上的Controller结点,谁先抢到,谁就是broker leader。而其他broker会监听该结点事件,以便后续leader下线后触发重新选举。总的来说,作为主宇宙的broker主要会承担以下职责:监听Broker变化、监听Topic变化、监听Partition变化、获取和管理Broker、Topic、Partition的信息、管理Partition的主从信息。
4.2 主池选举------ISR选举机制
早期Kafka的分区副本选举实际上采用的和Broker选举一样的机制,直接通过Zookeeper的Watch机制来完成。这样实现比较简单,但是也存在一定的弊端,例如:分区和副本的数量过多的情况下,如果所有的副本都直接参与选举,此时一旦某个节点出现增减,就会造成大量的Watch事件被触发,导致Zookeeper的负担过重。新版的Kafka对于分区副本的选举更换了一种实现方式,不是所有的Replica都参与Leader的选举,而是由Controller的Broker进行统一指挥。
在Kafka集群中不是每个Replica都有资格参加选举,由于OSR集合中的消息与先前的Leader消息之间的差距过大,所以OSR会被排除在选举候选人之外。而且,由于Leader节点出现了宕机等故障,因此Leader节点也会被从ISR集合中移动到OSR集合中。所以只有在ISR集合中保持心跳同步的Replica才有资格参加选举。
知道了选举范围接下来的工作就是执行选举操作了,在分布式系统选举中,有非常多的选举协议比如说:ZAB、Raft等,这些分布式选举算法的思想归纳起来就是:先到先得,少数服从多数。但是在Kafka中没有使用这些算法,而是使用了一种自己实现的算法,其核心原理也很简单------默认让ISR中第一个Replica成为Leader。这个机制就是这么简单粗暴,就跟古代皇长子即位一样。
5. 装水的坑------Kafka的持久化机制
三峡大坝的持久化机制很简单,因为它本身就是个坑,水被存储在坑里面是很正常的事情。但是对于我们的Kafka来说,想要挖这么个坑却不那么简单。
首先,一般的消息队列,为了实现高性能,其数据都是先存储在内存中的,而内存这玩意儿啥啥都好,就是掉电数据全没了,而且贵得很。但是咱们Kafka就是独树一帜,就不采用内存的数据缓存方案,而是采用文件系统来作为存储和缓存消息的媒介。为了保障使用文件系统下的高性能,Kafka内部实现了一套数据的持久化机制,来实现消息的持久化功能。
另外,为什么不采用内存缓存机制主要是基于JVM内存进行消息缓存有2个缺点,首先,JVM中对象内存开销非常大,通常是需要存储数据的2倍甚至更高;其次,随着堆内存数据的增加,GC的速度会越来越慢。而实际上,磁盘的线性写入性能远远大于随机读写的性能,线性读写由操作系统进行了大量优化(read-ahead、write-behind),甚至比内存的读写更快,因此Kafka即使采用了文件日志系统的方案,其性能也很高。
5.1 池塘的实现------持久化文件
5.1.1 日志文件结构
在本文的世界观里面,同种颜色的水(topic)会被装进一类池塘(partition)里面,池塘在不同的三峡大坝中存在着不同的副本(replica),而同一个三峡大坝会有多个装这种颜色的池塘(partition-1、partition-2、...)。那么,在我们的磁盘系统中,每个池塘对应的就是一个offset.log的日志文件,属于当前这个partition的消息都会被直接追加到日志文件的末尾,而每条消息在文件中的位置被称为offset(偏移量).
如上图所示,由于生产者生产的消息会不断追加到log文件末尾,为防止log文件过大而导致数据定位效率低下,Kafka采用了分片和索引机制,将每个 partition 分为多个 segment,每个 segment 对应两个文件:".index" 索引文件和 ".log" 数据文件。这些文件位于同一文件下,该文件夹的命名规则为:topic 名-分区号。例如,first 这个 topic 有三分分区,则其对应的文件夹为 first-0,first-1,first-2。
shell
# ls /root/data/kafka/first-0
00000000000000009014.index
00000000000000009014.log
00000000000000009014.timeindex
00000000000000009014.snapshot
leader-epoch-checkpoint
index 和 log 文件以当前 segment 的第一条消息的 offset 命名。下图为 index 文件 和 log 文件的结构示意图。
index 和 log 文件以当前 segment 的第一条消息的 offset 命名。上图为 index 文件 和 log 文件的结构示意图。".index" 文件存储大量的索引信息,".log" 文件存储大量的数据,索引文件中的元数据指向对应数据文件中 message 的物理偏移量。
日志文件内容由"日志条目(log entries)"序列组成,每一个日志条目包含一个4字节整型数(值为N),其后跟N个字节的消息体。每条消息都有一个当前 Partition 下唯一的64字节的 offset,标识这条消息的起始位置。消息格式如下:
yaml
On-disk format of a message
offset : 8 bytes
message length : 4 bytes (value: 4 + 1 + 1 + 8(if magic value > 0) + 4 + K + 4 + V)
crc : 4 bytes
magic value : 1 byte
attributes : 1 byte
timestamp : 8 bytes (Only exists when magic value is greater than zero)
key length : 4 bytes
key : K bytes
value length : 4 bytes
value : V bytes
5.1.2 读数据
从broker中读取数据的时候,Kafka的Consumer通过给出消息的偏移量offset和最大块大小S来读取数据,返回一个缓冲区为S大小的消息迭代器。读取指定偏移量的数据时,需要首先找到存储数据的segment,由全局偏移量计算segment中的偏移量然后开始读取。
5.1.3 写数据
日志文件支持顺序附加,始终追加到当前最后一个文件。一旦文件大小达到配置的阈值(log.segment.bytes = 1073741824字节),就会滚动到一个新文件中(每个文件被称为一个segment文件)。该日志具有两个配置参数:M,指定在操作系统将消息写入磁盘之前需要缓存的消息数量;S,指定在操作系统将消息写入磁盘之前需要缓存的时间间隔(以秒为单位)。在系统崩溃的情况下,最多可能会丢失M条消息或S秒的数据。
5.1.4 删数据
消息数据与segment文件一同被删除。Log manager支持可插拔的删除策略,用于选择符合删除条件的文件。当前的删除策略包括删除修改时间早于N天前的所有日志,或者保留最近的N GB数据。
为了确保在删除操作时不阻塞读取操作,采用了copy-on-write技术。在删除操作进行时,读取操作的二分查找实际上是在一个静态的快照副本上执行的。这意味着读取操作不受删除操作的影响,能够继续在静态副本上执行,确保系统的可用性和性能。
5.2 文件索引
Kafka集群系统还维护了一个索引文件,用于标识每个segment中包含的日志条目的offset范围。
有了索引文件,消费者可以从Kafka的任意可用偏移量位置开始读取消息,提供了更大的灵活性。索引文件本身也被划分为多个片段,因此在删除消息时,可以相应地删除索引片段,以保持索引的实时性。
值得注意的是,Kafka并未维护索引的校验和。在索引损坏的情况下,Kafka通过重新读取消息来重新生成索引,确保数据的完整性和可用性。这种处理方式减少了对校验和的计算负担,并提供了一种容错机制,以应对潜在的索引文件损坏问题。
6. 总结
本文主要通过类比MQ和水库以一个比较新奇的角度阐述了Kafka是如何保障可用的,实际上高可用主要分为3个部分,即:数据副本机制,ISR选举机制和持久化机制。Kafka的副本机制其实是比较复杂的,一开始我根本不太理解为什么一个topic会分成多个partition,而且partition之间又会存在副本这样的机制。但是通过类比多元宇宙的概念,开始理解了这样实现的原因,确实可以在很大程度上保障消息队列的高可用和高性能。