目录
1. TiKV架构和作用
2. RocksDB
2.1 写入
2.2 查询
2.3 Column Families列簇
3. 分布式事务
3.1 事务流程
3.2 分布式事务流程
3.3 MVCC
4. Raft与Multi Raft
4.1 Raft日志复制
4.2 Raft Leader选举
5. TiKV- 读写
5.1 数据的写入
5.2 数据的读取ReadIndex Read
5.3 数据的读取Lease Read
5.4 数据的读取 Follower Read
6. Coprocessor
7. 小结
1. TiKV架构和作用
- 数据持久化
- 分布式一致性
- MVCC
- 分布式事务
- Coprocessor
2. RocksDB
RocksDB 针对 Flash 存储进行优化,延迟极小,使用 LSM 存储引擎
- 高性能的 Key-Value 数据库
- 完善的持久化机制,同时保证性能和安全性
- 良好的支持范围查询
- 为需要存储 TB 级别数据到本地 FLASH 或者 RAM 的应用服务器设计
- 针对存储在高速设备的中小键值进行优化一可以存储在 FLASH 或者直接存储在内存
- 性能随 CPU 数量线性提升,对多核系统友好
2.1 写入
假如插入(1,tom)这条数据,首先做WAL(Write Ahead Log,保证事务的原子性和持久性),写到磁盘中的日志文件中(sync_log=True),不经过操作系统缓存,再把(1,tom)写到MemTable内存中,当写入的大小达到write_buffer_size的值时,把数据刷到immutable MemTable中(固定内存,不能修改了,防止写阻塞),然后RocksDB重新开辟一个MemTable,immutable MemTable中的数据会刷到磁盘SST文件中,当写入速度太快,immutable MemTable个数太多还没来得及写入磁盘,这时候就会限制写入IO,日志中会产生write stall信息,当MemTable中的数据落盘了,wal日志文件中的数据就可以被覆盖了
Level 0是immutable MemTable的复刻,当Level 0文件默认达到4个的时候,就会compaction压缩到Level 1文件并把Key排好序,当Level 1达到256M继续向下一级合并并排好序,当Level 2达到2.5G时再向下一级合并以此类推
2.2 查询
最近最常读取的数据在Block Cache中读取速度就很快,如果读取的数据不在Block Cache中,则相继去MemTable、immutable MemTable、Level 0、Level 1等等中去找,当在SST文件中查找时,有个Bloom Filter过滤器,意思就是要查找的值一定不在该文件中那就真不在该文件中,要查找的值在该文件中,那该值可能在该文件中
2.3 Column Families列簇
写的时候可以指定列簇,可以存放同一类的数据(一张表或者几张表的键值对),没有指定列簇的话,就会放在一个默认default中,一个RocksDB可以有多个列簇,每个列簇可以对应一张表或者几张表,每个列簇有自己的Mem Table和SST文件,列簇之间共享一份WAL日志,其实就是RocksDB的分片技术
3. 分布式事务
3.1 事务流程
在TiDB中begin一个事务,会先从PD中获取一个时间戳start timestamp(start_ts)表示事务的开始时间,然后把要修改的数据读取到内存中,在内存中进行修改,commit后就进入两阶段提交,首先进行prewrite,将修改的数据和锁信息写到TiKV节点中(三个列簇,一个存修改的数据Default,一个存锁信息Lock(第一行主锁),一个存提交信息Write),然后commit,commit的时候会向PD获取一个事务的结束时间commit_ts,同时产生一个提交信息,把锁信息标记为D删除状态,表示事务结束。
在事务中,如果有其他查询来读取,首先查看write列簇,有信息就直接读取该事务中已经修改过的数据,如果write中没有信息,能看到锁信息表示不能从这个地方读,则从其他地方读取。
- Write 列:当用户写入了一行数据时,如果该行数据长度小于 255 字节,那么会被存储 write 列中,否则的话该行数据会被存入到 default 列中。
- Default 列:用于存储超过 255 字节长度的数据
3.2 分布式事务流程
当还未写入TiKV Node 2中的Write的时候,发生了宕机,此时2节点Lock中就没有删除锁的信息,那就会顺着<W,@1>这条锁信息去找1节点中的锁信息,发现1节点的锁信息是删除了,那么2节点会补上一个删除锁的信息。
3.3 MVCC
事务1提交了,事务2未提交,假设此时TSO=120,要读取1和4,会去查看write最近提交信息的TSO版本,查到了就去Default里面找数据,如果此时要修改1和4,则还要查看Lock中的信息,有锁就不能修改,如果此时去读2,也是先查看write提交信息最近的TSO版本,然后去Default里面把数据读出来,修改2的话,则查找了Write的提交信息还要去查看Lock中的锁,此时没有2的锁信息,则可以修改2,这就是MVCC的实现
4. Raft与Multi Raft
- Leader:所有的读写流量都走Leader,通过心跳与Follower通信,并通过日志把数据同步给Follower
- Follower:不参与读写,长时间收不到Leader消息,会变成候选者发起投票
- region:左闭又开的存放数据,一个region初始默认大小96M,达到96M后生成一个新region,类似于存[1,1000),[1000,2000),如果后面有修改,并且修改后比原来大,达到144M后会分裂,如果修改后的数据比原来小,还可以合并region,多个TiKV的同一个region构成一个raft group
4.1 Raft日志复制
- propose ,Leader接收到数据,写入本地raft log,命名为region号+日志序号
- append,将raft log存入本地RocksDB
- replicate,Leader通过raft算法将raft日志一条条给Follower做复制(replicate),Follower收到日志写入本地raft log的RocksDB中持久化,返回一个消息给Leader
- committed,当大多数节点(超过一半)返回成功的消息后,Leader就认为该条修改的数据修改成功
- apply,将数据写入KV的RocksDB中
- Propose
- Append
- Replicate
- Append
- Committed
- Apply
4.2 Raft Leader选举
每个region有一个计时器,election_timeout为10s(raft-election-timeout-ticks=10),集群建立初始状态,都不是Leader,如果Follower10S都没有Leader的消息,Follower就会认为集群中没有Leader ,就会变为condidate发起投票,term时间大于其他节点,大多数其他节点投票给该节点,该节点就成为Leader。
有一种情况就是几个节点同时发出选举,term时间一样,这样就选不出Leader,会发生重复选举,把每个TiKV的elaction_timeout设置为随机值,比如100ms~300ms之间,这样每个TiKV的elaction_timeout值不一样,重复选举的概率就大大降低
集群运行一段时间后,heartbeat time interval为5s(raft-heartbeat-ticks=5),当Leader节点挂了,有其他节点5s都未接收到Leader消息,则发起投票
- Election timeout:raft-election-timeout-ticks
- Heartbeat time interval:raft-heartbeat-ticks
- raft-heartbeat-ticks *raft-base-tick-interval
- raft-election-timeout-ticks *raft-base-tick-interval
意思就是 Election timeout是由参数raft-election-timeout-ticks的值决定,Heartbeat time interval是由raft-heartbeat-ticks参数值决定,这两个参数的值乘以raft-base-tick-interval(默认1S)的值就是得到实际的秒数,raft-election-timeout-ticks>=raft-heartbeat-ticks
5. TiKV- 读写
5.1 数据的写入
Leader的raftstore pool写入raft RocksDB,并且其他副本也写好后,apply pool解析raft日志写入RocksDB kv,此时用户的commit才算完成
5.2 数据的读取ReadIndex Read
能保证读取的TiKV是Leader角色吗?读的时候Leader向Follower发心跳,我是Leader,你是不是Follower ,没错你是我们的Leader,我们是Follower。
保证线性一致性?当用户在10:00时插入一个值(1,jack),用户commit后,此时raft log为1_95,目前RocksDB KV还在1_92,另一个用户在10:05读取(1,jack),此时apply在1_93,apply还没到1_95,raft commit在1_97,这时就记录该值(1_97),该值就叫readindex,read的时候会等待,一直等待到1_97apply后,(1,jack)就能读取。
5.3 数据的读取Lease Read
10:00的时候Leader发了一个心跳,heartbeat time interval为10秒,那么在10秒内TiKV node 2一直是Leader,如果心跳成功收到回复,那么下一个时间间隔(heartbeat time interval)继续发心跳,如果10秒内其他节点未收到心跳,其他节点会认为集群是无主状态,并且时间超过了election timeout,则其他节点发起投票重新选举,从10:00到election timeout的这段时间该Leader会一直是Leader,读取就叫lease read。lease read又叫local read。
5.4 数据的读取 Follower Read
原理和indexread类似
10:00的时候(1,tom)修改为(1,jack),用户commit后,此时Leader raft log为1-95,然后10:05的时候,用户去Follower读取(1,jack),此时Follower的raft commit为1-97,1-97就是readindex,要等待Follower1-97 apply后才能读取到(1,jack),10:08的时候Leader的1-95apply成功(相当于用户commit成功了),实际上在Follower上还是读不到(1,jack),要到到Follower上的1-97apply后才能读取,所以在10:10时,Follower上apply了1-97,此时Follower上就能读取(1,jack)
Leader
Follower
有可能在Follower中读取的新值比在Leader中要快,也就是说Leader中未读取到的值,在Follower中已经有了,Follower在10:06的时候1-97已经apply了,而Leader在10:06后1-97才apply成功,此时在Follower中就先读取到(1,jack),比在Leader中快,原因就在于Follower apply的速度比Leader要快。
6. Coprocessor
当用户发送语句select count(*) from T; 如果把数据都读到TiDB上,网络开销比较大,cpu负载比较高。
当用户发送语句select count(*) from T;把count(*)计算下推到TiKV中做,TiKV中的coprocessor分别在几个节点计算完,然后汇聚在TiDB中,TiDB在收到数据后进行二次整理(3+3+3=9),这样TiDB压力就减轻了很多。
TiKV coprocesstor大多数都在执行物理算子,为sql计算出中间结果,减少TiDB的计算。
下推的物理算子包括
- table scan
- index scan
- selection过滤
- limit
- 聚合
- 采样分析数据统计信息
- 对表进行校验
7. 小结
- TiKV 的结构与作用
- TiVK 的持久化的数据读取原理
- TiKV 对于分布式事务和 MVCC 的支持
- TiKV 基于 Raft 算法的分布式一致性保证
- TiKV 的 coprocessor
来自TiDB官方资料