RebornLee 2019-07-01
本文为我司 Engineering VP 申砾在 TiDB DevCon 2019 上的演讲实录。在 上篇 中,申砾老师重点回顾了 TiDB 2.1 的特性,并分享了我们对「如何做好一个数据库」的看法。
本篇将继续介绍 TiDB 3.0 Beta 在稳定性、易用性、功能性上的提升,以及接下来在 Storage Layer 和 SQL Layer 的规划,enjoy~
2018 年年底我们开了一次用户吐槽大会,当时我们请了三个 TiDB 的重度用户,都是在生产环境有 10 套以上 TiDB 集群的用户。那次大会规则是大家不能讲 TiDB 的优点,只能讲缺点;研发同学要直面问题,不能辩解,直接提解决方案;当然我们也保护用户的安全(开个玩笑 :D),让他们放心的来吐槽。刚刚的社区实践分享也有点像吐槽大会第二季,我们也希望用户来提问题,分享他们在使用过程遇到什么坑,因为只有直面这些问题,才有可能改进。所以我们在 TiDB 3.0 Beta 中有了很多改进,当然还有一些会在后续版本中去改进。
TiDB 3.0 版本第一个目标就是「更稳定」,特别是在大规模集群、高负载的情况下保持稳定。稳定性压倒一切,如果你不稳定,用户担惊受怕,业务时断时续,后面的功能都是没有用的。所以我们希望「先把事情做对,再做快」。
首先来看 TiDB 3.0 一个比较亮眼的功能——多线程 Raft。我来给大家详细解释一下,为什么要做这个事情,为什么我们以前不做这个事情。
<center>图 8 TiKV 抽象架构图</center>
这是 TiKV 一个抽象的架构(图 8)。中间标红的图形是 RaftStore 模块,所有的 Raft Group 都在一个 TiKV 实例上,所有 Raft 状态机的驱动都是由一个叫做 RaftStore 的线程来做的,这个线程会驱动 Raft 状态机,并且将 Raft Log Append 到磁盘上,剩下的包括发消息给其他 TiKV 节点以及 Apply Raft Log 到状态机里面,都是由其他线程来做的。早期的时候,可能用户的数据量没那么大,或者吞吐表现不大的时候,其实是感知不到的。但是当吞吐量或者数据量大到一定程度,就会感觉到这里其实是一个瓶颈。虽然这个线程做的事情已经足够简单,但是因为 TiKV 上所有的 Raft Peer 都会通过一个线程来驱动自己的 Raft 状态机,所以当压力足够大的时候就会成为瓶颈。用户会看到整个 TiKV 的 CPU 并没有用满,但是为什么吞吐打不上去了?
<center>图 9 TiDB 3.0 Multi-thread RaftStore</center>
因此在 TiDB 3.0 中做了一个比较大的改进,就是将 RaftStore 这个线程,由一个线程变成一个线程池, TiKV 上所有 Raft Peer 的 Raft 状态机驱动都由线程池来做,这样就能够充分利用 CPU,充分利用多核,在 Region 特别多以及写入量特别大的时候,依然能线性的提升吞吐。
<center>图 10 TiDB 3.0 Beta oltp_insert</center>
通过上图大家可以看到,随着并发不断加大,写入是能够去线性扩展的。在早期版本中,并发到一定程度的时候,RaftStore 也会成为瓶颈,那么为什么我们之前没有做这个事情?这个优化效果这么明显,之所以之前没有做,是因为之前 Raft 这块很多时候不会成为瓶颈,而在其他地方会成为瓶颈,比如说 RocksDB 的写入或者 gRPC 可能会成为瓶颈,然后我们将 RaftStore 中的功能不断的向外拆,拆到其他线程中,或者是其他线程里面做多线程,做异步等等,随着我们的优化不断深入,用户场景下的数据量、吞吐量不断加大,我们发现 RaftStore 线程已经成为需要优化的一个点,所以我们在 3.0 中做了这个事情。而且之前保持单线程也是因为单线程简单,「先把事情做对,然后再做快」。
第二个改进是 Batch Message。我们的组件之间通讯选择了 gRPC,首先是因为 gRPC 是 Google 出品,有人在维护他,第二是用起来很简单,也有很多功能(如流控、加密)可以用。但其实很多人吐嘈它性能比较慢,在知乎上大家也能看到各种问题,包括讨论怎么去优化他,很多人也有各种优化经验,我们也一直想怎么去优化他。以前我们用的方法是来一个 message 就通过 gRPC 发出去,虽然性能可能没有那么好,或者说性能不是他最大的亮点,但有时候调性能不能单从一个模块去考虑,应该从架构上去想,就是架构需要为性能而设计,架构上的改进往往能带来性能的质变。
所以我们在 TiDB 3.0 Beta 中设计了 Batch Message 。以前是一个一个消息的发,现在是按照消息的目标分队列,每个队列会有一个 Timer,当消息凑到一定个数,或者是你的 Timer 到了时间(现在应该设置的是 1ms,Batch 和这个 Timer 数量都可以调),才会将发给同一个目的地的一组消息,打成一个包,一起发过去。有了这个架构上的调整之后,我们就获得了性能上的提升。
<center>图 11 TiDB 3.0 Beta - Batch Message</center>
当然大家会想,会不会在并发比较低的时候变慢了?因为你凑不到足够的消息,那你就要等 Timer。其实是不会的,我们也做了一些设计,就是由对端先汇报「我当前是否忙」,如果对端不忙,那么选择一条一条的发,如果对端忙,那就可以一个 Batch 一个 Batch 的发,这是一个自适应的 Batch Message 的一套系统。图 11 右半部分是一个性能对比图,有了 Batch Message 之后,在高并发情况下吞吐提升非常快,在低并发情况下性能并没有下降。相信这个改进可以给大家带来很大的好处。
第三点改进就是 Titan。CEO 刘奇在 Opening Keynote 中提到了我们新一代存储引擎 Titan,我们计划用 Titan 替换掉 RocksDB,TiDB 3.0 中已经内置了 Titan,但没有默认打开,如果大家想体验的话,可以通过配置文件去把 RocksDB 改成 Titan。我们为什么想改进 RocksDB 呢?是因为它在存储大的 Key Value 的时候,有存储空间放大和写放大严重的问题。
<center>图 12 TiDB 3.0 中内置的新存储引擎 Titan</center>
所以我们尝试解决这个问题。当你写入的 Key Value 比较大的时候,我们会做一个检查,然后把大的 Value 放到一个 Blob File 里去,而不是放到 LSM-Tree。这样的分开存储会让 LSM-Tree 变得很小,避免了因为 LSM-Tree 比较高的时候,特别是数据量比较大时出现的比较严重的写放大问题。有了 Titan 之后,就可以解决「单个 TiKV 服务大量数据」的需求,因为之前建议 TiKV 一个实例不要高于 1T。我们后面计划单个 TiKV 实例能够支持 2T 甚至 4T 数据,让大家能够节省存储成本,并且能在 Key Value 比较大的时候,依然能获得比较好的性能。
除了解决写放大问题之外,其实还有一个好处就是我们可以加一个新的 API,比如 KeyExist,用来检查 Key 是否存在,因为这时 Key 和 Value 是分开存储的,我们只需要检查 Key 是否在,不需要把 Value Load 进去。或者做 Unique Key 检查时,可以不需要把 Key Value 取出来,只需要加个接口,看这个 Key 是否存在就好了,这样能够很好的提升性能。
第四点是保持查询计划稳定。这个在数据库领域其实是一个非常难的问题,我们依然没有 100% 解决这个问题,希望在 2019 年第一季度,最多到第二季度,能有一个非常好的解决方案。我们不希望当数据量变化 、写入变化、负载变化,查询计划突然变错,这个问题在线上使用过程中是灾难。那么为什么会跑着跑着变错?首先来说我们现在是一个 Cost-based optimizers,我们会参考统计信息和当前的数据的分布,来选择后面的 plan。那么数据的分布是如何获得的呢?我们是通过统计信息,比如直方图、CM Sketch来获取,这里就会出现两个问题:
1. 统计信息可能是不准的。统计信息毕竟是一个采样,不是全量数据,会有一些数据压缩,也会有精度上的损失。
2. 随着数据不断写入,统计信息可能会落后。因为我们很难 100% 保证统计信息和数据是 Match 的。
<center>图 13 查询计划稳定性解决方案</center>
一个非常通用的思路是, 除了依赖于 Cost Model 之外,我们还要依赖更多的 Hint,依赖于更多启发式规则去做 Access Path 裁减。举个例子:
select * from t where a = x and b = y; idx1(a, b) idx2(b) -- pruned
大家通过直观印象来看,我们一定会选择第一个索引,而不是第二个索引,那么我们就可以把第二个索引裁掉,而不是因为统计信息落后了,然后估算出第二个索引的代价比较低,然后选择第二个索引。上面就是我们最近在做的一个事情,这里只举了一个简单的例子。
TiDB 3.0 第二个目标是可用性,是让 TiDB 简单易用。
在 TiDB 2.0 中,大家看一个 Query 为什么慢了依赖的是 Explain,就是看查询计划,其实那个时候大家很多都看不懂,有时候看了也不知道哪有问题。后来我们在 TiDB 2.1 中支持了 Explain Analyze,这是从 PG 借鉴过来一个特性,就是我们真正的把它执行一边,然后再看看每个算子的耗时、处理的数据量,看看它到底干了一些什么事情,但其实可能还不够细,因为还没有细化到算子内部的各种操作的耗时。
<center>图 14 TiDB 3.0 - Query Tracing</center>
所以我们又做了一个叫 Query Tracing 的东西,其实在 TiDB 2.1 之前我们已经做了一部分,在 TiDB 3.0 Beta 中做了一个收尾,就是我们可以将 Explain 结果转成一种 Tracing 格式,再通过图形化界面,把这个 Tracing 的内容展示出来,就可以看到这个算子具体干了一些什么事,每一步的消耗到底在哪里,这样就可以知道哪里有问题了。希望大家都能在 TiDB 3.0 的版本中非常直观的定位到 Query 慢的原因。
然后第二点 Plan Management 其实也是为了 Plan 不稳定这个问题做准备的。虽然我们希望数据库能自己 100% 把 Plan 选对,但是这个是非常美好的愿望,应该还没有任何一个数据库能保证自己能 100% 的解决这个问题。那么在以前的版本中,出现问题怎么办?一种是去 Analyze 一下,很多情况下他会变好,或者说你打开自动 Analyze 这个特性,或者自动 FeedBack 这个特性,可以一定程度上变好,但是还可能过一阵统计信息又落后了,又不准了,Plan 又错了,或者由于现在 cost 模型的问题,有一些 Corner Case 处理不到,导致即使统计信息是准确的, Plan 也选不对。
<center>图 15 TiDB 3.0 Beta - Plan Management</center>
那么我们就需要一个兜底方案,让大家遇到这个问题时不要束手无策。一种方法是让业务去改 SQL,去加 Hint,也是可以解决的,但是跟业务去沟通可能会增加他们的使用成本或者反馈周期很长,也有可能业务本身也不愿意做这个事情。
另外一种是用一种在线的方式,让数据库的使用者 DBA 也能非常简单给这个 Plan 加 Hint。具体怎么做呢?我们和美团的同学一起做了一个非常好的特性叫 Plan Management,就是我们有一个 Plan 管理的模块,我们可以通过 SQL 接口给某一条 Query,某一个 Query 绑定 Plan,绑定 Hint,这时我们会对 SQL 做指纹(把 Where 条件中的一些常量变成一个通配符,然后计算出一个 SQL 的指纹),然后把这个 Hint 绑定在指纹上。一条 Query 来了之后,先解成 AST,我们再生成指纹,拿到指纹之后,Plan Hint Manager 会解析出绑定的 Plan 和 Hint,有 Plan 和 Hint 之后,我们会把 AST 中的一部分节点替换掉,接下来这个 AST 就是一个「带 Hint 的 AST」,然后扔给 Optimizer,Optimizer 就能根据 Hint 介入查询优化器以及执行计划。如果出现慢的 Query,那么可以直接通过前面的 Query Tracing 去定位,再通过 Plan Management 机制在线的给数据库手动加 Hint,来解决慢 Query 的问题。这样下来也就不需要业务人员去改 SQL。这个特性应该在 TiDB 3.0 GA 正式对外提供,现在在内部已经跑得非常好了。在这里也非常感谢美团数据库开发同学的贡献。
TiDB 3.0 中我们增加了 Join Reorder。以前我们有一个非常简单的 Reorder 算法,就是根据 Join 这个路径上的等值条件做了一个优先选择,现在 TiDB 3.0 Beta 已经提供了第一种 Join Reorder 算法,就是一个贪心的算法。简单来说,就是我有几个需要 Join 的表,那我先从中选择 Join 之后数据量最小的那个表(是真正根据 Join 之后的代价来选的),然后我在剩下的表中再选一个,和这个再组成一个 Join Path,这样我们就能一定程度上解决很多 Join 的问题。比如 TPC-H 上的 Q5 以前是需要手动加 Hint 才能跑出来,因为它没有选对 Join 的路径,但在 TiDB 3.0 Beta 中,已经能够自动的选择最好的 Join Path 解决这个问题了。
<center>图 16 TiDB 3.0 Beta - Join Reorder</center>
我们接下来还要再做一个基于动态规划的 Join Reorder 算法,很有可能会在 3.0 GA 中对外提供。 在 Join 表比较少的时候,我们用动态规划算法能保证找到最好的一个 Join 的路径,但是如果表非常多,比如大于十几个表,那可能会选择贪心的算法,因为 Join Reorder 还是比较耗时的。
说完稳定性和易用性之外,我们再看一下功能。
<center>图 17 TiDB 3.0 Beta 新增功能</center>
我们现在做了一个插件系统,因为我们发现数据库能做的功能太多了,只有我们来做其实不太可能,而且每个用户有不一样的需求,比如说这家想要一个能够结合他们的监控系统的一个模块,那家想要一个能够结合他们的认证系统做一个模块,所以我们希望有一个扩展的机制,让大家都有机会能够在一个通用的数据库内核上去定制自己想要的特性。这个插件是基于 Golang 的 Plugin 系统。如果大家有 TiDB Server 的 Binary 和自己插件的 .so,就能在启动 TiDB Server 时加载自己的插件,获得自己定制的功能。
图 17 还列举了一些我们正在做的功能,比如白名单,审计日志,Slow Query,还有一些在 TiDB Hackathon 中诞生的项目,我们也想拿到插件中看看是否能够做出来。
<center>图 18 TiDB 3.0 Beta - OLTP Benchmark</center>
从图 18 中可以看到,我们对 TiDB 3.0 Beta 中做了这么多性能优化之后,在 OLTP 这块进步还是比较大的,比如在 SysBench 下,无论是纯读取还是写入,还是读加写,都有几倍的提升。在解决稳定性这个问题之后,我们在性能方面会投入更多的精力。因为很多时候不能把「性能」单纯的当作性能来看,很多时候慢了,可能业务就挂了,慢了就是错误。
当然 TiDB 3.0 中还有其他重要特性,这里就不详细展开了。(TiDB 3.0 Beta Release Notes )
刚才介绍是 3.0 Beta 一些比较核心的特性,我们还在继续做更多的特性。
<center>图 19 TiDB 存储引擎层未来规划</center>
比如在存储引擎层,我们对 Raft 层还在改进,比如说刚才我提到了我们有 Raft Learner,我们已经能够极大的减少由于调度带来的 Raft Group 不可用的概率,但是把一个 Learner 提成 Voter 再把另一个 Voter 干掉的时间间隔虽然比较短,但时间间隔依然存在,所以也并不是一个 100% 安全的方案。因此我们做了 Raft Joint Consensus。以前成员变更只能一个一个来:先把 Learner 提成 Voter,再把另一个 Voter 干掉。但有了 Raft Joint Consensus 之后,就能在一次操作中执行多个 ConfChange,从而把因为调度导致的 Region 不可用的概率降为零。
另外我们还在做跨数据中心的部署。前面社区实践分享中来自北京银行的于振华老师提到过,他们是一个两地三中心五部分的方案。现在的 TiDB 已经有一些机制能比较不错地处理这种场景,但我们能够做更多更好的东西,比如说我们可以支持 Witness 这种角色,它只做投票,不同步数据,对带宽的需求比较少,即使机房之间带宽非常低,他可以参与投票。在其他节点失效的情况下,他可以参与选举,决定谁是 Leader。另外我们支持通过 Follower 去读数据,但写入还是要走 Leader,这样对跨机房有什么好处呢? 就是可以读本地机房的副本,而不是一定要读远端机房那个 Leader,但是写入还是要走远端机房的 Leader,这就能极大的降低读的延迟。除此之外,还有支持链式复制,而不是都通过 Leader 去复制,直接通过本地机房复制数据。
之后我们还可以基于 Learner 做数据的 Backup。通过 learner 去拉一个镜像,存到本地,或者通过 Learner 拉取镜像之后的增量,做增量的物理备份。所以之后要做物理备份是通过 Learner 实时的把 TiKV 中数据做一个物理备份,包括全量和增量。当需要恢复的时候,再通过这个备份直接恢复就好了,不需要通过 SQL 导出再导入,能比较快提升恢复速度。
<center>图 20 TiDB 存储引擎层未来规划</center>
在 SQL 层,我们还做了很多事情,比如 Optimizer 正在朝下一代做演进,它是基于最先进的 Cascades 模型。我们希望 Optimizer 能够处理任意复杂的 Query,帮大家解决从 OLTP 到 OLAP 一整套问题,甚至更复杂的问题。比如现在 TiDB 只在 TiKV 上查数据,下一步还要接入TiFlash,TiFlash 的代价或者算子其实不一样的,我们希望能够在 TiDB 上支持多个存储引擎,比如同一个 Query,可以一部分算子推到 TiFlash 上去处理,一部分算子在 TiKV 上处理,在 TiFlash 上做全表扫描,TiKV 上就做 Index 点查,最后汇总在一起再做计算。
我们还计划提供一个新的工具,叫 SQL Tuning Advisor。现在用户遇到了慢 Query,或者想在上线业务之前做 SQL 审核和优化建议,很多时候是人肉来做的,之后我们希望把这个过程变成自动的。
除此之外我们还将支持向量化的引擎,就是把这个引擎进一步做向量化。未来我们还要继续兼容最新的 MySQL 8.0 的特性 Common Table,目前计划以 MySQL 5.7 为兼容目标,和社区用户一起把 TiDB 过渡到 MySQL 8.0 兼容。
说了这么多,我个人觉得,我们做一个好的数据库,有用的数据库,最重要一点是我们有大量的老师,可以向用户,向社区学习。不管是分享了使用 TiDB 的经验和坑也好,还是去提 Issue 报 Bug,或者是给 TiDB 提交了代码,都是在帮助我们把 TiDB 做得更好,所以在这里表示一下衷心的感谢。最后再立一个 flag,去年我们共写了 24 篇 TiDB 源码阅读文章,今年还会写 TiKV 源码系列文章。我们希望把项目背后只有开发同学才能理解的这套逻辑讲出来,让大家知道 TiDB 是怎样的工作的,希望今年能把这个事情做完,感谢大家。
1 月 19 日 TiDB DevCon 2019 在北京圆满落幕,超过 750 位热情的社区伙伴参加了此次大会。会上我们首次全面展示了全新存储引擎 Titan、新生态工具 TiFlash 以及 TiDB 在云上的进展,同时宣布 TiDB-Lightning Toolset & TiDB-DM 两大生态工具开源,并分享了 TiDB 3.0 的特性与未来规划,描述了我们眼中未来数据库的模样。此外,更有 11 位来自一线的 TiDB 用户为大家分享了实践经验与踩过的「坑」。同时,我们也为新晋 TiDB Committer 授予了证书,并为 2018 年最佳社区贡献个人、最佳社区贡献团队颁发了荣誉奖杯。