基于raft算法的分布式KV存储服务

m0_65107844 2021-12-25 15:43:52

作者:283

 

1、项目概述

随着互联网业务量的不断激增,单机数据库无论是从存储空间还是访问速度上,都已经远达不到目前业务的需求,目前业界的主流方案是对数据库进行分库分表,并由此产生了大量中间件。但是这种方案对于业务的侵入性最高,存在兼容性问题。况且由业务方来实现分布式事务处理和容灾,并不是非常好的选择。在这种背景下,分布式数据库的方案是一种更优秀的解决方案,某种程度上也是一种未来的趋势。基于上述背景,选择了基于强一致性共识算法raft的非关系型K/V键值对存储服务作为工程实践的课题。

这个课题一共由两个模块组成,一是raft模块的实现,二是K/V存储模块的实现,其中Raft模块实现也是项目的核心部分。


2、用例建模


raft集群中共包含三种角色:

  • leader:负责日志的同步管理,处理来自客户端的请求,与follower保持心跳通信;
  • candidate:负责选举投票,集群启动或是leader宕机时,状态follower的节点转换为candidate并发起选举,选举获胜后(获得超过半数节点的投票)转换为leader;
  • follower:响应leader的日志同步请求,响应candidate的投票请求,并把客户端发给follower的请求转发给leader。

 

 

 

 

 

kv模块则包含两种角色:

  • kvserver:kv服务模块维护一个简易版的K/V键值对数据库,key和value的值均是字符串,并提供Put(key, value)、 Append(key, arg)和Get(key)三种方法;
  • kvclient:kv客户端就是使用KV存储服务的用户,可以调用服务模块提供的方法。

 

 

3、业务领域建模

业务领域建模步骤:

1. 收集应用业务领域的信息。聚焦在功能需求层面,也考虑其他类型的需求和资料;
2. 头脑风暴。列出重要的应用业务领域概念,给出这些概念的属性,以及这些概念之间的关系;
3. 给这些应用业务领域概念分类。分别列出哪些是类、哪些属性和属性值、以及列出类之间的继承关系、聚合关系和关联关系;
4. 将结果用 UML 类图画出来。

 

对于raft模块,功能主要有三部分构成,分别是领导选举、日志复制和安全性要求。


3.1、领导选举

Raft算法把时间轴划分为不同任期 Term。每个任期 Term 都有自己的编号 TermId,该编号全局唯一且单调递增

  1. 根据Raft协议,一个Raft集群在刚启动时,所有节点都处于follower状态,初始 Term(任期)为 0。同时启动选举定时器,每个节点的选举定时器超时时间都在 100~500 毫秒之间且并不一致(避免同时发起选举)。
  2. 没有 Leader,Followers 无法与 Leader 保持心跳(Heart Beat),节点启动后在一个选举定时器周期内未收到心跳和投票请求,则状态转为候选者 Candidate 状态,且 Term 自增,并向集群中所有节点发送投票请求并且重置选举定时器。
  3. 节点收到投票请求后会根据以下情况决定是否接受投票请求(每个 follower 刚成为 Candidate 的时候会将票投给自己):请求节点的 Term 大于自己的 Term,且自己尚未投票给其它节点,则接受请求,把票投给它;请求节点的 Term 小于自己的 Term,且自己尚未投票,则拒绝请求,将票投给自己。其余情况的投票按照先到先得的顺序。
  4. 一轮选举过后,正常情况下,会有一个 Candidate 收到超过半数节点(N/2 + 1)的投票,它将胜出并升级为 Leader。然后定时发送心跳给其它的节点,其它节点会转为 Follower 并与 Leader 保持同步,到此,本轮选举结束。

 


