云上的 TiDB 和 TiFlash

介绍一下 TiFlash 和 TiDB 云服务相关的一些设计思考。

个人对云原生 TiFlash 的理解

关于存算分离

实现云原生数据库,主要挑战是:

  • 无状态读节点的调度,以及如何做 local caching
  • 数据的 freshness,以及快照读
    通常意味着要 merge S3 和写入节点的数据。写入节点要有状态(挂载 EBS)。
    而这就要求写入节点能够维护本地状态和 S3 状态的对应关系。
    TiFlash 借助于 UniPS 和 Disagg stream 解决了这个问题。并且,因为反正要请求 WN,所以强一致性,以及快照读的能力也能通过它来维护。
  • 节约成本
    • 利用 S3 的特性,减少 Voter 需要存储的数据份数。
    • 利用 S3 的特性,减少异构 Learner 副本构建的开销,如行转列。TiFlash 引入了 FAP 来解决,使得多个写节点最终复用了 S3 上的数据,但是这也让 GC 变得更复杂。
  • Compaction 机制
    因为 S3 上没有 inplace 写,即使有,IOPS 也是个问题。所以对于写入的组织更难,并且对数据整理服务的要求更高。但也有好处,例如我们不需要平衡 Compaction 和 Write 任务的磁盘带宽或者内存的分配。
  • GC 机制
    • 识别过期数据
      S3 上的 GC 通常是通过设置 Lifecycle 或者直接 Delete Object 实现。但是如何识别哪些数据不被需要则很困难,因为读副本和写副本都可能会 refer 由某个写节点 A 上传的数据。可以通过让读副本请求写副本的方式,将问题全部规约到写副本上。
      写节点之间的协调,无论是通过共识还是仲裁者都很复杂。此外,因为写节点很喜欢 refer 另一个写节点在 S3 上的数据,从而节省资源。所以
      TiFlash 实际上是通过每个写节点对需要保留的文件 A 写一个 A.lock 来实现类似 rc 的机制。
    • 原子性问题
      无论 GC worker 是从各个写入节点收集,或者是从 S3 上读取 Manifest 文件,都需要发现得到的 Manifest 文件和上传的 Data 文件是异步的。需要区分一个没有被引用的文件是陈旧的,还是太新了。
  • S3 上的读写问题
  • 写副本的 HA
    • 对于无状态的写节点,如果它发生重启,就只能基于 S3 上的 Checkpoint 做 replay,恢复过程会很慢。
    • 对于有状态的写节点,问题相对简单。

TiFlash Cloud

UniPS 的设计

关于 Delta Index

关于 TiKV、TiDB、TiFlash 的一些思考中“Delta Index”的部分讨论了相关的设计。

快速扩容(FAP)

TiFlash 的快速新建副本(FAP)特性

使用 UniPS 替换 RaftEngine

TiFlash 中列存、KVStore、Raft 日志、列存 Meta、Raft Meta 存储的各个位置是不一样的,所以需要有一个更下层的,能提供 Checkpoint 语义的存储能够兜住。因此,UniPS 应运而生。

目前 TiKV 使用 engine_traits 描述了一个可以用来作为 raftstore 的存储的 engine 所需要的接口。这些接口基本是基于 RocksDB 而抽象出来的。因此 UniPS 需要模拟出其中关键的特性,例如 WriteBatch 等。

UniPS 的性能劣于 RaftEngine,写入延迟大约是两倍。另外,scan 性能预期也比较差,但是仍有不少优化空间。

另外一点是 UniPS 目前不支持 Delete Range,所以在大批量清理 Raft 日志的时候,我们的 WriteBatch 通常会很大。这里面出现过一些 OOM 的问题。

为什么 TiFlash Cloud 目前是两副本?

目前快速恢复还是实验状态。TiFlash 重启后也需要进行一些整理和追日志才能服务,可能影响 HA,这些需要时间优化。尽管如此,快速恢复依然是一个很好的特性,因为:

  1. 快速恢复在 1 wn 下,可以从本节点重启,减少 TiKV 生成 Snapshot 的负担。而这个负担在 v1 版本的 TiKV 上是比较大的。
  2. 减少宕机一个节点恢复后,集群恢复到正常 2 副本的时间。

因为基于 Raft,所以本地数据的丢失只会导致从上一个 S3 Checkpoint 开始回放。如果只有一个存储节点,会失去 HA 特性。

S3 在 TiFlash Cloud 中起到什么作用?

  1. TiFlash Cloud 会定期上传 Checkpoint 到 S3 上,Checkpoint 是一个完整的快照,可以用来做容灾。即使在存储节点宕机后,其上传的那部分数据依然可以被用来查询,可能只能用来服务 stale read?
  2. TiFlash 计算节点可以从 S3 获得数据,相比从存储节点直接获取要更为便宜。存储节点只需要提供一些比较新的数据的读取,减少压力。
  3. 快速扩容逻辑可以复用其他存储节点的数据,此时新节点并不需要从 TiKV 或者其他 TiFlash 获得全部的数据。副本迁移同理,不需要涉及全部数据的移动。

