Kafka Raft

背景

Kafka2.8 之后,移除了Zookeeper,而使用了自己研发的Kafka Raft。

为什么移除Zookeeper?

原来Zookeeper在Kafka中承担了Controller选举、Broker注册、TopicPartition注册和选举、Consumer/Producer元数据管理和负载均衡等。

即承担了各种元数据的保存和各种选举。

而Zookeeper并“不快”,集群规模大了之后,很容易成为集群的性能瓶颈。

Kafka作为一个消息中间件,还依赖额外的一个协调系统,而不能实现自我管理,说不过去~无法做到开箱即用,还得要求使用者掌握Zookeeper的调优知识。

到了2.8版本,Kafka移除了Zookeeper,使用自己的Kafka Raft(KRaft),这名字,一眼就能看出来是基于Raft算法实现的。

Raft算法

Raft是一种共识算法,即在分布式系统中,所有节点对同一份数据的认知能够达成一致。

算法主要做两件事情:

分解问题,将复杂的分布式共识问题拆分为: 领导选举、日志复制、安全性。

压缩状态空间,相对于Paxos算法而言施加了更合理的限制,减少因为系统状态过多而产生的不确定性。

复制状态机

在共识算法中,所有服务器节点都会包含一个有限状态自动机,名为复制状态机(replicated state machine)。

每个节点都维护着一个复制日志(replicated logs)的队列,复制状态机会按序输入并执行该队列中的请求,执行状态转换并输出结果。

可见,如果能保证各个节点中日志的一致性,那么所有节点状态机的状态转换和输出也就都一致。

 

基本流程为:

  1. 某个节点的共识模块(包含共识算法的实现)从客户端接收请求。
  2. 该共识模块将请求写入自身的日志队列,并与其他节点的共识模块交流,保证每个节点日志都相同。
  3. 复制状态机处理日志中的请求。
  4. 将输出结果返回给客户端。

领导选举

节点状态与转移规则

在Raft集群中,任意节点同一时刻只能处于领导者(leader)、跟随者(follower)、候选者(candidate)三种状态之一。下图示出节点状态的转移规则。

 

所有节点,一开始角色都是follower,当发现没有leader的时候,就会把自己的角色切换为candidate,发起选举。

得到半数节点投票的,会成为leader。

如果follower或者当前leader发现变更了leader,就会主动退出follow状态。

当leader故障或断开连接,follower就会重新切换为candidate,发起新一轮的选举。

只有leader节点能管理日志的复制,即leader接受客户端的请求,再复制到follower节点。

领导任期

 

上图中,蓝色表示选举时间段,绿色表示选举出的领导者在位的时间段,这两者合起来即称作一个任期(term),其计数值是自增的。

任期的值就可以在逻辑上充当时间戳,每个节点都会保存一份自己所见的最新任期值,称为currentTerm。

选举流程

如果一个或多个follower节点在选举超时(election timeout)内没有收到leader节点的心跳(一个名为AppendEntries的RPC消息,本意是做日志复制用途,但此时不携带日志数据),就会发起选举流程:

  1. 增加本地的currentTerm值;
  2. 将自己切换到候选状态;
  3. 给自己投一票;
  4. 给其他节点发送名为RequestVote的RPC消息,请求投票;
  5. 等待其他节点的消息。

一个任期内,每个节点只能投一票,并且先到先得,也就是会把票投给RequestVote消息第一个到达的那个节点。

选举可能有三种结果:

  • 收到多数节点的投票,赢得选举,成为领导节点;
  • 收到其他当选节点发来的AppendEntries消息,转换回跟随节点;
  • 选举超时后没收到多数票,也没有其他节点当选,就保持候选状态重新选举。

获得超过多数选票的节点当选leader节点,需要立即给其他节点发送AppendEntries消息,告知其他节点,已经选举完毕,避免触发新的一轮选举。

日志复制

日志格式

