前言
TIKV
的 BatchSystem
是实现 MultiRaft
的关键模块。在阅读本文章前,大家可以先参考一些官方博客:
TiKV 源码解析系列文章(二)raft-rs proposal 示例情景分析
TiKV 源码解析系列文章(十八)Raft Propose 的 Commit 和 Apply 情景分析
官方的文章基本已经把 RAFT
的使用和 RaftStore
原理讲的比较清晰了。
通过这些文章,大家应该比较了解 RaftStore
的大概流程。在此基础上,本系列文章将会进一步对其细节进行个人的分析。
同时个人认为这个文章对 BatchSystem
的架构图比较清晰明了。
下面我们将要详细说明这个模块各个组件的细节与作用。
BatchSystem
与邮政业务系统
对于之前接触过 RAFT
的同学来说,大家都比较了解整个 RAFT
几乎所有的流程都是以 Msg
来驱动的,可以参考:TiKV 源码解析系列文章(二)raft-rs proposal 示例情景分析
我们可以把整个 BatchSystem
当做一个全国范围的邮局业务系统,把 Msg
当做用户想要邮寄的信件。接下来我们大概描述一下邮局业务系统各个组件的作用。
由于特殊的原因,这个邮件系统做出了限制,同一个时间全国内发往一个省 (Region
) 的信件,一次只能送一个:即前一个信件 (Msg
) 没有处理完毕前,发往同一个省的信件只能排队等待。不同的省可以并发邮寄。
FSM
我们可以简单的把 Msg
当做一个信件,而把 FSM
当做信封。和现实世界有些不同,并不是每个信件都有一个信封供它使用。而是每个省只有一个信封可用,有几个省就有几个信封。
FSM
信封上面提前标明了目标省地址 (ReginID
),不断重复使用。
其结构是这样的:
pub struct Fsm<EK: KvEngine, ER: RaftEngine> {
...
receiver: Receiver<Msg>,
...
}
结构非常简单,receiver
仅仅是一个 channel
,用于存放 Msg
。
特别的,有一种信件 (ControlMsg
) 比较特殊,它有专门的信封 (ControlFsm
) 对其提供邮寄服务。
Mailbox
有了信件 (Msg
) 与信封 (FSM
),我们还需要邮箱 (Mailbox
)。和信封类似,也是每个省(Region
) 仅有一个邮箱 (Mailbox
) 可用。
每当邮箱 (Mailbox
) 收到一个信件 (Msg
),那么它负责找到对应省的信封 (FSM
),把信件 (Msg
) 放到信封 (FSM
) 里面去,再将信封 (FSM
) 投递到邮局的运转中心去。
当然我们前面也说过,可能对应省的信封 (FSM
) 正在运送其他信件 (Msg
),这个时候需要告知邮寄失败。
其结构为:
pub struct BasicMailbox<Owner: Fsm> {
sender: mpsc::LooseBoundedSender<Owner::Message>,
state: Arc<FsmState<Owner>>,
}
其 sender
对应上面 FSM
的 receiver
,负责把 Msg
传递给 FSM
state
可以看做 FSM
对象和 FSM
当前的状态(即是否正在运送其他 Msg
),Msg
发送给 FSM
后,Mailbox
还需要将 FSM
发送给 运转中心。
特殊信件 (ControlMsg
) 拥有特殊信封 (ControlFsm
),自然也有特殊的 Mailbox
对应。
Scheduler
前一个小节里面 Mailbox
所说的运转中心指的就是 Scheduler
。
Scheduler
的作用非常简单,它负责接收信封 FSM
,然后再投递给多个 快递车。我们前面说过,信封FSM
的数量和省 Regions
的数量是一对一的,但是为了节约成本,快递车的数量可能是少于省 Regions
的数量的。Scheduler
收到信封 FSM
后,就负责找到空闲的快递车,然后将 FSM
投递到快递车,让它送到对应的省地址去。
因此,其结果也很简单:
pub struct Scheduler<N: Fsm, C: Fsm> {
pub(crate) sender: Sender<FsmTypes<N, C>>,
...
}
无非就是一个用于发送 FSM
的 sender
,其实就是一个 channel
的发送端。快递车哪个空闲了,就通过 channel
的接收端获取 FSM
来进行处理。
特别地,对于特殊信件 (ControlMsg
) 特殊信封 (ControlFsm
),自然有特殊的 Scheduler
对应:ControlScheduler
Poller
上面 Scheduler
所说的 快递车 就是这个 Poller
,Poller
的数量是可配置的,如果系统负载量不大,快递车没有必要拥有那么多,平白造成空转系统消耗。
多个快递车 Poller
共享运转中心 Scheduler
channel
的任务屏幕 (receiver
)。只要 Scheduler
通过 sender
在任务屏幕 (receiver
) 展示了信件 FSM
运送任务,多个 Poller
就需要通过任务屏幕 (receiver
) 竞争获取。
同时 Poller
的 handle
成员变量需实现两个重要的接口:handle_normal
与 handle_control
。这两个接口用于识别信封上面的省地址 (RegionID
),进而向该省 (Region Raft
) 投递信件 (Msg
)
因此,其结构为:
pub struct Poller<N: Fsm, C: Fsm, Handler> {
...
pub fsm_receiver: Receiver<FsmTypes<N, C>>,
pub handler: PollHandler,
...
}
pub trait PollHandler<N, C>: Send + 'static {
/// This function is called when the control FSM is ready.
///
/// If `Some(len)` is returned, this function will not be called again until
/// there are more than `len` pending messages in `control` FSM.
///
/// If `None` is returned, this function will be called again with the same
/// FSM `control` in the next round, unless it is stopped.
fn handle_control(&mut self, control: &mut C) -> Option<usize>;
/// This function is called when some normal FSMs are ready.
fn handle_normal(&mut self, normal: &mut impl DerefMut<Target = N>) -> HandleResult;
}
}
Router
目前好像邮局业务系统的整个流程都比较清晰了,用户将信件 (Msg
) 投递给对应省 (Region
) 的邮箱 Mailbox
,Mailbox
将信件放入信封 FSM
,然后把信封发送给邮局运转中心 Scheduler
,运转中心 Scheduler
将 FSM
放到任务屏幕,多个快递车 (Poller
) 竞争任务,然后使用自己的 handle
对 FSM
进行处理,最后将信件 (Msg
) 成功运送到 Region
的 Raft
系统。
但是好像还有一个问题,用户手里只有信件,也知道想要投递到哪个省 Region
,但是每次都要去找对应 Region
的邮箱 (Mailbox
) 好像有点麻烦。
因此,组件 Router
专门负责路由 Mailbox
这一个环节,用户只需要将信件 (Msg
) 交给 Router
,Router
帮忙定位邮箱 (Mailbox
) 的位置,对邮箱 (Mailbox
) 进行投递操作。
其结构为:
pub struct Router<N: Fsm, C: Fsm, Ns, Cs> {
normals: Arc<DashMap<u64, BasicMailbox<N>>>,
pub(super) control_box: BasicMailbox<C>,
pub(crate) normal_scheduler: Ns,
pub(crate) control_scheduler: Cs,
...
}
pub fn try_send(
&self,
addr: u64,
msg: N::Message,
) -> Either<Result<(), TrySendError<N::Message>>, N::Message> {
let mut msg = Some(msg);
let res = self.check_do(addr, |mailbox| {
let m = msg.take().unwrap();
match mailbox.try_send(m, &self.normal_scheduler) {
Ok(()) => Some(Ok(())),
...
}
});
match res {
CheckDoResult::Valid(r) => Either::Left(r),
CheckDoResult::Invalid => Either::Left(Err(TrySendError::Disconnected(msg.unwrap()))),
CheckDoResult::NotExist => Either::Right(msg.unwrap()),
}
}
可以看到,Router
基本包含了 Mailbox
和 scheduler
组件。
关键函数是 try_send
,参数是 Msg
和 RegionID
,该函数负责传递 RegionID
调用 check_do
获取具体某个 mailbox
,然后向 mailbox
传递 Msg
和 scheduler
进行消息投递,完成整个 BatchSystem
对某个 Region
Raft
的消息传达。
除此以外,对于特殊的 ControlMsg
,Router
还有 send_control
接口。
BatchSystem
好了,讲到这里,BatchSystem
的整体架构已经比较清楚了:
Mailbox
和FSM
共用一套channel
的sender
和receiver
,用于Msg
的传递;scheduler
和Poller
共用一套channel
的sender
和receiver
,用于FSM
的传递Router
包含Mailbox
和scheduler
组件
由于 Router
已经包含了 Mailbox
和 scheduler
组件,Mailbox
包含了 FSM
组件,因此 BatchSystem
的结构只需要 Router
和 Poller
:
pub struct BatchSystem<N: Fsm, C: Fsm> {
router: BatchRouter<N, C>,
...
pool_state_builder: Option<PoolStateBuilder<N, C>>,
}
总结
TIKV
的 RaftStore
系统承担着 Multi Raft
模块多个 Region
Msg
的流通与交互,是保证分布式数据库的强力保障。本文为了让大家更加直观的理解 RaftStore
系统,采用了邮政系统来类比。但是实际上,RaftStore
系统不止本文所描述的如此简单,还涉及到 RaftLogEngine
、 RaftApplySystem
等等子功能。我们有机会会在接下来的系列文章继续详细展开描述。