Raft分布式系统构建方法论

前文讲述了Raft算法的核心,本文提供将Raft算法应用于分布式系统的宏观指导,将不再赘述算法细节而是专注于用户层面和应用层面,描述一个Raft的分布式系统需要进行哪些工作。

组件

一个Raft节点,需要包含的组件有

  • Config 配置
  • Storage 日志存储器
  • Logger 打印程序运行时日志
  • Raft 算法核心
  • Node 一个节点的代称,是Raft的上层包装

提议和消息

我们把向Raft集群发起用户自定义操作的行为称为提议(Propose),而一个提议称为(Proposal)。Raft内部沟通的信息称为消息(Message)。

对于一个Node,如果收到的是一个Message,则直接进入Raft的状态机中;如果向Leader发起一个Proposal,则需要先包裹成Message,然后再进入状态机。到了这一步再称为提议(Proposal)可能就不恰当了,我们可以称一个提议的数据实体为Entry。

快照

快照即把当前集群的状态保存下来。事实上,快照并不属于Raft算法的一部分,是由用户自行维护的。别以为很复杂,其实总共需要记录的数据就两个,term和commit index (applied index)。主要是为了再启动时能够回到关闭时的状态。

轮询

需要一个轮询时刻监听来自客户端的Proposal或Message。轮询需要做的事情大概如下:

  1. 接收到一个Proposal或Message,接收后就会进入状态机中处理。
  2. 更新时钟滴答,以便于适时发送心跳。
  3. 进入Ready阶段,发送Message,检查并处理提交后的提议。

以上操作需要顺序执行,且在单线程内完成。

而接收Proposal或Message的具体做法可以通过跨线程管道来处理,即CSP并发模型。这种多生产者,单消费者的模式在这种轮询场景下特别适用。

Ready

当我们把Message投入状态机后,经过状态机的一系列判断和转换,某些操作会产生一个新的Message,并存入内部日志中。要注意的是,Raft算法不应该包含网络传输,使用怎样的网络传输方式应该由外部决定,即用户需要自己从内部日志中获取该Message,然后再发送出去。

Raft会把需要发送的Message,和需要处理的Entry都包裹到一个名为Ready的结构中,它可以包含零个,一个或多个Message和Entry。在Ready阶段,主要对该结构进行处理。

在处理一个Ready时,你可能需要做以下几件事情:

  1. 持久化Entry。当本节点拿到新Entry时,需要手动持久化该Entry。注意是持久化Entry,而不仅是你所提议的数据,因为Entry还包含了term,index等信息。

  2. 保存快照。快照是对当前内存数据的一次集中存储,方便宕机后快速还原。

  3. 发送Message。根据状态机创建的Message中的目标节点ID,向其发送该Message。

  4. 处理提交后的Config Entry。提交后的Entry会放到缓存区中,在这里你需要尝试获取,如果有就进行处理。Config Entry是用于配置集群的指令,比如新增一个节点或删除一个节点。根据算法,必须在提议提交后才能正式加入新节点或删除节点,对于类似行为必须在此处进行操作。

  5. 处理提交后的Normal Entry。Normal Entry就是用户发起的提议。需要在提议提交后进行一些回调,比如,向客户端响应读操作的结果等。提议和Message一定会被顺序执行和提交,所以你可以使用链表等结构存储提议的回调函数或其他信息。另一个办法是在提议数据中包含一个唯一ID,凭此做对应的处理。

  6. 检查是否存储快照。一般记录每隔提交个entry就存储一次快照。

  7. 前进(Advance)。这里表示该Ready已经处理完成的意思,标记一下,然后就可以获取下一个Ready,不标记则永远返回同一个Ready。

滴答与心跳

Raft结构体只负责算法的实现,即定义了每隔多少个滴答(Tick)就发送心跳的行为,而程序运行时间和一个滴答的时间间隔则需要外部维护。比如一个滴答是100ms,当发现经过了100ms,就需要让Raft内部的滴答数加一,当滴答数到达3(假设),Raft就自动发送心跳。

运行

讨论Raft集群运行过程中需要处理的问题。

配置

主要配置一些不会变动的信息,启动一个Raft节点主要需要配置以下信息:

  1. 节点ID
  2. 心跳的滴答数
  3. 重新选举的滴答数(没有收到心跳)
  4. 读操作的模式
  5. 是否开启预投票(Pre vote)