领导节点选举出来后,集群就可以开始处理客户端请求了。前面已经说过,每个节点都维护着一个复制日志的队列,它们的格式如下图所示:

 

可见,日志由一个个按序排列的entry组成。每个entry内包含有请求的数据,还有该entry产生时的领导任期值。在论文中,每个节点上的日志队列用一个数组log[]表示。

复制流程

客户端发来请求时,领导节点首先将其加入自己的日志队列,再并行地发送AppendEntries RPC消息给所有跟随节点。

领导节点收到来自多数跟随者的回复之后,就认为该请求可以提交了(见图中的commited entries)。

然后,领导节点将请求应用(apply)到复制状态机,并通知跟随节点也这样做。这两步做完后,就不会再回滚。

这种从提交到应用的方式与最基础的一致性协议——两阶段提交(2PC)有些相似,但Raft只需要多数节点的确认,并不需要全部节点都可用。

只保证最终一致性!

安全性

安全性是施加在领导选举、日志复制两个解决方案上的约束,用于保证在异常情况下Raft算法仍然有效,不能破坏一致性,也不能返回错误的结果。所有分布式算法都应保障安全性,在其基础上再保证活性(liveness)。

Raft协议的安全性保障有5种,分别是:选举安全性(election safety)、领导者只追加(leader append-only)、日志匹配(log matching)、领导者完全性(leader completeness)、状态机安全性(state machine safety) 。

选举安全性

选举安全性是指每个任期内只允许选出最多一个领导。如果集群中有多于一个领导,就发生了脑裂(split brain)。根据“领导选举”一节中的描述,Raft能够保证选举安全,因为:

  • 在同一任期内,每个节点只能投一票;
  • 只有获得多数票的节点才能成为领导。

领导者只追加

在讲解日志复制时,我们可以明显地看出,客户端发出的请求都是插入领导者日志队列的尾部,没有修改或删除的操作。

这样可以使领导者的行为尽量简单化,使之没有任何不确定的行为,同时也作为下一节要说的日志匹配的基础。

日志匹配

如果两个节点的日志队列中,两个entry具有相同的下标和任期值,那么:

  • 它们携带的客户端请求相同;
  • 它们之前的所有entry也都相同。

第一点自然由上一节的“领导者只追加”特性来保证,而第二点则由AppendEntries RPC消息的一个简单机制来保证:每条AppendEntries都会包含最新entry之前那个entry的下标与任期值,如果跟随节点在对应下标找不到对应任期的日志,就会拒绝接受并告知领导节点。

有了日志匹配特性,就可以解决日志复制中那个遗留问题了。假设由于节点崩溃,跟随节点的日志出现了多种异常情况,如下图:

 

图中是6种可能的情况。比如a和b是丢失了entry,c和d是有多余的未提交entry,e和f则是既有丢失又有冗余。

这时领导节点就会找到两个日志队列中最近一条匹配的日志点,将该点之后跟随节点的所有日志都删除,然后将自己的这部分日志复制给它。

例如对于上图中的情况e来说,最近一条匹配的日志下标为5,那么5之后的所有entry都会被删除,被替换成领导者的日志。

领导者完全性

领导者完全性是指,如果有一条日志在某个任期被提交了,那么它一定会出现在所有任期更大的领导者日志里。这也是由两点来决定的:

  • 日志提交的定义:该条日志被成功复制到多数节点才算是提交;
  • 选举投票的附加限制:只有当节点A的日志比节点B的日志更新时,B才可能会投票给A。也就是说,如果一个候选节点没有包含所有被提交的日志,那么它一定不会被选举为领导。

根据这两个描述,每次选举出的领导节点一定包含有最新的日志,因此只存在跟随节点从领导节点更新日志的情况,而不会反过来,这也使得一致性逻辑更加简化,并且为下面的状态机安全性提供保证。

状态机安全性

