HTAP Overview: TiKV、TiFlash RAFT 同步流程
KvService
当用户从 mysql 客户端 insert 数据时,在 TiDB 通过 pd 定位到具体的每条数据对应的 region,然后将这个数据插入到对应的 region leader 中。TiKV 在接受到写指令后,再进行处理。本节专注于写流程本身,忽略 Tansactions 和 MVCC。TiKV 写指令的逻辑大致如下:
- RaftStoreRouter: KvService 封装了所有 TiKV 相关的 RPC 接口,在接受到写指令后,通过 RaftStoreRouter 将接收到的消息传递给后台线程池 BatchSystem 处理,在 RaftStore 内部再基于 Raft Log 的共识算法复制到其他 TiKV Followers。
- Transport:内部封装了 Raft Client,用于将 TiKV 的消息发送出去,比如 leader 和 follower之间的信息交互即基于 Transport 完成的。
Service
代码位于 src/server/service/kv.rs
, 表征 TikvService,实现 tikvpb.proto
中的 TiKV RPC Service。
1 | pub struct Service<T: RaftStoreRouter<E::Local> + 'static, E: Engine, L: LockManager, F: KvFormat> { |
在这个字段中 storage
字段比较重要,其实例化后是 ServerRaftStoreRouter,
ServerRaftStoreRouter
RaftStoreRouter: 负责将 received 的 Raft 消息转发给 raftstore 中对应的 Region,ServerRaftStoreRouter 则是 trait RaftStoreRouter 的实例化。
1 | /// A router that routes messages to the raftstore |
收到的请求如果是一个只读的请求,则会由 local_reader 处理;其它情况则是交给内层的 router 来处理。
- router: 由 EK, ER
- local_reader:
Server
Server 表征的是一个服务器,Service 表征的是提供的服务。
位于 src/server/server.rs
文件中的 Server 是我们本次介绍的 Service 层的主体。它封装了 TiKV 在网络上提供的服务和 Raft group 成员之间相互通信的逻辑。Server本身的代码比较简短,大部分代码都被分离到 RaftClient,Transport,SnapRunner 和几个 gRPC service 中。
1 | pub struct Server<T: RaftStoreRouter<E::Local> + 'static, S: StoreAddrResolver + 'static, E: Engine> |
ServerTransport
ServerTransport 则是 TiKV 实际运行时使用的 Transport 实现(Transporttrait 的定义在 raftstore 中),其内部包含一个 RaftClient 用于进行 RPC 通信。
1 | pub struct ServerTransport<T, S, E> |
发送消息时:
- ServerTransport 通过上面说到的 Resolver 将消息中的 store_id 解析为地址,并将解析的结果存入 raft_client.addrs 中;下次向同一个 store 发送消息时便不再需要再次解析。
- 再通过 RaftClient 进行 RPC 请求,将消息发送出去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16impl<T, S, E> Transport for ServerTransport<T, S, E>
where
T: RaftStoreRouter<E> + Unpin + 'static,
S: StoreAddrResolver + Unpin + 'static,
E: KvEngine,
{
fn send(&mut self, msg: RaftMessage) -> RaftStoreResult<()> {
match self.raft_client.send(msg) {
Ok(()) => Ok(()),
Err(reason) => Err(raftstore::Error::Transport(reason)),
}
}
}
// TODO RaftClient::send 方法
Node
Node 位于 src/server/node.rs
, 可以认为是将 raftstore 的复杂的创建、启动和停止逻辑进行封装的一层,其内部的 RaftBatchSystem 便是 raftstore 的核心。在启动 函数 Node:: start中,如果该节点是一个新建的节点,那么会进行 bootstrap 的过程,包括分配 store_id、分配第一个 Region 等操作。
1 | /// A wrapper for the raftstore which runs Multi-Raft. |
Node 并没有直接包含在 Server 之内,但是运行 raftstore 需要 Transport 向其它 TiKV 发送消息,而 Transport 包含在 Server 内。
所以我们可以看到,在 components/server/src/server.rs
文件的 init_servers 中(被 tikv-server 的 main 函数调用),启动过程中需要先创建 Server,然后创建并启动 Node 并把 Server 所创建的 Transport 传给 Node,最后再启动 Node。
1 | fn TiKVServer::init_servers<F: KvFormat>(&mut self) -> Arc<VersionTrack<ServerConfig>> { |
TiKV 包含多个服务,其中比较重要的是 TiKVService.
Raft log replication
在 TiKV 内部有两个 rocksdb 实例:
- raft-rocksdb:用于保存新增的 Raft Log 及最新的 Raft state;
- kv-rocksdb:用于保存TiKV 执行 command 之后产生的 kv 数据
kv-rocksdb 中有四个 Column Family:- CF_DEFAULT:用于保存 MVCC 数据,(user_key, start_ts) => value
- CF_WRITE :用于保存 Value 可见的版本控制,(key, commit_ts)=> write_info
- CF_LOCK:用于保存事务的锁信息,表示有事务正在写这个 key,key => lock_info
- CF_RAFT:用于保存 Raft Log 中 applied 的最新元信息 RaftLocalState、RaftApplyState
自然,TiFlash 没有 ColumnFamily,就需要通过别的方式保存这些数据。
写入数据,从生成 Raft Log 到被存储到 kv-rocksdb 的整个流程如下:
在 Raft Replication 过程中,snapshot 是由 Raft 状态机自动产生的,比如当 follower / learner 出现宕机或者网络分区、新加入 follower / learner 节点或者处于 backup 需求,leader 就需要生成 snapshot。
follower、learner 接收到 snapshot 时,处理的优先级最高,因为这个 snapshot 是 follower 处理后续的 proposals 的基准数据。
BatchSystem
Batch System 类似线程池,是 RaftStore 的核心执行组件, 其职责就是检测哪些状态机需要驱动,然后调用 PollHandler 去消费消息。消费消息会产生副作用,而这些副作用或要落盘,或要网络交互。PollHandler 在一个批次中可以处理多个 normal 状态机。
PollHandler
PollHandler 负责处理处理接收到的消息,消息的处理流程按照如下trait。
1 | pub trait PollHandler<N, C>: Send + 'static { |
在 raftstore 里,一共有两个 Batch System。分别是 RaftBatchSystem 和 ApplyBatchSystem,对应的 PollHandler也分为 RaftPoller、ApplyPoller。
- RaftBatchSystem 用于驱动 Raft 状态机,包括日志的分发、落盘、状态跃迁等,当日志 committed 并持久化到 raft-rocksdb 后,会发往 ApplyBatchSystem 进行处理;
- ApplyBatchSystem 将日志解析并应用到状态机,数据持久化到 kv-rocksdb,执行回调函数回复客户端。
所有的写操作都遵循着这个流程,简要流程如下:
TiFlash
Overview
Arch
TiKV 是用 Rust 实现的,而 TiFlash 是用 C++ 实现的,TiFlash 和 TiFlash-Proxy(简称 Proxy)经常需要相互调用,这一机制基于 FFI (Foreign Function Interface)实现。
C++ 端和 Rust 端共享一个 ProxyFFI.h,任何新增的 FFI 调用只需要在该接口中定义一遍,调用 gen-proxy-ffi 模块即可生成 Rust 端的接口代码
TiFlash 和 TiFlash-Proxy 会各自将 FFI 函数封装入 Helper 对象中,然后再互相持有对方的 Helper 指针。
- RaftStoreProxyFFIHelper:是 Proxy 给 TiFlash 调用的句柄,它封装了 RaftStoreProxy 对象。TiFlash 通过该句柄可以进行 ReadIndex、解析 SST、获取 Region 相关信息以及 Encryption 等相关工作;
- EngineStoreServerHelper 是 TiFlash 给 Proxy 调用的句柄,Proxy 通过该句柄可以向 TiFlash 写入数据和 Snapshot、获取 TiFlash 的各种状态等
write path
TiFlash-Proxy 处理的是写流程,将TiFlash包装成TiKV,是将 TiKV 的 apply 流程中的 kv-rocksdb 替换为 TiFlash,由 TiFlash 来持久化保存kv数据。
TiFlash-Proxy 处理的写入主要分为普通的 KV write、Admin Command 以及 IngestSST。这些写入会被存放在内存中。
一个 region 的数据分为两个部分:
- committed:已写入列式引擎 storage,通过 tryFlushRegionCacheInStorage 函数来刷盘;
- uncommitted:尚未写入 storage 中的,由 RegionPersister 调用 persistRegion 函数持久化到 PageStorage中
所以一般 flush 的整体流程:
- tryFlushRegion 将 committed 的数据写入 storage 的 cache
- tryFlushRegionCacheInStorage 将 storage 的 cache 数据刷盘
- 将 uncommitted 的数据写入 PageStorage
流程图如下:
read path
读取操作,直接由 TiDB 通过 RPC 发送到 TiFlash 进行读取,不经过 TiFlash-Proxy。
Question
为什么说是 TiKV 是异步同步数据到 TiFlash 而不会影响 TiKV写入流程?
因为 TiFlash 作为 raft-learner ,非 voter ,即不具有投票权,leader 同步的 proposals,只需要原来 TiKV 集群的半数节点回应即可 commit,不需要等待 TiFlash 的回应,因此即便 TiFlash 节点出现问题,也不会拖慢原有TiKV集群。TiDB 是强一致性的,如果 TiKV 同步到 TiFlash 的流程比较慢,那么 TiFlash 是如何保证读取一致性的?
默认的 TiKV 是只有 leader 能提供读写能力,开启 Follower Read 后就能让 TiKV 集群中的 follower 节点也能分摊 leader 的读压力,Follower Read 的一致性是基于 ReadIndex 保证的- 先读取当前 Leader 的最新 commit_index,然后
- 等待 follower 自己的状态机的 applied_index >= commit_index
StaleRead
这两步,即可满足线性一致性。
1 | fn ready_to_handle_unsafe_replica_read(&self, read_index: u64) -> bool { |
TiFlash 可以视为特殊的 TiKV,对于 TiFlash 的读一致性保证基于 Learner Read 实现,原理与 TiKV follower 类似。
1 | bool RegionMeta::doCheckIndex(UInt64 index) const { |