存储架构

存储的选型

作为保存数据的系统,首先要决定的是数据的存储模型,也就是数据以什么样的形式保存下来。

Key-Value

TiKV的选择是Key-Value模型,并且提供有序遍历方法。简单来讲,可以将TiKV看做一个巨大的Map,其中KeyValue都是原始的Byte数组,在这个Map中,Key按照Byte数组总的原始二进制比特位比较顺序排列。大家这里需要对TiKV记住两点:

  • 这是一个巨大的Map,也就是存储的是Key-Value pair

  • 这个Map中的Key-Value pair按照Key的二进制顺序有序,也就是我们可以Seek到某一个Key的位置,然后不断的调用Next方法以递增的顺序获取比这个Key大的Key-Value

RocksDB

任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV也不例外。但是TiKV没有选择直接向磁盘上写数据,而是把数据保存在RocksDB中,具体的数据落地由RocksDB负责。这个选择的原因是开发一个单机存储引擎工作量很大,特别是要做一个高性能的单机引擎,需要做各种细致的优化,而RocksDB是一个非常优秀的开源的单机存储引擎,可以满足我们对单机引擎的各种要求,而且还有Facebook的团队在做持续的优化,这样我们只投入很少的精力,就能享受到一个十分强大且在不断进步的单机引擎。

Raft

接下来我们面临一件更难的事情:如何保证单机失效的情况下,数据不丢失,不出错?简单来说,我们需要想办法把数据复制到多台机器上,这样一台机器挂了,我们还有其他的机器上的副本;复杂来说,我们还需要这个复制方案是可靠、高效并且能处理副本失效的情况。

Raft是一个一致性协议,提供几个重要的功能:Leader选举、成员变更、日志复制。TiKV利用Raft来做数据复制,每个数据变更都会落地为一条Raft日志,通过Raft的日志复制功能,将数据安全可靠地同步到Group的多数节点中。

Raft & RocksDB 落库

通过单机的RocksDB,我们可以将数据快速地存储在磁盘上;通过Raft,我们可以将数据复制到多台机器上,以防单机失效。数据的写入是通过Raft这一层的接口写入,而不是直接写RocksDB。通过实现Raft,我们拥有了一个分布式的KV,现在再也不用担心某台机器挂掉了。

Region

前面提到,我们将TiKV看做一个巨大的有序的KV Map,那么为了实现存储的水平扩展,我们需要将数据分散在多台机器上。这里提到的数据分散在多台机器上和Raft的数据复制不是一个概念,在这一节我们先忘记Raft,假设所有的数据都只有一个副本,这样更容易理解。

对于一个KV系统,将数据分散在多台机器上有两种比较典型的方案:一种是按照KeyHash,根据Hash值选择对应的存储节点;另一种是分Range,某一段连续的Key都保存在一个存储节点上。TiKV选择了第二种方式,将整个Key-Value空间分成很多段,每一段是一系列连续的Key,我们将每一段叫做一个Region,并且我们会尽量保持每个Region中保存的数据不超过一定的大小(这个大小可以配置,目前默认是64mb)。每一个Region都可以用StartKeyEndKey这样一个左闭右开区间来描述。

TiKV Region

将数据划分成Region后,我们将会做 两件重要的事情:

  • Region为单位,将数据分散在集群中所有的节点上,并且尽量保证每个节点上服务的Region数量差不多。
  • Region为单位做Raft的复制和成员管理。

首先,数据按照Key切分成很多Region,每个Region的数据只会保存在一个节点上面。我们的系统会有一个组件来负责将Region尽可能均匀的散布在集群中所有的节点上,这样一方面实现了存储容量的水平扩展(增加新的结点后,会自动将其他节点上的Region调度过来,另一方面也实现了负载均衡(不会出现某个节点有很多数据,其他节点上没什么数据的情况。同时为了保证上层客户端能够访问所需要的数据,我们的系统中也会有一个组件记录Region在节点上面的分布情况,也就是通过任意一个Key就能查询到这个Key在哪个Region中,以及这个Region目前在哪个节点上。

其次,TiKV是以Region为单位做数据的复制,也就是一个Region的数据会保存多个副本,我们将每一个副本叫做一个ReplicaReplica之间是通过Raft来保持数据的一致(终于提到了Raft,一个Region的多个Replica会保存在不同的节点上,构成一个Raft Group。其中一个Replica会作为这个GroupLeader,其他的Replica作为Follower。所有的读和写都是通过Leader进行,再由Leader复制给Follower。大家理解了Region之后,应该可以理解下面这张图:

TiKV Raft Group

我们以Region为单位做数据的分散和复制,就有了一个分布式的具备一定容灾能力的KeyValue系统,不用再担心数据存不下,或者是磁盘故障丢失数据的问题。

MVCC

很多数据库都会实现多版本控制(MVCCTiKV也不例外。设想这样的场景,两个Client同时去修改一个KeyValue,如果没有MVCC,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁问题。TiKVMVCC实现是通过在Key后面添加Version来实现,简单来说,没有MVCC之前,可以把TiKV看做这样的:

Key1 -> Value
Key2 -> Value
……
KeyN -> Value

有了MVCC之后,TiKVKey排列是这样的:

Key1-Version3 -> Value
Key1-Version2 -> Value
Key1-Version1 -> Value
……
Key2-Version4 -> Value
Key2-Version3 -> Value
Key2-Version2 -> Value
Key2-Version1 -> Value
……
KeyN-Version2 -> Value
KeyN-Version1 -> Value
……

注意,对于同一个Key的多个版本,我们把版本号较大的放在前面,版本号小的放在后面,这样当用户通过一个Key + Version来获取Value的时候,可以将KeyVersion构造出MVCCKey,也就是Key-Version。然后可以直接Seek(Key-Version),定位到第一个大于等于这个Key-Version的位置。

事务

TiKV的事务采用的是Percolator模型,并且做了大量的优化。事务的细节这里不详述,大家可以参考论文以及我们的其他文章。这里只提一点,TiKV的事务采用乐观锁,事务的执行过程中,不会检测写写冲突,只有在提交过程中,才会做冲突检测,冲突的双方中比较早完成提交的会写入成功,另一方会尝试重新执行整个事务。当业务的写入冲突不严重的情况下,这种模型性能会很好,比如随机更新表中某一行的数据,并且表很大。但是如果业务的写入冲突严重,性能就会很差,举一个极端的例子,就是计数器,多个客户端同时修改少量行,导致冲突严重的,造成大量的无效重试。

上一页