状态机安全性是说,如果一个节点已经向其复制状态机应用了一条日志中的请求,那么对于其他节点的同一下标的日志,不能应用不同的请求。这句话就很拗口了,因此我们来看一种意外的情况。

  1. 在时刻a,节点S1是领导者,第2个任期的日志只复制给了S2就崩溃了。
  2. 在时刻b,S5被选举为领导者,第3个任期的日志还没来得及复制,也崩溃了。
  3. 在时刻c,S1又被选举为领导者,继续复制日志,将任期2的日志给了S3。此时该日志复制给了多数节点,但还未提交。
  4. 在时刻d,S1又崩溃,并且S5重新被选举为领导者,将任期3的日志复制给S1~S4。

这里就有问题了,在时刻c的日志与新领导者的日志发生了冲突,此时状态机是不安全的。

抄的,没懂为什么算是发生冲突,不是没提交么,没提交,怎么就算冲突了。这算冲突,时刻b就不冲突么?难道大多数同步了,就认为是会提交的,不应该丢?

为了解决该问题,Raft不允许leader在当选后提交“前任”的日志,而是通过日志匹配原则,在处理“现任”日志时将之前的日志一同提交。

具体方法是:在领导者任期开始时,立刻提交一条空的日志

所以上图中时刻c的情况不会发生,而是像时刻e一样先提交任期4的日志,连带提交任期2的日志。就算此时S1再崩溃,S5也不会重新被选举了。

Kafka Raft的实现

Quorum节点状态机

在KRaft协议下,节点可以处于以下4种状态之一。

  • Candidate(候选者)——主动发起选举。
  • Leader(领导者)——在选举过程中获得多数票。
  • Follower(跟随者)——已经投票给Candidate,或者正在从Leader复制日志。
  • Observer(观察者)——没有投票权的Follower,与ZK中的Observer含义相同。

 

消息定义

经典Raft协议只定义了两种RPC消息,RequestVote(让其他节点给自己投票)和AppendEntries(leader节点给follower节点发送的心跳),并且都是以推的方式。

在KRaft协议下,以拉的交互模式(使用内部topic来实现元数据同步,topic的日志消费采用拉取的方式),定义的RPC消息有:

  • Vote,投票消息。
  • BeginQuorumEpoch,新Leader当选时发送,告知其他节点当前的Leader信息。
  • EndQuorumEpoch,当前Leader退位时发送,触发重新选举,用于graceful shutdown。
  • Fetch,复制Leader日志,由Follower/Observer发送。经典Raft协议中的AppendEntries消息是Leader将日志推给Follower,而KRaft协议中则是靠Fetch消息从Leader拉取日志。同时Fetch也可以作为Follower对Leader的活性探测。

为什么要采用拉取的方式?

  1. 可以将一致性检查的工作放在Leader节点,拉取的时候校验,Follower/Observer请求中携带当前日志的LEO。Leader会匹配对应的Leader节点任期,如果匹配,就返回要同步的数据;不匹配,则告知Follower/Observer截断日志。
  2. 可以更快速地启动一个全新的Follower,直接从offset 0开始复制日志,Leader不需要记录Follower同步到哪里。
  3. 方便淘汰过期的Follower,过期的Follower跟Leader同步数据,Leader从配置上知道是无效的Follower,就可以告知。
  4. 推送模式下,当Leader节点无法给Follower发送心跳的时候,才会主动退位;在拉取模式下,当Leader无法提交数据的时候,就可以退位了。

拉取方式的缺点:

  1. 处理僵尸Leader和Fetch的延迟比较大。多数Follower向Leader去Fetch日志超时后,才认为Leader挂了;定时Fetch日志,肯定没有直接Leader推来的快。

领导选举

当满足以下三个条件之一时,Quorum中的某个节点就会触发选举:

  • Follower向Leader发送Fetch请求后,在超时阈值quorum.fetch.timeout.ms之后仍然没有得到Fetch响应,表示Leader疑似失败。
  • 从当前Leader收到了EndQuorumEpoch请求,表示Leader已退位。
  • Candidate状态下,在超时阈值quorum.election.timeout.ms之后仍然没有收到多数票,也没有Candidate赢得选举,表示此次选举作废,重新进行选举。