3.2、日志复制

  1. Leader 在收到客户端请求后,会将它作为日志条目(Entry)写入本地日志中。需要注意的是,此时该 Entry 的状态是未提交(Uncommitted),Leader并不会更新本地数据。
  2. Leader 与 Followers 之间保持着心跳联系,随心跳 Leader 将追加的 Entry(AppendEntries)并行地发送给其它的 Follower,并让它们复制这条日志条目,这一过程称为复制(Replicate)。
  3. Followers 接收到 Leader 发来的复制请求后,有两种可能的回应:写入本地日志中,返回 Success;一致性检查失败,拒绝写入,返回 False,原因和解决办法上面已做了详细说明。
  4. 完成前三个阶段后,Leader会向客户端回应 OK,表示写操作成功。Leader 回应客户端后,将随着下一个心跳通知 Followers,Followers 收到通知后也会将 Entry 标记为提交状态。至此,Raft 集群超过半数节点已经达到一致状态,可以确保强一致性。


3.3、安全性要求

  1. Election Safety 选举安全性:避免脑裂问题
  2. Leader Append-Only 日志只能由 leader 添加修改
  3. Log Matching 日志匹配特性
  4. Leader Completeness 选举完备性:leader 必须具备最新提交日志

 

3.4、K/V模块

对于K/V存储模块,功能主要是存储服务提供的Put(key, value)、 Append(key, arg)和Get(key)方法,Put(key, value)替换数据库中特定键的值,Append(key, arg) 将 arg 附加到键的值,而Get(key)获取键的当前值。

 

3.5、UML类图

 

 

 

4、基本架构

本项目中提供存储服务的集群模块采用了简单的分层模式,具体如下图所示:

其中图中的数字表示不同模块相互通信、进行数据交互的方式

  1. 客户端可以访问集群中的任一节点,当访问到 leader节点时,可以进行正常读写操作;当访问到 follower节点时,会返回 leader节点的信息,并通过返回的信息访问 leader节点;
  2. 状态机读取存储模块的快照(snapshot)从而决定返回哪种状态的存储信息;
  3. 状态机启动raft模块,并通过获取raft模块的状态来决定需要返回的信息;
  4. raft模块调用存储模块进行持久化存储,并在存储模块保存自身信息;
  5. 各个节点的 raft模块相互间可以通过 RPC的方式进行通信,其中 leader模块需要用 follower模块进行心跳通信。

 

5、重要API接口

func (ps *Persister) SaveRaftState(state []byte)

保存持久化raft节点状态的函数

func (ps *Persister) ReadRaftState()

读取持久化raft节点状态的函数

func (ps *Persister) SaveStateAndSnapshot()

存储快照点的函数

func (ps *Persister) ReadSnapshot()

读取快照点的函数

func (rf *Raft) GetState()

获取当前任期节点状态的函数

func (rf *Raft) AppendEntries()

处理心跳或者日志的rpc通信函数

func (rf *Raft) sendAppendEntries()

发送心跳或者日志的rpc通信函数

func (rf *Raft) RequestVote

获取投票结果的rpc通信函数

func (rf *Raft) sendRequestVote

向其他结点发起投票的rpc通信函数

func (rf *Raft) PutAppend()

KV存储的写操作(含有插入、修改等)

func (rf *raft) Get

KV存储的读操作

 

6、总结

 

1、通过进行需求分析和设计这两方面的工作,认识到需求分析和设计在软件开发中是至关重要的。一方面,由于它们是软件开发的前期工作,后续的编码和测试等等其他流程都要已它们为依托,如果没有做好,出现的问题会在后续被逐级放大,从而产生更多更严重的问题。另一方面,如果前期把这两项功能工作做好,后续的编码工作思路会非常清晰,按部就班把需求和模块翻译成代码即可,不会出现开发到一半进行卡壳的现象;

2、考虑到各大板块的互联网业务量逐渐达到瓶颈的现实情况,未来互联网市场必然是增量市场向存量市场转换的过程。某种程度上,这也是基础设施及基础软件发力的好机会,无论是数据库领域的各个方向,还是操作系统、编译器等,都是值得深究的方向。

...全文
803 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