尽管如此,S3 并不是当前 TiFlash 数据的全集。本地会存在:

  1. 上传间隔时间内,还没有上传到 S3 的数据。
  2. 因为生命周期太短,在上传前就被 tombstone 的数据。
  3. 尚在内存中的数据。

我认为使用 S3,是 TiFlash 架构从 shared nothing 走向 shared storage 的一步。我觉得有几个好处:

  • S3 的 durability、ha、跨 az 的能力能省很多很多精力
  • S3 的存储比 ebs 便宜,相应的能够容忍更高的空间方法,因此可以换来更少的 GC 频率,从而节约 CPU
  • 基于 S3 的架构实现 remote compaction 会更加容易

S3

S3 vs EBS

对于 S3 而言:

  1. 具备 99.999999999% 的持久性和 99.99% 的可用性。
    也就是说一天中的不可用时间大约在 9s 左右。
  2. 并发大概在 3500 ops 左右,相比本地磁盘是比较低的
  3. 访问延迟比较高,对于 PutObject 可能能到秒级的延迟

定价

  1. PUT/POST/LIST/COPY 0.005
  2. GET/SELECT 0.0004
  3. 存储每 GB 0.022 USD 每月

可以看到,S3 的定价相比 EBS 要便宜不少。此外,从灾备上来讲,使用 EBS 可能需要为跨 AZ 容灾付出更多的成本,而 S3 可以实现跨 AZ 容灾。

S3 的读写扩容

测试过 S3 的扩容情况。

Cloud Storage Engine

目的

  • 存储成本
    不同于 TiFlash 在云上需要两副本保证 HA,TiKV 在云上需要三副本保证容错。如果这三个副本都使用 EBS,那么代价非常高昂。
    从高可用的角度来说,三副本 EBS 是没必要的,完全可以两副本 EBS,另一个副本作为 witness 存在。在 S3 上存放一份数据(SST 文件),本地 EBS 的数据可以作为 S3 数据的 Cache。
    进一步的,如果能够实现三个副本在底层的 SST 完全一样,那么 Compaction 的成本也能降低。【Q】这个我理解就得一个 Region 一个 LSM 了,类似 TiFlash 目前的设计是不太行得通的。
  • 高可用和容错
    关于 TiKV、TiDB、TiFlash 的一些思考中“高可用和持久性”的部分。
  • Region
    因为 shared nothing 版本的 TiKV 的 Snapshot 传输代价大,所以限制了 region 大小为 96MB,目前已经调整了默认值为 256MB。
    但是在云上架构中,新增副本可以通过引用 S3 上的文件来实现,成本接近于 0,所以 Snapshot 的负担就会变小。
    对于 TiFlash 接入 Cloud KV,虽然行转列的成本仍然存在,但是因为 SST 并不需要缓存在本地,而是可以流式地从 S3 读取,并直接在内存中完成行转列,所以能看到 1.1GB/s 的逻辑写入速度(该数据没有经过落盘时候的进一步压缩)。
    调大 Region Size 减少 Region 数量的优势,在 TiKV 的 partitioned raft kv 特性 中也有提及,这里就省略了。

设计细节

Raft log

Raft log 需要存在 EBS 而不是 Local Disk 上,从而保证持久化。

相比于 OP 使用的 raft-engine,云上架构中,因为 memtable 中的数据需要从 raft log 恢复,因此需要保留更多的 raft log。如果继续使用 RaftEngine,则对内存的要求会非常高。

Flush

为了 S3 上只存一个副本,所以 flush 的方案不太一样:

  • Leader flush 得到 L0,
  • Leader 将 L0 传到 S3 上
  • Raft 将这次 flush 复制给其他 follower
    如果这个过程失败了,说明中间 Leader 切换,直接 abort 即可
  • 等该 flush 被 apply 后,才会将 L0 写入到存储里面

Split

Split 的复杂点主要在:

  • Cloud 的架构中的 Split 是物理 Split,不像 OP 一样只需要修改元数据就行了
    • 物理 Split 的过程中,Memtable 的 flush 以及 Compaction 是停止的
  • Split 过程中可能出现 Leader 切换
  • 分布式场景中,Leader 要得知所有 Follower 上的 Split 状态是困难的
    • 因为分布式系统的不可靠性,只要有大多数节点完成了物理 Split SST 文件的操作,就应该运行执行 FinishSplit 了。对于那些落后的节点,就需要特殊处理。

Merge