元数据日志复制

与维护Consumer offset的方式类似,脱离ZK之后的Kafka集群将元数据视为日志,保存在一个内置的Topic中,且该Topic只有一个Partition。

为什么只有一个Partition?

Raft的日志队列,是需要有序的,先进先出。Kafka的日志,每个分区是一个逻辑上的文件,顺序写入,只有一个Partition的时候,所有消息都是顺序的。

 

元数据日志的消息格式与普通消息没有太大不同,但必须携带Leader的纪元值(即之前的Controller epoch):

Record => Offset | LeaderEpoch | ControlType | Key | Value | Timestamp

Follower以拉模式复制Leader日志,就相当于以Consumer角色消费元数据Topic,符合Kafka原生的语义。

KRaft里,怎么判断哪些元数据是已经提交的?

Kafka里,有一个概念HW高水位,所有Follower都同步了的日志下标,就是这个partition的HW,表示日志不会丢失。HW之前的日志,代表已提交。

HW-LEO之间的数据,则是未提交的数据。

 

状态机安全性保证

在安全性方面,KRaft与传统Raft的选举安全性、领导者只追加、日志匹配和领导者完全性保证都是几乎相同的。下面只简单看看状态机安全性是如何保证的,仍然举论文中的极端例子:

  1. 在时刻a,节点S1是Leader,epoch=2的日志只复制给了S2就崩溃了;
  2. 在时刻b,S5被选举为Leader,epoch=3的日志还没来得及复制,也崩溃了;
  3. 在时刻c,S1又被选举为Leader,继续复制日志,将epoch=2的日志给了S3。此时该日志复制给了多数节点,但还未提交;
  4. 在时刻d,S1又崩溃,并且S5重新被选举为领导者,将epoch=3的日志复制给S0~S4。

此时日志与新Leader S5的日志发生了冲突,如果按上图中d1的方式处理,消息2就会丢失。

传统Raft协议的处理方式是:在Leader任期开始时,立刻提交一条空的日志,所以上图中时刻c的情况不会发生,而是如同d2一样先提交epoch=4的日志,连带提交epoch=2的日志。

与传统Raft不同,KRaft附加了一个较强的约束:当新的Leader被选举出来,但还没有成功提交属于它的epoch的日志时,不会向前推进HW。

为什么要增加这么个约束?

个人认为,Kafka的Consumer,只能拉取到HW之前的消息,如果消息本同步了ISR集合中的Follower节点,HW就会增加。Consumer就会消费到,但是这时候消息可能还是未提交的,可能会被清除,例如d1~

加上这个约束,即使上图中时刻c的情况发生了,消息2也被视为没有成功提交(HW未改变),所以按照d1方式处理是安全的,因为数据并不会被Consumer读取到。

相关配置

quorum.voters,可以投票的broker的名单,{broker-id}@{broker-host):{broker-port}。

quorum.fetch.timeout.ms,Follower/Observer拉取日志的超时时间。

quorum.election.timeout.ms,选举的超时时间,规定时间内,没有节点获得多数选票,就需要发起新的投票。

quorum.election.backoff.max.ms,选举超时后,最多等待的毫秒数,然后重新发起投票。

quorum.request.timeout.ms,请求的超时时间。

quorum.retry.backoff.ms,请求超时之后的初始间隔时间,过了这个时间之后,再重试

quorum.retry.backoff.max.ms,请求超时后,最大的重试间隔时间

broker.id,节点id

参考文章

详解分布式共识(一致性)算法Raft

帅呆了!Kafka移除了Zookeeper!

脱离ZooKeeper依赖的Kafka Controller Quorum(KRaft)机制浅析

官网KIP-595

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值