存储器

存储器是Raft节点加载和存储信息和数据的工具。使得Raft节点在启动能跟上集群的最新状态,以及重新启动时迅速回到上次关闭时的状态。

储存器需要存储和提供以下信息:

  1. Raft状态,包括commit index, leader, term, 成员ID等信息。
  2. 所有Entry。
  3. 快照(Snapshot)。

启动

当启动集群时,有一些需要注意的问题。

  1. 不要同时启动所有节点,而是先选定一个节点启动一段时间,至少该子节点自己成为了Leader,然后再启动其他节点。如果节点启动时间太近会导致难以选举出leader,因为大部分节点的term和index可能相同,大家都争着当候选人,但又无法拉开差距,进而不断发起选举。
  2. 时间滴答的时间间隔不能太小,如果太小会导致还没来得及响应投票选举出leader就发起了新一轮选举。

对于多节点启动,配置方式大概分为两种,一种是一开始就在成员配置表中写好所有节点,然后所有节点启动时都读取该配置,即静态配置;第二种方法是先启动一个节点,再启动其他节点,其他节点并没有成员配置表,而是走“新增节点”流程,即运行时配置。

新增节点

新增节点的过程大概如下:

  1. 新增节点向leader发送一个请求新增节点的指令,需要附带上自己的ID和RPC端口。
  2. leader收到指令后通过网络库提取出该节点的host地址(IP地址),和它发送过来的RPC端口号可以组成该节点程序的具体地址。
  3. leader创建一个Conf Message,填入新节点的ID和地址,然后投入状态机。
  4. 当多数节点收到该Message后就会被提交,提交后所有成员将该节点加入成员记录中。

再启动

再次启动时需要读取本地存储中的快照,即term和commit index,以便于直接跳过已经提交的日志。

删除节点

删除节点过程则简单很多。向leader发起删除指定节点的提议,然后leader将该节点从记录中删除即可。

与对外服务沟通

一般集群服务都会运行一个对外服务程序与外部沟通,可以是TCP Server或HTTP Server。该服务要与Raft服务相互沟通,则可以使对外服务作为生产者,往管道中投入一个Proposal,使其进入Raft的轮询中。并构建一个反向的管道,在回调中向反向管道输入结果,而对外服务则接收结果。

读操作可以向任何节点请求,而写操作只能向Leader请求。一般,所有成员都运行同一份代码,收到提议的节点会通过ID标识并缓存提议和回调,虽然Entry提交后所有节点都会做出同样的响应操作,但只有具有该ID缓存的那个节点才会成功向客户端响应。

框架化

上面讲到的点很多都是通用的,不同的仅仅是业务逻辑。我们可以提炼出一个框架来处理。事实上,框架化是一个很个性化的事情,不同人的思路和实现方式都可能不同,下面仅提供一种思路。

首先,业务逻辑和消息队列交给用户自己维护,抽象为RaftService,而Raft的轮询和算法抽象为RaftServer。我们的目标是把RaftService传入RaftServer中即可完成Raft集群的一系列工作,同时我们也可以把RaftService传入对外服务中,起到沟通作用。

RaftService实现定义一系列接口,具体实现交给用户。我们可以提炼出的接口有:

  1. recv() -> Message 从管道(或队列)获取一个Message。
  2. send(Message) 向管道(或队列)投入一个Message。注意,该Message应该是框架定义的对象,而不是Raft里面的Message,因为可能还要包含其他操作。
  3. update_context(Context) 更新上下文(Context),上下文对象用于储存集群状态信息,该接口被框架定时调用并传入新的Context。
  4. context() -> Context 用户获取可以通过该接口获得集群状态信息。
  5. propose(bytes, callback) -> Something 向Raft发起一个提议,用户自行将需要提交的数据包装成Message,并放入管道中。callback是指提交之后的回调函数,由用户自行管理。一般有一个返回值,告诉上游是否提议成功,比如有些提议只能由leader发起。
  6. propose_conf_change(ConfChange) -> Something 用于发布配置操作。
  7. on_committed(bytes) 提交后的处理,Raft集群提交一个Entry后调用。

其中提议传入bytes只是为了通用,如果你使用的语言有更强大的泛型系统,可以使用泛型取代。

实现

根据上面的理论,我用Rust实现了一个Raft服务框架,详见github

Kafka结构分析 Raft 简述

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×