本文大约 15000 字,阅读需要 50 分钟。
上一篇文章啃硬骨头差点把我牙给崩了,本文我们还是回到舒适区来,继续挥舞架构大棒,欺负可怜的小数据库。本文虽然不太硬,但是量还是很大的,管饱。
说到后端系统对于数据库的要求,基本上和你老板一样:既要又要还要。数据库扮演的那个单点角色,在单机上已经如此的困难了,换到分布式环境下只会更困难。而分布式数据库的出现也是被迫的:应用规模越来越大,对性能和可用性的要求越来越高,不得不搞分布式数据库了。
接下来请大家坐稳扶好,我们正式开始分布式数据库历史变迁之旅。
正如经济政策的不可能三角“不可能同时实现资本流动自由,货币政策的独立性和汇率的稳定”那样,单机数据库也有一个不可能三角,那就是:①持久化 ②事务隔离 ③高性能。
- 持久化需要每一次写数据都要落到磁盘上,宕机再启动以后,数据库可以自修复。如果只要求这一条,很好实现。
- 事务隔离需要每一次会话(session)的事务都拥有自己的数据库版本:既要多个并行的事务相互之间不会写到对方的虚拟数据库上(读提交),又要不能读到对方的虚拟数据库上(可重复读),还要在一个事务内不能读到别的事务已经提交的新增的数据(幻读),终极需求则是完全串行化:我的读 session 不结束,你就不能读。这个需求和持久化需求结合以后,会大幅增加日志管理的复杂度,但,还是可以管理的。
- 读写都要尽量地快:单独实现也很快,Redis 嘛,但是加上持久化和事务隔离,就很难做了:需要对前两项进行妥协。
MySQL 首先选择了持久化:失去人性,失去很多,失去持久化,失去一切。没有持久化能力,那还当个毛的核心数据库,所以这一条是所有磁盘数据库的刚需,完全无法舍弃。
然后 MySQL 选择了一部分高性能:MyISAM 就是为了快速读写而创造的,早期 MySQL 在低配 PC 机上就有不错的性能。后来更高级的 InnoDB 出现了,小数据量时它的读取性能不如 MyISAM,写性能更是彻底拉胯,但是在面对大数据量场景时,读性能爆棚,还能提供很多后端程序员梦寐以求的高级功能(例如丰富的索引),承担了大部分互联网核心数据库的角色。
最后,MySQL 将事务隔离拆成了几个级别,任君挑选:你要强事务隔离,性能就差;你能接受弱事务隔离,性能就强。你说无事务隔离?那你用 MySQL 干什么,Redis 它不香吗。
所以 MySQL 其实选择了 持久化*1 + 高性能*0.8 + 事务隔离*0.5,算下来,还赚了 0.3 ( ̄▽ ̄)"
不过,从 MySQL 也可以看出,“数据库的不可能三角”并不是完全互斥的,是可以相互妥协的。
在开始细数分布式数据库之前,我们先看一个非分布式的提升数据库性能的方案,读写分离,主从同步。
由于 web 系统中读写需求拥有明显的二八分特征——读取流量占 80%,写入流量占 20%,所以如果我们能把读性能拆分到多台机器上,在同样的硬件水平下,数据库总 QPS 也是能提高五倍的。
无论是远古时代谷歌的 MMM(Multi-Master Replication Manager for MySQL) 还是中古时代的 MySQL 官方的 MGR(MySQL Group Replication),还是最近刚刚完成开发且收费的官方 InnoDB Cluster,这些主从架构的实现方式都是一致的:基于行同步或者语句同步,近实时
地从主节点向从节点同步新增和修改的数据。
由于这种方法必然会让主从之间存在有一段时间的延迟(数百毫秒到数秒),所以一般在主从前面还要加一个网关进行语句分发:
select
等读语句默认发送到从节点,以尽量降低主节点负载- 一旦出现
update
、insert
等些语句,立刻发送到主节点 - 并且,本次会话(session)内的所有后续语句,必须全部发送给主节点,不然就会出现数据写入了但是读不到的情况
搭建一个一主四从的 MySQL 集群,总 QPS 就能从单节点的 1 万提升到 5 万,顺便还能拥有主节点故障后高可用的能力。主从架构比较简单,也没有什么数据冲突问题,就是有一个很大的弱点:
那怎么提升写性能呢?这个时候就要掏出分布式数据库了。
由于数据库的单点性非常强,所以在谷歌搞出 GFS、MapReduce、Bigtable 三驾马车之前,业界对于高性能数据库的主要解决方案是买 IOE 套件:IBM 小型机 + Oracle + EMC 商业存储。而当时的需求也确实更加适合商用解决方案。
后来搜索引擎成为了第一代全民网站,而搜索引擎的数据库却“不那么关系型”,所以谷歌搞出了自己的分布式 KV 数据库。后来谷歌发现 SQL 和事务隔离在很多情况下还是刚需,于是在 KV 层之上改了一个强一致支持事务隔离的 Spanner 分布式数据库。而随着云计算的兴起,分布式数据库已经成了云上的“刚需”:业务系统全部上云,总不能还用 Oracle 物理服务器吧?于是云上数据库又开始大踏步发展起来。
下面我们按照时间顺序,逐一梳理分布式数据库的发展史。
关系型数据库为了解决不可能三角需求,其基本架构 40 年没有变过。
MySQL 自己其实已经是一个非常优秀的满足互联网业务场景的单体数据库了,所以基于 MySQL 的基本逻辑进行架构改进,是最稳妥的方案。
在没有分布式关系型数据库技术出现的时代,后端开发者们往往只能选择唯一的刀耕火种的路:在应用代码里调用多个数据库,以应对单个数据库性能不足的困境。后来,有人把这些调用多个数据的代码抽出来作为单独的一层,称作数据库中间件。
首先,对数据表进行纵向分表:按照一定规则,将一张超多行数的表分散到多个数据库中。
ShardingSphere 中的 Sharding-Proxy 工作方式然后,无论是插入、更新还是查询,都通过一个 proxy 将 SQL 进行重定向和拆分,发送给多个数据库,再将结果聚合,返回。
大名鼎鼎的数据库中间件,其基本原理一句话就能描述:使用一个常驻内存的进程,假装自己是个独立数据库,再提供全局唯一主键、跨分片查询、分布式事务等功能,将背后的多个数据库“包装”成一个数据库。
虽然“中间件”这个名字听起来像一个独立组件,但实际上它依然是强业务亲和性的:没有几家公司会自己研发数据库,但每家公司都会研发自己的所谓中间件,因为中间件基本上就代表了其背后的一整套“多数据库分库分表开发规范”。所以,中间件也不属于“通用数据库”范畴,在宏观架构层面,它依然属于应用的一部分。我称这个时代为刀耕火种时代。
那该怎么脱离刀耕火种呢?人类的大脑是相似的:既然应用代码做数据规划和逻辑判断很容易失控,那我们在数据库层面把这件事接管了行不行呢?当然可以,但是需要拿东西找信息之神交换
。
历史上,第一个被放弃的是事务隔离
,而它带来的就是第二代分布式数据库:KV 数据库。
在分布式数据库时代,持久化已经不是分布式数据库“真正的持久化”了,取而代之的是“数据一致性”:由于数据存在了多台机器上,那机器之间数据的一致性就成了新时代的“持久化”。于是新不可能三角出现了:①一致性 ②事务隔离 ③高性能。
你是不是在期待 CAP 理论呀?别着急,我们后面会说。
数据库技术一共获得过四次图灵奖,后面三次都在关系型数据库领域。事务隔离模型是关系型数据库的核心,非常地简洁、优美、逻辑自恰。
Google 是第一个全民搜索引擎,系统规模也达到了史上最大。但是,搜索引擎技术本身却不需要使用关系型数据库来存储:搜索结果中的网页链接之间是离散的。这块我要挖个坑,本系列完结以后,我会写一篇如何自己开发搜索引擎的文章。
由于搜索不需要关系型数据库,自然谷歌搞的分布式数据库就是 KV 模型。谷歌的三驾马车论文发布以后,业界迅速发展出了一个新的数据库门类 NoSQL(Not Only SQL),专门针对非结构化和半结构化的海量数据。
目前,缓存(Redis)和文档/日志(MongoDB)大家一般都会用 NoSQL 来承载。在这个领域,最成功的莫过于基于 Hadoop 生态中 HDFS 构建的 HBase 了:它主要提供的是行级数据一致性,即 CAP 理论中的 CP,放弃了事务,可以高性能地存储海量数据。
KV 数据库结构简单,性能优异,扩展性无敌,但是它只能作为核心数据库的高性能补充,绝大多数场景下,核心数据库还是得用关系型。
从 2005 年开始,Google Adwords 开始基于 MySQL 搭建系统,这也推动了 MySQL 在 Google 内部的大规模使用。随着业务的发展,MySQL 集群越来越庞大,其中最痛苦的就是“数据再分片”,据说有一次谷歌对数据库的重新分片持续了 2 年才完成。于是谷歌痛定思痛,搞出了一个支持分布式事务和数据强一致性的分布式关系型数据库:Google Spanner。
2012 年,谷歌发布了 Spanner 论文¹,拉开了分布式强一致性关系型数据库的大幕。这个数据库是真的牛逼,当我第一次看到它机柜照片的时候直接震惊了:
这套系统采用了 GPS 授时 + 2 台原子钟 + 4 台时间服务器,让分布在全球多个数据中心的 Spanner 集群进行相当精确的时间同步:基于 TrueTime 服务,时间差可以控制在 10ms 之内。这种真正的全球数据库可以做到即使单个数据中心完全失效,应用也完全无感知。
当然,如此规模的全球数据库全世界也没有几家公司有需求,如果我们只在一个数据中心内署数据库集群,时间同步可以很容易地做到 1ms 之内,原子钟这种高端货还用不到。
为什么上篇文章要写一万多字,就是为本文打基础的,现在就用到了。
上文中我们说过,写入型 SQL 会在写入缓存页 + 写入磁盘 redo log 之后返回成功,此时,真正的 ibd 磁盘文件并未更新。所以,Spanner 使用 Paxos 协议在多个副本之间同步 redo log,只要 redo log 没问题,多副本数据的最终一致性就没有问题。
由于分布式场景下写请求需要所有节点都完成才算完成,所以两阶段提交是必须存在的。单机架构下的事务,也是某一旦一条 SQL 执行出错,整个事务都是要回滚的嘛。多机架构下这个需求所需要的成本又大幅增加了,两阶段提交的流程是这样的:
- 告诉所有节点更新数据
- 收集所有节点的执行结果,如果有一台返回了失败,则再通知所有节点,取消执行该事务
这个简单模型拥有非常恐怖的理论故障概率:一旦在第一步执行成功后某台机器宕机,则集群直接卡死:大量节点会被锁住。
Spanner 使用 Paxos 化解了这个问题:只要 leader 节点的事务执行成功了,即向客户端返回成功,而后续数据的一致性则会基于prepare timestamp
和commit timestamp
加上 Paxos 算法来保证。
Spanner 使用时间戳来进行事务之间的 MVCC:为每一次数据的变化分配一个全球统一的时间戳。这么做的本质依然是“空间+时间”换时间,而且是拿过去的时间换现在的时间,特别像支持事后对焦的光场相机。
- 传统的单机 MVCC 是基于单机的原子性实现的事务顺序,再实现的事务隔离,属于即时判断。
- Spanner 基于 TrueTime 记录下了每行数据的更新时间,增加了每次写入的时间成本,同时也增加了存储空间。
- 在进行多节点事务同步时,就不需要再和拥有此行数据的所有节点进行网络通信,只依靠 TrueTime 就可以用 Paxos 算法直接进行数据合并:基于时间戳判断谁前谁后,属于事后判断。
Spanner 是一个强一致的全球数据库,那他放弃了什么呢?这个时候就需要 CAP 理论登场了。
一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
Google Spanner 数据库首先要保证的其实是分区容错性,这是“全球数据库”的基本要求,也最影响他们赚钱;然后是一致性,“强一致”是核心设计目标,也是 Spanner 的核心价值;谷歌放弃的是可用性(A),只有 majority available。
除此之外,为了“外部一致性”,即客户端看到的全局强一致性,谷歌为每一个事务增加了 2 倍的时钟延迟,换句话说就是增加了写操作的返回时间,这就是分布式系统的代价:目前平均 TrueTime 的延迟为 3.5ms,所以对 Spanner 的每一次写操作都需要增加 7ms 的等待时间。
大家应该都发现了,其实 Spanner 是通过给 Paxos 分布式共识算法加了一个“本地外挂” TrueTime 实现的海量数据的分布式管理,它实现全局强一致性的根本来源是Paxos
和TrueTime
。而在普通单机房部署的分布式系统中,不需要 GPS 授时和原子钟,直接搞一个时间同步服务就行。
Google Spanner 的推出代表着一个新时代到来了:基于分布式技术的 SQL 兼容数据库(NewSQL),而兼容到什么地步就看各家的水平了。
NewSQL 最大的特点就是使用非 B 树磁盘存储结构(一般为 LSM-Tree),在上面构筑一个兼容 SQL 常用语句和事务的兼容层,这样既可以享受大规模 LSM-Tree 集群带来的扩展性和高性能,也可以尽量少改动现有应用代码和开发习惯,把悲伤留给自己了属于是。
目前比较常见的 NewSQL 有 ClustrixDB、NuoDB、VoltDB,国内的 TiDB 和 OceanBase 也属于 NewSQL,但他们俩有本质区别,下面的番外篇会讨论。
在 NewSQL 时代之后,随着云计算的兴起,云上数据库突然成为了市场的宠儿,市场占有率迅速上涨。它们其实都是对 MySQL 的改造,并不属于 NewSQL 范畴,下面我们认识一下他们。
我实在是不想用“云原生”这个风口浪尖上的词来形容美丽的云上数据库们,它们就像 TCP/IP,简洁但有用。市场从来不会说谎,它们一定是有过人之处的。
2014 年 10 月,亚马逊发布了 Aurora 云上数据库,开创性地在云环境中将计算节点和存储节点分离:基于云上资源的特点,将计算节点 scale up(增配),将存储节点 scale out(增加节点),实现了极佳的性能/成本平衡。Aurora 将云上关系型数据库产品推向了一个新的高度。
Aurora 提出的计算与存储分离可以说是目前数据库领域最火的方向,但是它火的原因我相信大多数人都认识的不对:不是因为性能强,而是因为便宜。
十年前我在 SAE 实习的时候,中午大家一起吃饭,组长说云计算就是云安全,这句话当然说的很对。从这句话推开,我们很容易就能找到云计算真正的商业价值在哪里:传统托管式部署,哪些资源浪费的最多,哪里就是云计算的商业价值所在。
为了满足业务波动而多采购的 CPU 和内存,可能浪费了 50%;网络安全设备,可以说 95% 以上的资源都是浪费;高端存储,这个已经不能用资源浪费来形容了,而是被云计算颠覆了:云厂商用海量的多地域多机房内廉价的 x86 服务器里面的廉价磁盘,基于软件,构建出了超级便宜、多副本、高可用的存储,唯一的问题是性能不是太好。亚马逊 S3 和阿里云 OSS 就是最佳代表,可以说这类对象存储服务,其单价已经低于本地机房的 2.5 寸 SAS 机械磁盘了,更不要说本地机房还需要另外采购昂贵的存储控制器和 SAN 交换机了。
云数据库可以算是云服务厂商最重要的产品:受众足够广,成本足够低,性能足够高。这一点很像特斯拉汽车,时至今日,特斯拉依然在疯狂地想各种办法压低生产成本,虽然在降价,但是单车毛利依然维持在 30% 以上,是 BBA 的 2-3 倍。
Aurora 和 PolarDB 的核心价值是用一种低成本的方式,制造了一个 Oracle 要高成本才能做出来的软件和服务,这才是真的“创造价值”。
计算与存储分离并不是什么“高性能”技术,而是一种“低成本”技术:关系型数据的存储引擎 InnoDB 本身就是面向低性能的磁盘而设计的,而 CPU 和内存却是越快越好、越大越好,如果还把磁盘和 MySQL 进程部署在同一台物理机内,一定会造成磁盘性能的浪费。计算与存储分离的真正价值在于大幅降低了存储的成本。
虽然说这个架构的主要价值在于便宜,但是在技术上,它也是有优势的:
它显著降低了传统 MySQL 主从同步的延迟。传统架构下,无论是语句同步还是行同步,都要等到事务提交后,才能开始同步,这就必然带来很长时间的延迟,影响应用代码的编写。而计算和存储分离之后,基于 redo log 传递的主从同步就要快得多了,从 1-2s 降低到了 100ms 以下。由于主从延迟降低,集群中从节点的个数可以提升,总体性能可以达到更高。
看了上篇文章的人应该都知道,在更新数据时,主节点在完成了 redo log 写入,并对内存缓存 Buffer Pool 中相应的数据页进行修改之后,就会返回成功。这个内存缓存给 Aurora 从节点的数据更新造成了不小的影响:
- 主从节点之间只有 redo log 传递
- 从节点在拿到 redo log 之后,会刷新自己 Buffer Pool 中存在的数据页,其它不存在的页的信息会丢弃
- 这带来了两个问题:
- 从节点的客户端在主从不同步的一段时间内,读到的是旧数据,这个需要网关或者应用代码来处理
- 从节点的 Buffer Pool 有效性变差,命中率下降,引发性能下降
Aurora 的出现确实独具慧眼,但是也会被时代所局限。
在 Aurora 论文²中,开篇就提到 Instead, the bottleneck moves to the network between the database tier requesting I/Os and the storage tier that performs these I/Os
。Aurora 认为网络速度会成为云数据库的瓶颈,而在它研发的 2012-2013 年也确实如此,当时万兆网卡刚刚普及,CPU 单核性能也不足,软件也没有跟上,可以说速度比千兆网卡也快不了多少,所以亚马逊又搞了神奇的技术:存储节点具有自己将 redo log 写入 ibd 文件的能力。
由于这个神奇能力的存在,Aurora 的多机之间采用传输 redo log 的方式来同步数据,并用一种今天看起来不太靠谱的协议来保证最终一致性:consul 使用的那个 gossip 协议。由于 Aurora 采用六副本技术,所以每次写入都需要发起六次不怎么快的网络 IO,并且在其中 4 个成功以后才给客户端返回成功。Aurora 确实便宜,但是单节点的性能也确实捉鸡,这代表的就是写入性能差,进而限制了整个集群的性能上限。而且,经过比较长的时间(100ms)才能保证从从节点
上读到的数据是最新的,这会让主节点压力增大影响集群性能上限,或者让应用代码做长时间的等待,严重的会引起应用代码的执行逻辑变更,引入持久的技术债务。
那该怎么提升计算存储分离情况下的集群性能呢?我们看阿里云是怎么做的。
阿里云 RDS 集群的成本已经够低了,不需要再用计算存储分离技术降低成本了,而中国市场的用户,普遍需要高性能的 MySQL 数据库:ECS 价格太低了,如果不是运维方便和性能压力过大,谁愿意用你昂贵的数据库服务啊。
2015 年,PolarDB 开始研发,当时 25Gb RDMA 网络已经逐渐普及,所以阿里云将视角放在了网络速度之外:在 IO 速度没有瓶颈以后,基于内核提供的 syscall 所编写的旧代码将会成为新的性能瓶颈。
站在 2023 年初回头看,阿里云的判断是非常准确的。
由于所有节点都使用了同一块“逻辑磁盘”,所以双主可写
想都不要想,一个计算存储分离的数据库集群的性能上限就是主节点的写入性能上限
。(Aurora 有多主可写数据库,对 ID 进行自动切分,使用时有一堆限制;PolarDB 也有多主可写数据库,但是更绝:每个库/表只支持绑定到一个可写节点,感情就是对多个数据库做了个逻辑聚合,还不如中间件呢。)
在主节点不接受读的情况下,主节点只承接写入操作,以及和写入操作在同一个会话 session 中的后续的读请求。
那 PolarDB 是如何提升主节点性能的呢?
主从之间并不是依靠纯属 redo log 来同步数据的,而是直接共享同一个 ibd 文件,即真正的共享磁盘。而且,基于块设备的 Raft 算法也比基于文件的 gossip 协议要快很多。
虽然对 redo log 的解析这一步在 Aurora 那边是存储做的,PolarDB 这边是主节点做的,看似增加了 CPU 消耗,但是这并不是真正的性能瓶颈所在,真正的瓶颈是网络栈和 UNIX 进程模型。看过我《性能之殇》系列文章的人应该都比较熟悉了,这是老生常谈了。那 PolarDB 是怎么优化的呢?
- 跳过 TCP/IP 网络栈,直接使用 RDMA 网络从存储节点读取数据,延迟暴降
- 跳过 kernel 的线程调度,自行开发绑定 CPU 核心的状态机,采用非阻塞 IO,在 CPU 占用下降的情况下,延迟进一步降低
ParallelRaft 协议让 Aurora 那边需要执行六次的网络 IO 变成了一次:只需要向 leader 节点写入成功,剩下的数据同步由 Raft 算法来执行,这和 Google Spanner 的两阶段提交优化是一个思路。
原始的 Raft 协议确实逻辑完备,实现简单,就是一个一个地协商太慢了。ParallelRaft 让收敛协商能够并行起来,加速 redo log 落入 ibd 文件的过程。
基于共享存储的低延迟优势,PolarDB 主从之间使用共享存储来同步 redo log 以刷新缓存,这一点逻辑上和 Aurora 一致,但是实际表现比较好,我实测主从同步时间在 20~70ms 范围内。
RDMA 存储比本地存储更快,因为减少了计算和存储争抢中断的问题:IO 这个 CPU 不擅长的东西完全卸载给了 RDMA 网卡。同配置下 PolarDB 比标准 MySQL 的性能要高几倍。
在各种实测里面,PolarDB 在相同规格下对其他的云上数据库都拥有 2 倍的性能优势,但是它基于 RDMA 存储的特点也让它付出了两个代价:1. 硬件成本高昂 2. 扩展性有上限。
是不是感觉很熟悉?Shared-Disk 的代表 Oracle RAC 也有这两个缺点。不知道大家有没有发现,PolarDB 就是云时代的 RAC 数据库:看起来是 Shared-Disk,其实是共享缓存让他们的性能变的超强。
各代分布式数据库的兼容性/扩展性对比一个分布式系统中,不可能完全满足①一致性、②可用性、③分区容错性。我们以一个两地三中心的数据库为例:
- 一致性:同一个时刻发送到三个机房的同一个读请求返回的数据必须一致(强一致读),而且磁盘上的数据也必须在一段时间后变的完全逻辑一致(最终一致)
- 可用性:一定比例的机器宕机,其它未宕机服务器必须能够响应客户端的请求(必须是正确格式的成功或失败),这个比例的大小就是可用性级别
- 分区容错性:一个集群由于通信故障分裂成两个集群后,不能变成两个数据不一致的集群(脑裂),对外必须依然表现为一个逻辑集群
在一个分布式数据库系统中,到底什么是可以放弃的呢?我觉得可以从分布式系统带来了什么优势这个问题开始思考。
相比于单体系统,一个分布式的数据库,在一致性上进步了吗?完全没有。在可用性上进步了吗?进步了很多。在分区容错性上呢?单体系统没有分区,不需要容错。所以,结论已经一目了然了:
①和③都是分布式系统带来的新问题,只有②是进步,那就取长补短,选择牺牲可用性来解决自己引发的一致性和分区容错性两个新问题。这也正是目前常见分布式数据库系统的标准做法。
TiDB 和 OceanBase 是目前中国 NewSQL 数据库的绝代双骄,争论一直不绝于耳。
TiDB 是承袭 Spanner 思想的 NewSQL,对 MySQL 的兼容性一般,基于key+版本号
的事务控制也比较弱,据说性能比较好,特别是写入性能。
OceanBase 是基于 Shared-Nothing 思想原生开发的分区存储数据库,其每个节点都支持完整的 SQL 查询,相互之间无需频繁通信。OceanBase 还支持多租户隔离,这明显就是为了云服务准备的(无论是公有云还是私有云),和绝大多数企业无关。另外,OceanBase 对于 MySQL 的兼容性也几乎是 NewSQL 里面最高的,毕竟它需要支持支付宝的真实业务,兼容性是硬性要求,业务屎山可没人移得动 (づ。◕‿‿◕。)づ
下面我们详细对比一下两者的设计思路。
我画的架构图如下:
上图中的“SQL 层”就是解析 SQL 语句并将其转化为 KV 命令的一层,是无状态的,下面的存储层才是核心,它叫 TiKV。
TiKV 官方原理图如下:
TiKV 是 TiDB 的核心组件,一致性和事务隔离都是基于它的能力得以实现的。每个 TiKV 拥有两个独立的 RocksDB 实例,一个用于存储 Raft Log,另一个用于存储用户数据和多版本隔离数据(基于key+版本号
实现),从这里可以看出,TiDB 的存储存在大量冗余,所以 TiDB 的测试性能才会如此的高,符合空间换时间的基本原理。
和 TiKV 并列的还有一个 TiFlash 列存储引擎,是为了 OLAP 在线数据分析用的,我们在此不做详细讨论。
TiKV 数据分片除此之外,TiKV 还发明了一层虚拟的“分片”(Region),将数据切分成 96MB~144MB 的多个分片,并且用 Raft 算法将其分散到多个节点上存储。注意,在 TiKV 内部存储用户数据的那个 RocksDB 内部,多个分片是致密存储的,分片之间并没有逻辑关系。
TiDB 的实现风格比较狂野,所以不兼容的部分比较多:
TiDB 和 MySQL 不兼容的部分TiDB 放弃了新不可能三角中的事务隔离,和 Spanner 一样放弃了 CAP 理论中的“完全可用性”:一旦出现脑裂,就会出现意外的返回结果(如超时),因为 TiDB 选择了保证一致性:如果无法达到数据强一致,就要停止服务。
我们以最新的 OceanBase 4.0 版本的架构为目标进行讨论。
TiDB 底层数据叫分片,那 OceanBase 为什么叫分区呢?因为分片的意思只是数据被分开了(本身 KV 数据之间也没关系),但分区表示的是分区内部的数据之间是有联系的:OceanBase 的每个节点本身,依然是一个关系型数据库,拥有自己的 SQL 引擎、存储引擎和事务引擎。
OceanBase 在建表时就需要设定数据分区规则,之后每一行数据都属于且仅属于某个分区。在数据插入和查询的时候,需要找到这一行数据所在的区,进行针对性地路由。这和第一代分布式——中间件的思想一致。这么做相当于简单地并行执行多条 SQL,以数据切分和数据聚合为代价,让数据库并行起来。而这个数据切分和数据聚合的代价,可以很小,也可以很大,需要 OceanBase 进行精细的性能优化,我们下面还会说到。
分区之间,通过 Multi-Paxos 协议来同步数据:每一个逻辑分区都会有多个副本分布在多台机器上,只有其中一个副本会成为 leader,并接受写请求。这里的架构和 PolarDB 一样了,此时,客户端的一致性读需要网关(OBProxy)来判断,主从之间的同步是有可感知的延迟的。
官方存储架构图OceanBase 数据库的存储引擎基于 LSM Tree 架构,将数据分为静态基线数据(放在 SSTable 中)和动态增量数据(放在 MemTable 中)两部分,其中 SSTable 是只读的,一旦生成就不再被修改,存储于磁盘;MemTable 支持读写,存储于内存。数据库 DML 操作插入、更新、删除等首先写入 MemTable,等到 MemTable 达到一定大小时转储到磁盘成为 SSTable。在进行查询时,需要分别对 SSTable 和 MemTable 进行查询,并将查询结果进行归并,返回给 SQL 层归并后的查询结果。同时在内存实现了 Block Cache 和 Row cache,来避免对基线数据的随机读。
当内存的增量数据达到一定规模的时候,会触发增量数据和基线数据的合并,把增量数据落盘。同时每天晚上的空闲时刻,系统也会自动每日合并。
OceanBase 数据库本质上是一个基线加增量的存储引擎,在保持 LSM-Tree 架构优点的同时也借鉴了部分传统关系数据库存储引擎的优点。
以上是官方描述,我本来想简化一下,读了一遍觉得还是放原文吧,原文描述的就非常的清晰精炼了:OceanBase 用内存 B+ 树和磁盘 LSM-Tree 共同构成了数据读写体系,和上一篇文章中的 InnoDB 是多么像啊!只是 OceanBase 做的更细:跟 TiDB 相比,就像是在 TiKV 上面加了一层 Buffer Pool 一样。
还有一个细节:OceanBase 除了记录日志(Redo Log)并修改内存缓存(MemTable)之外,只要内存充足,白天 OceanBase 不会主动将内存缓存中的数据刷洗到 SSTable 里的,官方更推荐每天凌晨定时刷洗。这是什么思想?可以说是空间(内存)换时间,也可以说是拿未来的时间换现在的时间。
从基础电气属性上讲,磁盘一定是慢的,内存一定是快的,所以在数据量大于机器的内存容量时,各种数据库的性能差别可以聚焦到一个点上:内存利用效率,即热数据命中内存缓存的概率。
为了提升缓存命中率,OceanBase 设计了很多层内存缓存,尽全力避免了对磁盘的随机读取,只让 LSM-Tree 的磁盘承担它擅长的连续读任务,包子有肉不在褶上,商用环境中捶打过的软件就是不一样,功夫都在细节里:
- BloomFilter Cache:布隆过滤器缓存
- MemTable:同时使用 B+ 树和 HashTable 作为内存引擎,分别处理不同的场景
- Row Cache:存储 Get/MultiGet 查询的结果
- Block Index Cache:当需要访问某个宏块的微块时,提前装载这个宏块的微块索引
- Block Cache:像 Buffer Pool 一样缓存数据块(InnoDB 页)
- Fuse Row Cache:在 LSM-Tree 架构中, 同一行的修改可能存在于不同的 SSTable 中,在查询时需要对各个 SSTable 查询的结果进行熔合,对于熔合结果缓存可以更大幅度地支持热点行查询
- Partition Location Cache:用于缓存 Partition 的位置信息,帮助对一个查询进行路由
- Schema Cache:缓存数据表的元信息,用于执行计划的生成以及后续的查询
- Clog Cache:缓存 clog 数据,用于加速某些情况下 Paxos 日志的拉取
为了极致的性能,OceanBase 直接取消了 MySQL 中“后台进程每秒将 redo log 刷写到 ibd 文件”这一步,等于放大了集群宕机重启后恢复数据的时间(重启后需要大量的时间和磁盘 IO 将 redo log 刷写入磁盘),然后把这件事放到半夜去做:
当内存的增量数据达到一定规模的时候,会触发增量数据和基线数据的合并,把增量数据落盘。同时每天晚上的空闲时刻,系统也会自动每日合并。
把一整天的新增和修改的数据全部放到内存里,相当于直接变身成了内存数据库(还会用 B 树和 Hashtable 存两份),确实是一种终极的性能优化手段,OceanBase 真有你的。
传统的中间件 Sharding 技术中,也会做一些并行查询,但是他们做的都是纯客户端的查询:proxy 作为标准客户端,分别从多台机器拿到数据之后,用自己的内存进行数据处理,这个逻辑非常清晰,但有两个问题:1. 只能做简单的并行和聚合,复杂的做不了 2. 后端数据库相互之间无通信,没有很好地利用资源,总响应时间很长。
OceanBase 让一切尽可能地并行起来了:在某台机器上的 proxy(OBServer) 接到请求以后,它会担任协调者的角色,将任务并行地分发到多个其他的 OBServer 上执行;同时,将多个子计划划分到各个节点上以后,会在各节点之间建立 M*N 个网络通信 channel,并行地传输信息;此外,OceanBase 还对传统数据库的执行计划优化做了详细的拆分,对特定的关键词一个一个地做分布式优化,才最终达成了地表最强的成就。
OceanBase 和 MySQL 不兼容的部分由于数据是分区的,所以当脑裂时,两个大脑的数据肯定已经不完整了,相当于两万行的表只剩一万行数据可以进行查询和更新,此时,如果 OceanBase 梗着脖子非要追求数据强一致,也是可以让所有的 OBProxy 拒绝服务的,但是 OceanBase 选择了继续苟着:互联网公司嘛,能实现最终一致性就行了,要啥自行车。
OceanBase 放弃了 CAP 和新不可能三角中的一致性,只能做到最终一致性:为了事务隔离和性能,哥忍了。
其实,不追求强一致和我们下一篇《站在地球表面》中的终极高并发架构在思想上是一致的,我想这就是经历过大规模生产应用的数据库,被现实世界毒打过后的痕迹吧。
其实,分布式数据库根本就轮不到你来选:应用准备好了吗?有足够的研发资源吗?性能问题已经大到压倒其他需求了吗?
如果你有一个正在成长的业务,影响最小、成本最低的方案就是选择 Aurora/PolarDB 这种高兼容性数据库,等到这类云数据库的主节点达到性能上限了,再对应用做逐步改造,滚动式地替换每个部分的数据库依赖。
如果压力大到必须换分布式数据库技术方案了,再看看你能获得什么样的分布式数据库呢?无非是在哪个云平台就用哪家呗。
Shared-Nothing 只是一种思想,并不是一种明确的数据库架构,它非常笼统,只是描述了一种状态。在这里我们简单讨论一下 Shared-Nothing。
Shared-Nothing 描述的是一种分布式数据库的运行状态:两台物理机,除了网络通信之外,不进行任何资源共享,CPU、内存、磁盘都是独立的。这样,整个系统的理论性能就可以达到单机的二倍。
怎么理解 Shared-Nothing 思想呢?把它和 Shared-Disk 放到一起就明白了:
Shared-Disk:多台机器通过共享 SAN 磁盘的方式协同工作,让系统整体性能突破单机的极限。Oracle RAC 是这个架构的佼佼者,不过它的成功并不在于磁盘,而在于它的分布式锁(CACHE FUSION):RAC 利用时间戳和分布式锁实现了分布式事务和多台机器同时可写,大幅提升了集群的性能。注意,时间戳在这里又出现了。CACHE FUSION 其实已经可以被称作 Shared-Memory 了。感兴趣的可以自己了解,我们不再深入。
21 世纪初,Oracle 推出了 Shared-Disk 的 RAC,IBM 推出了 Shared-Nothing 的 DB2 ICE。十年后,Oracle RAC 发展的如火如荼,而 DB2 ICE 已经消失在了历史的长河中。
但是,2012 年 Google 发布了 Spanner 论文,在非常成熟的世界上最大规模的 KV 数据库之上,构建 SQL 层,实现持久化、事务和多版本并发控制,扛起了 Shared-Nothing 技术方向的大旗,直到今天。
十年前我在新浪云(SAE)实习的时候,听过一个关于 MongoDB 的技术小故事:当时,SAE 的 KV 服务是使用 MongoDB 实现的,在规模大到一定程度以后,性能会突然下降,SAE 自己解决不了这个问题,就给 MongoDB 开发组的各位大哥买机票请他们到北京理想国际大厦 17 层现场来帮忙,研究了几天,MongoDB 开发组的人说:你们换技术吧,MongoDB 解决不了你们这个规模的问题,然后 SAE 的 KV 就更换技术方案来实现了。
也是在 SAE,我坐在厕所附近临过道的工位(上厕所很方便),某天早上刚上班,我亲眼看到 SAE 的一名 MySQL DBA 从厕所里出来后,晕倒在我面前,砸烂了一个大花盆。数据库作为系统架构中最重要的那个单点的残酷,可见一斑。
与其将列存储认定为数据库的一种,我倒觉得它更应该被称作一种思想:观察数据到底是被如何读取,并加以针对性地优化。
列存储有点像第一性原理在数据库领域的应用:不被现实世界所束缚,没有屈服于 B 树和它亲戚们的淫威,勇敢地向更底层看去,思考着在我们大量读取数据时,数据怎样组织才能读的更快。
在读取一行数据时,显然 B+ 树的效率无人能及,但是当我们需要读取 100 万行数据中的某一列时,B+ 树就需要把这 100 万行数据全部载入内存:每次将一页 16KB 载入内存,循环这一页内的 14 行数据,把这个特定的字段复制出来;重复执行这个操作 71429 次,才能得到我们想要的结果。这显然是 B+ 树非常不擅长的需求。
而列存储将数据基于行的排布翻转过来了:所有数据基于列,致密地排列在磁盘上,这样对某一列的读取就变成了磁盘顺序读,即便是机械磁盘,顺序读也非常快。
clickhouse 推荐使用尽量多的 CPU 核心,对单核性能无要求,我拿 E5-V2 旧服务器测过,速度确实非常惊人,8000 万行的表,查询起来不仅比 MySQL 快,比 Hadoop 也快特别多。
在中国,我们现在有下面两种方案可以选择:
- OceanBase 已经蝉联 TPC-C 数年的全球冠军了,每分钟可以处理 7.07 亿个订单,每秒订单数都已经过千万了,更不要说 QPS 500 万了,所以,如果你用 OceanBase,你的百万 QPS 的高并发系统已经搭建完成了! :-D
- 如果你用阿里云,那 1 主 4 从,88 vCore 710 GB * 5 个节点的 PolarDB 集群可以跑到大约 200 万 QPS³。那离 500 万还有不小的距离呢,不要着急,我们下篇文章解决这个问题。
接下来就是本系列最后一篇文章了:我们不仅要用架构顶住五百万数据库 QPS,还会找出一个哲学♂办法,打造能够服务全人类的系统。
- Google Spanner 论文(中文版) https://ying-zhang.github.io/time/2013-Spanner-cn.pdf
- 亚马逊 Aurora 论文 https://web.stanford.edu/class/cs245/readings/aurora.pdf
- 复盘:我在真实场景下对几款主流云原生数据库进行极限性能压测的一次总结 https://cloud.tencent.com/developer/article/2066823