目前 FAP 特性在 TiDB Serverless 上已经发布,减少了新建副本的 CPU 和内存开销,提高了吞吐量。在大部分情况下,还能
简介
目的
- 复用 TiFlash 行转列的结果。减少 TiKV 生成、传输和 TiFlash 接收、转换 Snapshot 的开销。
在测试中,发现能够减少 96% 的 CPU 开销和 20% 的内存开销。
如果提升调度的 limiter,能够大幅提高吞吐量,体现为添加副本总时间的减少。但该增长不是线性的,也取决于 TiFlash 侧线程池的大小,以及串行 ingest 的开销。
需要注意,因为 Region 和 Raft Group 绑定,导致 FAP 必须等待 apply Confchange 之后的 Checkpoint,所以对于单个小 Region 来说,可能要花费更长的时间来处理。
目前,TiFlash 上会有一些自建索引,FAP 也会避免这些自建索引被重复构建。 - 利用如 S3 的特性,减少跨 Region 通信。
- 提高副本迁移,特别是单副本迁移的效率。
- 在扩容场景下,新节点可能因为处理全量 Snapshot 更慢,导致进度落后,从而进一步触发全量 Snapshot。此时新机器无法处理被 dispatch 过来的请求。
要点
- 使用 PageStorage 替换 RaftEngine。这样使得 Raft、KVStore 和 DeltaTree 数据都一起被存到同一个 checkpoint 里面,保证原子性和一致性。
- 副本选择和由 Learner 管理的副本创建。用来快速扩容的 TiFlash Checkpoint,必须要比扩容对应的 confchange log entry 要新。这是因为 TiKV 通过一个 Snapshot 来帮助新 node 追日志,而这个 Snapshot 必然在 confchange 后产生。如果接受一个更早的 Checkpoint,那么就要确保 raft 能够给新 peer 发送 confchange 前的日志。即使能,这也意味着新 peer 要处理添加自己的 confchange cmd。即使通过忽略等方案处理,那么在这之前的 batch split cmd 就需要伪装成生成 Checkpoint 的那个 peer,并将这个 region 重新切开(涉及一些行转列和写盘)。而如果与此同时,batch split 得到的某个 split 的最新版本又通过正常途径调度过来,并且在 apply snapshot,那么这里就可能产生 region overlap 导致的数据问题。可以看出,因为违反了 TiKV 的约束,所以产生了很多的潜在问题。
- 注入数据。需要注意,原有的 TiKV 的通过 Snapshot 初始化副本的流程需要重新走一遍。
- 对旧版本数据的清理。
Learner Snapshot
这个 feature 类似于 Learner Snapshot,其实后续我们也希望在 TiKV 实现 Learner Snapshot。目前方案的原因是:
- TiKV 主要需要该 Feature 来避免跨地区的 Snapshot 复制,而 TiFlash 需要该 Feature 实现异构的 Snapshot,侧重点上有所不同。
- 该 feature 需要在 TiKV 或者 PD 等组件中实现一定的调度机制。所以 FAP 实际可以视为一个部分的实现,后续有可能进行推广。届时 FAP 的 phase 1 过程就有可能被移动到 prehandle snapshot 中处理了。
- Follower Snapshot 有可能会失败,例如 Follower 节点实际上做不了该 Snapshot。此时 Snapshot 依然会由 Leader 来处理。目前 TiKV 的模型还不支持这种模式。
从 FAP 的 fallback
FAP 可以实现从 FAP Snapshot 到 Regular Snapshot 的 fallback。具体来说,如果构建失败后,FAP 就会退出,此时对 MsgAppend 的屏蔽就会被去掉,从而走到 Regular Snapshot 的逻辑中。而 FAP Snapshot 在构建完后,会发送一个 meta 等同于 Regular Snapshot 的 Snapshot,只是不包含数据而已。在 Prehandle Snapshot 的逻辑中,会先检查是否存在 FAP Snapshot 并且它的 (snapshot_index, snapshot_term)
是否 meta 中匹配。如果不匹配,说明这是后来的一个 Regular Snapshot,需要覆盖 FAP Snapshot。如果匹配,那么无论这个 Snapshot 是否包含数据,都是和 FAP Snapshot 等价的。
FAP 对 UniPS 的改造
- Checkpoint 中不仅需要上传 Stable 数据,也要上传 Delta 和 Raft Log 数据
原因是必须要上传对应的 Raft Meta 数据才能构建出副本。由此,必须要上传 KVStore 和 Delta 层。此时唯一的可选项就是 applied_index 之后的没有被 apply 的日志了。目前是同样选择上传的,原因是代价可控。并且上传了 Raft Log 后,能够避免新建立的副本从 TiKV Leader 处继续下载这些 Log,从而造成新一轮的落后。
理论上,上传 Delta 数据后,CN 可以从 S3 去读取这些 Delta 数据,从而避免重复请求 WN。我们现在没有做主要是发现 Delta 层的数据流处理没有给 TiFlash 产生太大的性能开销。
在上传 Raft Log 和 Meta 数据后,甚至可以在 CN 上处理 Learner Read 强一致读。但这可能得不偿失,因为读 S3 的开销可能更大。并且我们还是要在 CN 上实现一套 Read Index 的。
相比之下 Snowflake 将事务层移动到 Cloud Service 层上,从而使得可以直接从 S3 读存储层。但可能它们的写入场景应该没有 TiFlash 频繁。 - S3 文件的读写
过去 UniPS 使用了 Lazy 的方式处理 FAP 添加得到的 Page,在 write 的时候只是记录远程的 Page 在 S3 blob file 中的 offset 和 size,在第一次读取的时候,才将这些 Page 下载下来。但在上传 Delta 和 Raft 数据后,需要处理的 Page 数量明显变多了。如果对于每一个 Page 调用一次 GetObject API 花费几十到几百毫秒下载,代价对于可能有几万 Page 的 Region 来说是无法承受的。
这里通过 Prefetch + Reuse 的方式可以优化掉存在顺序读的部分,而顺序读的场景是占大多数的。因为上传 Checkpoint 的时候,会对所有的 Page 按照 PageID 的顺序进行 Compaction,以避免 S3 的空间放大。因此只要按照 page id 的顺序遍历,实际上就是顺序读写 blob file,就可以用上优化。
对于零散的小写入,我们是利用了操作系统的 page cache 来避免大量小 io。 - S3 文件的锁
为了避免 FAP 引用的 blob file 被 GC,引入了 S3 文件锁。这里的做法是对于每一个 blob file,都可能存在多个${data_file_name}.lock_${store_id}
文件,表示这个 blob file 被哪些 store 引用。只有一个 blob file 上没有关联 lock 文件的时候,才会清理掉。
个人感悟
兼容性
上线
上线过程中的逃逸路径的设计非常重要。在 TiFlash 存算分离上线的过程中,使用了双写的方式来避免影响生产环境。在一个月确认稳定后,正式开启这个特性。
FAP 的场景下,采用的方法是只对一个 TiFlash replica 开启 FAP 特性。并且我们在 CN 上增加了一个 blocklist 功能,如果这个节点因为 FAP 损坏,则可以立即设置 blocklist 将它屏蔽。此时,至少还有一个节点可以服务。而在过去,一个节点如果宕机,其实在它上面的查询会死掉,从而影响可用性。