ToyDB
简介
toydb
是Erik Grinaker
为学习rust
语言而开发的分布式sql数据库,支持分布式事务模型;
架构
toydb
主要由3部分组成:
sqlengine
: 负责sql语句的解析、执行计划;raftengine
: 负责存储层的数据副本同步;storage
: 负责提供kv及mvcc存储;
SQL层
SQL层主要负责将输入的sql语句字符串转化为执行计划,并通过raft交给各个副本的mvcc存储引擎执行;
SQL主要分为两个阶段:
词法分析:sql语句------->token---->AST;
生成执行计划:AST----->planner----->优化----->执行;
SQL语句 --> 词法分析 ----> 语法分析--->生成执行计划--->
词法分析(Lexer)
Lexer 也称为分词,从左向右扫描SQL,将其分割成一个个的toke(词元),在将token组装为AST(Abstract Tree);
Lexer的实现一般都是构造DFA(确定性有限状态自动机)来实现的。
状态转移图如下,这是一个能够识别标识符,数字和一般运算符的词法解析器。
语法分析(Parser)
Parser阶段有两种类型方法来实现:
一种是自顶向下分析法,
另一种是自底向上分析法,
简单介绍一下两种类型分析法的处理思路。
执行计划
|
|
Sql Engine
Parser(解释器)
Planner
Executor
Storage(存储)
Memory
MVCC
Raft Engine
toydb
通过raft来实现各节点间数据的一致性,其自带的raft模块由rust语言提供的一个简单的实现。
Raft内部有2个状态机:
复制状态机(): raft各节点间的日志复制,保证各个副本日志的顺序及一致;
指令状态机(
State
): 依次执行日志中的指令,驱动内部业务系统状态的改变;
由于复制状态机的Raft协议可以保证日志序列的顺序且一致性,所以由日志驱动的不同副本的指令状态机将拥有相同的输入指令序列,在初始状态相同的情况下,指令状态机将会得到相同的输出,以此就保证了各个副本外部最终状态的一致性;
复制状态机是raft协议的核心;
指令状态机由raft日志驱动来改变外界状态,是raft协议和外界交互的接口;
EvenLoop
Raft
的主驱动是EvenLoop
。节点启动时,会开启一个evenloop
后台异步任务,持续监听tick
, tcp_in_tx
, client_rx
, node_rx
这4个事件源上的消息Msg
,以此来驱动整个状态机的运行:
tick
事件由定时器产生,转入相应rolenode的tick
处理;tcp_in_tx
事件由其他节点peer产生,交由raft
状态机step
处理;node_rx
事件由节点内部产生,需根据事件消息的接收对象(to
)分别处理;- 发往副本(
to
为Address::Peer
,Address::Peers
)的消息,放入tcp_tx
交由TcpSender
进行发送; - 发往
Client
(Address::Client
) 且事件类型为Event::ClientResponse
的消息, 根据id
从requests
表中找到该消息响应rxresponse_tx
,通过response_tx
将消息响应回复给Client
; - 其他消息为非法消息, 报错并退出;
- 发往副本(
client_rx
事件由客户端产生,处理如下:先为事件生成uuid作为唯一id;
以id为key, 将消息响应rx
request_rx
放入requests
哈希表中,该表用于后续消息响应时处理消息返回;生成一个
ClientRequest
类型的消息 ,交由Rolenode
的step
处理;
Evenloop
接收到消息后,通过tick()
, step()
来驱动复制状态机执行日志复制操作。
各节点收到ClientRequest
后处理流程Step()
:
Candidate
:- 将
ClientRequest
消息放入queued_reqs
队列中进行缓存,等待变为Leader
后,再依次处理;
- 将
Follower
:如果没有
Leader
, 则也将消息放入queued_reqs
中缓存;// queud_reqs 后续处理?如果有
Leader
, 则将消息(id, from)放入proxied_reqs
中记录下来,然后转发到Leader
,由Leader
处理;
Leader
:Request::Query
消息:通过
state_tx
,向状态机发送Instruction::Query
指令,状态机将指令插入到queries
中;通过
state_tx
, 向状态机发送Instruction::Vote
指令, 统计;若存在副本,则向副本发送
Event::Heartbeat
消息,和Follower
;
Request::Mutate
消息:将消息记录到本地log中;
复制log到各个Follower;
接收到多数
Follower
的确认消息后,commit消息;给状态机发送
Instruction::Notify
指令;如果
peers
为空,提交;
Request::Status
消息:- 根据当前节点状态,生成
Instruction::Status
,通过state_tx
交由状态机执行;
- 根据当前节点状态,生成
指令状态机
指令状态机由Driver::drive()
驱动。每个Raft Node新建时,会开启一个driver后台任务,该任务从state_rx
接收指令,交由execute
处理,各指令处理流程如下:
Instruction::Abort
:Instruction::Apply
:Instruction::Notify
:如果指令
index
大于状态机已经applied_index
, 将(index, (address, id))插入到状态机notify
哈希表中;否则指令已被应用过,通过
node_tx
给Raft Node 发送ClientResponse
消息,由Raft Node将错误消息返回给客户端;
Instruction::Query
:Instruction::Status
:Instruction::Vote
:
源码
- Log
Log
(日志)是Raft状态机
|
|
- Driver
|
|
Raft角色
Leader
|
|