“我希望能够把 TiDB 的设计的一些理念能够更好的传达给大家,相信大家理解了背后原因后,就能够把 TiDB 用的更好。”
做 TiDB 的缘起是从思考一个问题开始的:为什么在数据库领域有这么多永远也躲不开的坑?从 2015 年我们写下第一行代码,3 年以来我们迎面遇到无数个问题,一边思考一边做,尽量用最小的代价来快速奔跑。
作为一个开源项目,TiDB 是我们基础架构工程师和社区一起努力的结果,TiDB 已经发版到 2.0,有了一个比较稳定的形态,大量在生产环境使用的伙伴们。可以负责任的说,我们做的任何决定都经过了非常慎重的思考和实践,是经过内部和社区一起论证产生的结果。它未必是最好的,但是在这个阶段应该是最适合我们的,而且大家也可以看到 TiDB 在快速迭代进化。
这篇文章是关于 TiDB 代表性“为什么”的 TOP 10,希望大家在了解了我们这些背后的选择之后,能更加纯熟的使用 TiDB,让它在适合的环境里更好的发挥价值。
这个世界有很多人,感觉大于思想,疑问多于答案。感恩大家保持疑问,我们承诺回馈我们的思考过程,毕竟有时候很多思考也很有意思。
一、为什么分布式系统并不是银弹其实并没有什么技术是完美和包治百病的,在存储领域更是如此,如果你的数据能够在一个 MySQL 装下并且服务器的压力不大,或者对复杂查询性能要求不高,其实分布式数据库并不是一个特别好的选择。 选用分布式的架构就意味着引入额外的维护成本,而且这个成本对于特别小的业务来说是不太划算的,即使你说需要高可用的能力,那 MySQL 的主从复制 + GTID 的方案可能也基本够用,这不够的话,还有最近引入的 Group Replication。而且 MySQL 的社区足够庞大,你能 Google 找到几乎一切常见问题的答案。
我们做 TiDB 的初衷并不是想要在小数据量下取代 MySQL,而是尝试去解决基于单机数据库解决不了的一些本质的问题。
有很多朋友问我选择分布式数据库的一个比较合适的时机是什么?我觉得对于每个公司或者每个业务都不太一样,我并不希望一刀切的给个普适的标准(也可能这个标准并不存在),但是有一些事件开始出现的时候:比如是当你发现你的数据库已经到了你每天开始绞尽脑汁思考数据备份迁移扩容,开始隔三差五的想着优化存储空间和复杂的慢查询,或者你开始不自觉的调研数据库中间件方案时,或者人肉在代码里面做 sharding 的时候,这时给自己提个醒,看看 TiDB 是否能够帮助你,我相信大多数时候应该是可以的。
而且另一方面,选择 TiDB 和选择 MySQL 并不是一刀切的有你没他的过程,我们为了能让 MySQL 的用户尽可能减小迁移和改造成本,做了大量的工具能让整个数据迁移和灰度上线变得平滑,甚至从 TiDB 无缝的迁移回来,而且有些小数据量的业务你仍然可以继续使用 MySQL。 所以一开始如果你的业务和数据量还小,大胆放心的用 MySQL 吧,MySQL still rocks,TiDB 在未来等你。
二、为什么是 MySQL和上面提到的一样,并不是 MySQL 不好我们要取代他,而是选择兼容 MySQL 的生态对我们来说是最贴近用户实际场景的选择:
- MySQL 的社区足够大,有着特别良好的群众基础,作为一个新的数据库来说,如果需要用户去学习一套新的语法,同时伴随很重的业务迁移的话,是很不利于新项目冷启动的。
- MySQL 那么长时间积累下来大量的测试用例和各种依赖 MySQL 的第三方框架和工具的测试用例是我们一个很重要的测试资源,特别是在早期,你如何证明你的数据库是对的,MySQL 的测试就是我们的一把尺子。MySQL 那么长时间积累下来大量的测试用例和各种依赖 MySQL 的第三方框架和工具的测试用例是我们一个很重要的测试资源,特别是在早期,你如何证明你的数据库是对的,MySQL 的测试就是我们的一把尺子。
- 已经有大量的已有业务正在使用 MySQL,同时也遇到了扩展性的问题,如果放弃这部分有直接痛点的场景和用户,也是不明智的。已经有大量的已有业务正在使用 MySQL,同时也遇到了扩展性的问题,如果放弃这部分有直接痛点的场景和用户,也是不明智的。
另一方面来看,MySQL 自从被 Oracle 收购后,不管是性能还是稳定性这几年都在稳步的提升,甚至在某些场景下,已经开始有替换 Oracle 的能力,从大的发展趋势上来说,是非常健康的,所以跟随着这个健康的社区一起成长,对我们来说也是一个商业上的策略。
三、为什么 TiDB 的设计中 SQL 层和存储层是分开的一个显而易见的原因是对运维的友好性。很多人觉得这个地方稍微有点反直觉,多一个组件不就会增加部署的复杂度吗?
其实在实际生产环境中,运维并不仅仅包含部署。举个例子,如果在 SQL 层发现了一个 BUG 需要紧急的更新,如果所有部件都是耦合在一起的话,你面临的就是一次整个集群的滚动更新,如果分层得当的话,你可能需要的只是更新无状态的 SQL 层,反之亦然。
另外一个更深层次的原因是成本。存储和 SQL 所依赖的计算资源是不一样的,存储会依赖 IO,而计算对 CPU 以及内存的要求会更高,无需配置 PCIe/NVMe/Optane 等磁盘,而且这两者是不一定对等的,如果全部耦合在一起的话,对于资源调度是不友好的。 对于 TiDB 来说,目标定位是支持 HTAP,即 OLTP 和 OLAP 需要在同一个系统内部完成。显然,不同的 workload 即使对于 SQL 层的物理资源需求也是不一样的,OLAP 请求更多的是吞吐偏好型以及长 query,部分请求会占用大量内存,而 OLTP 面向的是短平快的请求,优化的是延迟和 OPS (operation per second),在 TiDB 中 SQL 层是无状态的,所以你可以将不同的 workload 定向到不同的物理资源上做到隔离。 还是那句话,对调度器友好,同时调度期的升级也不需要把整个集群全部升级一遍。
另一方面,底层存储使用 KV 对数据进行抽象,是一个更加灵活的选择。
一个好处是简单。对于 Scale-out 的需求,对 KV 键值对进行分片的难度远小于对带有复杂的表结构的结构化数据,另外,存储层抽象出来后也可以给计算带来新的选择,比如可以对接其他的计算引擎,和 TiDB SQL 层同时平行使用,TiSpark 就是一个很好的例子。
从开发角度来说,这个拆分带来的灵活度还体现在可以选择不同的编程语言来开发。对于无状态的计算层来说,我们选择了 Go 这样开发效率极高的语言,而对于存储层项目 TiKV 来说,是更贴近系统底层,对于性能更加敏感,所以我们选择了 Rust,如果所有组件都耦合在一起很难进行这样的按需多语言的开发,对于开发团队而言,也可以实现专业的人干专业的事情,存储引擎的开发者和 SQL 优化器的开发者能够并行的开发。 另外对于分布式系统来说,所有的通信几乎都是 RPC,所以更明确的分层是一个很自然的而且代价不大的选择。
四、为什么不复用 MySQL 的 SQL 层,而是选择自己重写这点是我们和很多友商非常不一样的地方。 目前已有的很多方案,例如 Aurora 之类的,都是直接基于 MySQL 的源码,保留 SQL 层,下面替换存储引擎的方式实现扩展,这个方案有几个好处:一是 SQL 层代码直接复用,确实减轻了一开始的开发负担,二是面向用户这端确实能做到 100% 兼容 MySQL 应用。
但是缺点也很明显,MySQL 已经是一个 20 多年的老项目,设计之初也没考虑分布式的场景,整个 SQL 层并不能很好的利用数据分布的特性生成更优的查询计划,虽然替换底层存储的方案使得存储层看上去能 Scale,但是计算层并没有,在一些比较复杂的 Query 上就能看出来。另外,如果需要真正能够大范围水平扩展的分布式事务,依靠 MySQL 原生的事务机制还是不够的。
自己重写整个 SQL 层一开始看上去很困难,但其实只要想清楚,有很多在现代的应用里使用频度很小的语法,例如存储过程什么的,不去支持就好了,至少从 Parser 这层,工作量并不会很大。 同时优化器这边自己写的好处就是能够更好的与底层的存储配合,另外重写可以选择一些更现代的编程语言和工具,使得开发效率也更高,从长远来看,是个更加省事的选择。
五、为什么用 RocksDB 和 Etcd Raft很多工程师都有着一颗造轮子(玩具)的心,我们也是,但是做一个工业级的产品就完全不一样了,目前的环境下,做一个新的数据库,底层的存储数据结构能选的大概就两种:1. B+Tree, 2. LSM-Tree。
但是对于 B+Tree 来说每个写入,都至少要写两次磁盘: 1. 在日志里; 2. 刷新脏页的时候,即使你的写可能就只改动了一个 Byte,这个 Byte 也会放大成一个页的写 (在 MySQL 里默认 InnoDB 的 Page size 是 16K),虽然说 LSM-Tree 也有写放大的问题,但是好处是 LSM-tree 将所有的随机写变成了顺序写(对应的 B+tree 在回刷脏页的时候可能页和页之间并不是连续的)。 另一方面,LSMTree 对压缩更友好,数据存储的格式相比 B+Tree 紧凑得多,Facebook 发表了一些关于 MyRocks 的文章对比在他们的 MySQL 从 InnoDB 切换成 MyRocks (Facebook 基于 RocksDB 的 MySQL 存储引擎)节省了很多的空间。所以 LSM-Tree 是我们的选择。
选择 RocksDB 的出发点是 RocksDB 身后有个庞大且活跃的社区,同时 RocksDB 在 Facebook 已经有了大规模的应用,而且 RocksDB 的接口足够通用,并且相比原始的 LevelDB 暴露了很多参数可以进行针对性的调优。随着对于 RocksDB 理解和使用的不断深入,我们也已经成为 RocksDB 社区最大的使用者和贡献者之一,另外随着 RocksDB 的用户越来越多,这个项目也会变得越来越好,越来越稳定,可以看到在学术界很多基于 LSM-Tree 的改进都是基于 RocksDB 开发的,另外一些硬件厂商,特别是存储设备厂商很多会针对特定存储引擎进行优化,RocksDB 也是他们的首选之一。
反过来,自己开发存储引擎的好处和问题同样明显,一是从开发到产品的周期会很长,而且要保证工业级的稳定性和质量不是一个简单的事情,需要投入大量的人力物力。好处是可以针对自己的 workload 进行定制的设计和优化,由于分布式系统天然的横向扩展性,单机有限的性能提升对比整个集群吞吐其实意义不大,把有限的精力投入到高可用和扩展性上是一个更加经济的选择。 另一方面,RocksDB 作为 LSM-Tree 其实现比工业级的 B+Tree 简单很多(参考对比 InnoDB),从易于掌握和维护方面来说,也是一个更好的选择。 当然,随着我们对存储的理解越来越深刻,发现很多专门针对数据库的优化在 RocksDB 上实现比较困难,这个时候就需要重新设计新的专门的引擎,就像 CPU 也能做图像处理,但远不如 GPU,而 GPU 做机器学习又不如专用的 TPU。
选择 Etcd Raft 的理由也类似。先说说为什么是 Raft,在 TiDB 项目启动的时候,我们其实有过在 MultiPaxos 和 Raft 之间的纠结,后来结论是选择了 Raft。Raft 的算法整体实现起来更加工程化,从论文就能看出来,论文中甚至连 RPC 的结构都描述了出来,是一个对工业实现很友好的算法,而且当时工业界已经有一个经过大量用户考验的开源实现,就是 Etcd。而且 Etcd 更加吸引我们的地方是它对测试的态度,Etcd 将状态机的各个接口都抽象得很好,基本上可以做到与操作系统的 API 分离,极大降低了写单元测试的难度,同时设计了很多 hook 点能够做诸如错误注入等操作,看得出来设计者对于测试的重视程度。
与其自己重新实现一个 Raft,不如借力社区,互相成长。现在我们也是 Etcd 社区的一个活跃的贡献者,一些重大的 Features 例如 Learner 等新特性,都是由我们设计和贡献给 Etcd 的,同时我们还在不断的为 Etcd 修复 Bug。
没有完全复用 Etcd 的主要的原因是我们存储引擎的开发语言使用了 Rust,Etcd 是用 Go 写的,我们需要做的一个工作是将他们的 Raft 用 Rust 语言重写,为了完成这个事情,我们第一步是将 Etcd 的单元测试和集成测试先移植过来了(没错,这个也是选择 Etcd 的一个很重要的原因,有一个测试集作为参照),以免移植过程破坏了正确性。另外一方面,就如同前面所说,和 Etcd 不一样,TiKV 的 Raft 使用的是 Multi-Raft 的模型,同一个集群内会存在海量的互相独立 Raft 组,真正复杂的地方在如何安全和动态的分裂,移动及合并多个 Raft 组,我在我的 这篇文章 里面描述了这个过程。
六、为什么有这样的硬件配置要求我们其实对生产环境硬件的要求还是蛮高的,对于存储节点来说,SSD 或者 NVMe 或者 Optane 是刚需,另外对 CPU 及内存的使用要求也很高,同时对大规模的集群,网络也会有一些要求 (详见我们的官方文档推荐配置的 相关章节 ),其中一个很重要的原因是我们底层的选择了 RocksDB 的实现,对于 LSM Tree 来说因为存在写放大的天然特性,对磁盘吞吐需求会相应的更高,尤其是 RocksDB 还有类似并行 Compaction 等特性。 而且大多数机械磁盘的机器配置倾向于一台机器放更大容量的磁盘(相比 SSD),但是相应的内存却一般来说不会更大,例如 24T 的机械磁盘 + 64G 内存,磁盘存储的数据量看起来更大,但是大量的随机读会转化为磁盘的读,这时候,机械磁盘很容易出现 IO 瓶颈,另一方面,对于灾难恢复和数据迁移来说,也是不太友好的。
另外,TiDB 的各个组件目前使用 gRPC 作为 RPC 框架,gRPC 是依赖 HTTP2 作为底层协议,相比很多朴素的 RPC 实现,会有一些额外的 CPU 开销。TiKV 内部使用 RocksDB 的方式会伴随大量的 Prefix Scan,这意味着大量的二分查找和字符串比较,这也是和很多传统的离线数据仓库很不一样的 Pattern,这个会是一个 CPU 密集型的操作。在 TiDB 的 SQL 层这端,SQL 是计算密集型的应用这个自然不用说,另外对内存也有一定的需求。由于 TiDB 的 SQL 是一个完整的 SQL 实现,表达力和众多中间件根本不是一个量级,有些算子,比如 Hashjoin,就是会在内存里开辟一块大内存来执行 Join,所以如果你的查询逻辑比较复杂,或者 Join 的一张子表比较大的情况下(偏 OLAP 实时分析业务),对内存的需求也是比较高的,我们并没有像单机数据库的优化器一样,比如 Order by 内存放不下,就退化到磁盘上,我们的哲学是尽可能的使用内存。 如果硬件资源不足,及时的通过拒绝执行和失败通知用户,因为有时候半死不活的系统反而更加可怕。
另外一方面,还有很多用户使用 TiDB 的目的是用于替换线上 OLTP 业务,这类业务对于性能要求是比较高的。 一开始我们并没有在安装阶段严格检查用户的机器配置,结果很多用户在硬件明显没有匹配业务压力的情况下上线,可能一开始没什么问题,但是峰值压力一上来,可能就会造成故障,尽管 TiDB 和 TiKV 对这种情况做了层层的内部限流,但是很多情况也无济于事。 所以我们决定将配置检查作为部署脚本的强制检查,一是减少了很多沟通成本,二是可以让用户在上线时尽可能的减少后顾之忧。
七、为什么用 Range-based 的分片策略,而不是 Hash-basedHash-based 的问题是实现有序的 Scan API 会比较困难,我们的目标是实现一个标准的关系型数据库,所以会有大量的顺序扫描的操作,比如 Table Scan,Index Scan 等。用 Hash 分片策略的一个问题就是,可能同一个表的数据是不连续的,一个顺序扫描即使几行都可能会跨越不同的机器,所以这个问题上没得选,只能是 Range 分片。 但是 Range 分片可能会造成一些问题,比如频繁读写小表问题以及单点顺序写入的问题。 在这里首先澄清一下,静态分片在我们这样的系统里面是不存在的,例如传统中间件方案那样简单的将数据分片和物理机一一对应的分片策略会造成:
-
动态添加节点后,需要考虑数据重新分布,这里必然需要做动态的数据迁移;
-
静态分片对于根据 workload 实时调度是不友好的,例如如果数据存在访问热点,系统需要能够快速进行数据迁移以便于将热点分散在不同的物理服务商上。
回到刚才提到的基于 Range 分片的问题,刚才我说过,对于顺序写入热点的问题确实存在,但也不是不可解。对于大压力的顺序写入的场景大多数是日志或者类似的场景,这类场景的典型特点是读写比悬殊(读
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?