本次直播课程由京东云高级总监郭理靖数据库基础入手,从实践应用出发,深度解析从单机数据库到分布式数据库的技术发展与迭代,同时并理论结合实际为大家讲述企业选择数据库服务的金科玉律以及京东云针对此方面的应用实践等干货内容。
此次课程的分享内容更多关于数据库服务的演变史,重点讲述单机到分布式的变化从何而来以及从单机数据库如何进行改进才可达成分布式数据库的服务。
以下是郭理靖老师分享的全部内容,希望给各位开发者带来更多帮助:
从单机到分布式,数据库服务的演变史
— 京东云高级总监 郭理靖—
从Redis到MongoDB,最初的产生都很简单
很多时候,一门技术甚至一种产品,最初的产生往往都来源非常简单的想法,例如2019年,我们看到DB-Engines Ranking中有两个比较熟悉的身影,MongoDB和Redis。
很有意思的是,MongoDB属于DocumentDB。如果我们调头去看DocumentDB-Engines Ranking的话,可以发现MongoDB是排在第一位,而且是以绝对性的优势“吊打”第二位到第十位(第二名只有56分),整个DB-Engines中,Oracle和MySQL的分数靠得特别近;但是在DocumentDB中,MongoDB是400多分,但第二名可能就只有100多分。
同样,Redis在K/V的领域,分数也特别靠前,它146分,第三名是MemcacheD(第二名为Amazon DynamoDB,为56分) 只有28.7分。
所以这两个数据库在自己的专业领域中都是排名特别靠前的两个数据库,而且彼此出现的时间都不算特别长,不到十年:Redis出现的比MongoDB早一些,Redis是2009年,MongoDB差不多也是在2009年左右出现的。
为什么Redis会出现,并且成为K/VDB排名第一的一个数据库呢?
其实,在Redis出现之前已经有一个K/V的数据库叫Memcached,早在2003年发布,最初用Perl写了一个版本,最后用C语言重写了。在Redis没有出现之前,Memcached一直是K/V数据库的领头,为什么在2009年,当Redis出现时就飞快地代替了Memcached,直至今日,大部分的缓存我们都用Redis。甚至刚入行的同学们根本没有听说过还有Memcached这样的数据库,这个过程中Redis到底做了什么事情?
事情比较简单,从设计理念出发,就是把原先Memcached的存储结构进行了改变。之前是一个Key对应一个String,然后Redis把String变化成一个可以结构化的Structured storage,也就是说Redis可以支持一些结构化的数据,包括List、sorted set、hashes等 。
如此小的变化为什么能够让它取得这么大的优势?关于此还是要回溯下Memcached。
假设我们自己要设计一个缓存系统的话,设计目标与Memcached一样,过程中就需要设计一个Key String的缓存系统,那么在这个缓存系统上到底能够提供哪些服务呢?更进一步来说,当存储对象是String,针对此我们可以做到哪些操作?
可以想见,在一个编程语言中,以以C++或者Java为例,一个字符串可以set一个字符串,还可以get一个字符串,当然也可以delete一个字符串。此外,对于字符串我们还可以做一些其他操作,例如append、prepend等,也就是字符串的尾巴和头部加些内容,甚至可以将整个字符串替换掉。当然,也可以做insert、erase,还有trim这种操作以及find、substr等。
大家在设计过程中不妨想一想,为什么在Memcached中不支持insert或find、substr这样的操作呢?
这其实是一个很有意思的话题。理论上Memcached可以支持这些操作,包括针对Memcached的源代码进行修改之后,也可以支持这些函数和调用,但之所以选择不支持可能更多还是效率方面的考量。
例如在Memcached中支持substr或者find的操作的过程中,却浪费了Memcached服务器的性能。因为find可以把整个字符串转移到本地再做find,substr也是一样,其实都归功于性能。
当然Memcached也可以提供更高级的一些操作,例如add或者一些gets操作,还可以做incr/decr,对一个数字进行原子性加减,都可以。如果去研究Memcached整个release history的话,它是2003年开始发布的一个版本,前期支持的工作是非常有限的,所以后续的高级功能也是慢慢逐渐加进去的。
再看一下Redis,可以支持的数据结构有很多,包括Strings、Lists、Sets、Sorted sets、Hashes、Bit arrays,还有HyperLogLogs、streams,streams是5.0,最新的版本甚至可以支持一些时序性的数据。
纵观整个迭代历程,Redis最初的简单想法就是能不能将Strings的结构和存储结构,让其支持的多一些。从这个想法出发,才会逐渐发展成慢慢去支持越来越多的数据结构,支持越来越多操作类型的局面。
以C++为例,一个List操作支持pop/push,也可以支持在List中间去Insert/erase。对于Redis而言,除了常见的操作之外,我看了一下Redis整体的release history,最开始的时候也支持的是一些比较简单的操作,例如pop/push;随着时间的发展,支持越来越多的高级功能,都是建立在普通的操作上。
比方说BRPOPLPUSH这个功能,就是从一个List中POP一个数据出来,插入到另外一个List中去,这算是比较高级的功能体现了,但这个也是在用户发现其需要此种场景之后才去增加的事项。
再来探究下MongoDB。MongoDB的“工作”其实与Redis有点类似,可以认为Redis就是利用K/V结构,将Strings换成了一个类似于JSON的类型,JSON中可以支持的数据结构也非常多。
MongoDB所体现的功能相似度很高。以前在设计数据库的时候,如果是关系型数据库,一开始就需要将Schema设计好,然后去做一个DDL操作。当需要增加一列或者减少一列的时候,以上的操作并不是很方便;特别在表单很庞大的情况下,操作时间会很长。
随着业务的发展,是否可以用一个JSON来代替原型MySQL Schema?以这样一种简单的想法出发,如今尽管MySQL或者关系型数据库还是以Schema为核心,但是MongoDB就以Document,也就是JSON这个格式为核心去构建整个数据库的功能和生态。
可以看到,MySQL的常见SQL语句在MongoDB中命令长度不太一样,因为所有的命令都与JSON的格式有关,包括底层的存储结构也会因为要保存JSON格式而做出很大变化。
这部分集中想要表达的观点就是很多数据库以及技术产品的演进,其实都是有脉络可寻的,主要就是设定解决问题的目标进而完成底层数据结构的修改,其中底层结构确定之后还会在一定程度上影响产品功能,例如MongoDB在很长一段时间内都不支持事务,不是不想去尝试支持,而是取决于底层存储结构的存储引擎,无法支持事务。
同样的道理,探究Memcached和Redis的内存管理设计,也会发现,Memcached的开发者或者说社区,也想去支持像Redis一样的功能,但事实上由于底层的存储结构注定无法提供此类服务。
为什么会有分布式?主要还是由于处在信息爆炸的时代,业务量逐渐庞大导致单机数据库并不能更好满足需求,自然要寻找一些可行的解决方案。
这里的解决方案可以分为两种,一类相当于表层的解决方案,操作起来比较简单,其中涉及客户端的方案以及统一代理的方案等,例如proxy的方案。
相比之下,底层的方案可以分为三类。第一类是分布式块存储方案,第二类是计算与存储分离方案,第三类就是要另起炉灶了。
首先客户端方案,可以以友商开源的项目称为TDDL为代表,方案思路较为清晰。如果我们自身需要做一个分布式系统的话,将分布式解决方案放入客户端内,最重要的就是“告诉”客户端SQL语句应该链接到背后哪个数据库,毕竟不同的数据存储在不同的机器当中。对于客户端的方案,比较重要的就是与后端的一些连接,以及如何解析这个SQL语句。
第二种方案就是Proxy方案。Proxy方案相对客户端方案而言,开发难度稍微高一些,但思路上还是非常一致的。举例说明,一张用户表,用户表的组件是ID,我们对ID的分布式key进行分布,查询具体某一个用户ID的时候其实就有一个Proxy,如果说查询这个用户ID为0,进而就知道0是存在于后台的哪一台MySQL。
所以对于Proxy而言,非常重要的一点还是需要去解析具体的SQL语句,这个过程中必须需要有一个自己的路由,就可以完成识别。
客户端的方案与Proxy方案的区别在哪里?区别在于客户端的方案开发起来相对比较简单一点,在JDBC中或者要求客户端调用时,客户端可以不解析SQL语句,完成导向很容易。
对于Proxy方案,它需要兼容原有的MySQL协议,怎么建立MySQL连接,怎么与后端保持连接,这个难度比较大。但相对而言,Proxy方案的限制也比较多,不管是客户端的表层方案,还是Proxy的表层方案,它们对SQL的使用要求都是比较高的,例如一些join是比较难支持的,而且要与业务方达成一致。
此外,无论是客户端方案还是Proxy方案,整体架构还是比较类似的。例如Proxy方案,它会涉及前端如何与客户端进行连接以及后端的Backend connection,还要与后端真正提供MySQL服务的保持连接,这是两个连接的管理。
经过了解,整个Proxy一般的设计都会采取无状态,因为路由信息都是保存在Proxy本机的;但路由方面肯定有一个相对的路由管理中心去分发和更改控制。业界大部分Proxy设计,大家都会发现,Proxy代理的方案其架构设计非常相像,区别在于工程质量不同而已。
另外,对于Proxy方案的缺点就是怎么去做一个分布式事务,没办法保证次序。通常在一个分布式的情况下,其实很难保证每个节点上的事务执行是按照我们想像中的次序去做。
讲完表层的解决方案之后,我们还需要讲讲比较深层次的底层解决方案。从表层出发,不管是客户端方案还是Proxy方案,都并没有去改变底层MySQL的整体协议,也没有改变数据库原生服务,可以认为根本就没有让MySQL底层代码发生变动。
如今可知晓的底层解决方案主要有三个,第一个方案是块存储方案,包括SAN解决方案,涉及云端就是云硬盘解决方案,都是同一类型的。
但如果真正要做一个数据库的解决方案,目前的主流思路还是计算与存储分离,为什么会出现计算与存储分离呢?主要原因有以下几点:现在的网络延时一直在下降,带宽一直在增长,从我个人的工作经验来看,未来可能还有更大的网络带宽出现;但与此同时,磁盘IO的吞吐量并没有增长得像网络这么快。
此外,存储的虚拟化有利于成本下降,分布式存储有利于提高IOPS。如果在一个机器上做Raid的话,它的性能会比单个硬盘要好很多;如果采用分布式系统写入,它的IOPS其实会更高。
目前业界比较有名的几个计算与存储分离的解决方案,最著名的就是亚马逊的Aurora。
Aurora为什么选择计算存储分离?关于这一点,还需要探究当时亚马逊做RDS的解决方案如何。亚马逊RDS,就是它们的Realtional Database Service,整体数据存在local disk/EBS上面。
我们可以看到,整个IO的写入链条是比较长的,要把数据落盘,binlog落盘,redo log落盘,而且整个binlog两边要同步,最后还要把EBS的数据适当地备份到云存储中去。(这个只是备份,与实时写入无关)。
由于链路比较长,而且包括整个EBS的实现中,EBS就是个分布式的快存储,本质上也是多副本的。在多副本的情况下,EBS在远端也要做整个数据的同步,所以Aurora的设计目标就是思考能不能把存储与计算做一些相应的分离。出发点就是数据库存储引擎能不能与分布式块存储融合在一块,融合成一个新的存储引擎。
另外一个方案是F1/Spanner的方案,思路又是什么呢?
做一个分布式系统,如果去兼容MySQL比较麻烦的话,那是不是可以另起炉灶,做一个新的分布式系统。
它的思路很简单,就是重新去改写一些SQL,所以像F1/Spanner并不兼容MySQL的一些协议时,就会引入新的分布式事务管理,把SQL都拆解成K/V的一些操作结构,对这种方案而言,SQL的兼容性是个长期耗时而且是无限接近的一个过程,主要需兼容已有的生态会比较费力。但如果是一个新业务,用这种分布式方案就没有问题;如果是个旧业务,有自己成熟的SQL,想用它就比较困难了。
可以看到对一个MySQL服务而言,其实有两部分组成,一部分是Server,一部分Storage。Storage标准的架构是redo log加个data,这是InnoDB的一个结构,如今InnoDB已经成为MySQL的一个标准,大家都是按照这种方式去做的。
关于主从同步,是用binlog复制的方式,把binlog复制到Slave这边,Slave对binlog重放,然后Slave就有完整数据,这是传统的一个数据库组成模式。
对于MySQL而言,它的数据流向会是什么样子的?第一步,如果针对一个事务或者一个SQL语句,它会写很多redo log。第二步,会写到binlog中,binlog以后会做半同步,会同步到Slave中,返回一个信息,这时候可以进行一个整体的commit。
对于整个数据流向的一个示意图,主从之间用了整体的binlog复制方式去做。如果想做一个分布式系统的话,而且出发点是不能够将这些一样的数据放在同一个分布式系统中,需要做哪些事情?
我们发现,如果计算和存储分离之后,把所有的存储都放在一个分布式存储系统中去,master与slave读的是同一个数据,其实就不需要binlog了,这个是比较容易理解的。binlog用来传输具体的数据,因为数据都放在一起。
在于分布式存储的存储空间会相对大一些;另外一点,如果说要增加一个新的slave,以前在MySQL的主从复制中增加一个新的slave,通常怎么做?
要么就新建一个空白的slave,慢慢从组合开始同步数据,总而言之就是利用binlog同步数据;但如果整个数据量比较大的话,建立一个新的从时间会非常长,或者说根据备份去重建一个新的从,进而拷贝数据库备份。在这个基础上再去追新日志,无论如何增加一个节点,时间应该还是要以分钟计,至少以10分钟为单位。
但在计算与存储分离之后,新建一个slave时间就非常快了,而且备份数据的时候会快很多。因为针对传统主从系统做备份,其实都要去做一个文件系统的备份。做文件系统快照也行,用mysql dump也行,需要把本地文件传到云端存储;在分布式存储系统中,可以把这个任务转移给底层系统去做,当然数据的强一致还要靠底层存储去保证。
如果把计算、存储分离之后,整个高可用切换会是什么样子的?对于以前的MySQL而言,一个主从切换其实比较简单,因为主跟从是两个独立的存储和计算,之间没有关联关系,主切换到从,最多就是binlog还没有回放完;但对于整个计算存储分离之后,它的变化还是有一些的。
例如第一个变化,我们可以看到对于传统数据库而言,它的Redo日志是一个循环的文件系统,可以是三个Redo日志循环使用;如果在一个存储分离,计算分离的环境,它的Redo日志基本会设计成按编号去排序的情况。
等主从切换之后,log buffer和buffer pool,也许需要一点时间去重建它的两个buffer pool。因为在传统意义上,传统的MySQL主从切换都需要去根据日志稍微重建一下buffer pool。
对于备份而言,基本上变化也比较大,整个备份会基于底层的块存储:一部分可以去备份整个page页的数据;另外一部分,需要去做Redo日志的增量备份,这样很快能完成一个数据库的备份。
其实在云端去做一个数据库的备份会比自己搭建两台物理机去做备份更快,为什么?
因为在云端分布式系统中,做备份的时候不是一台机器在备份,因为存储在不同的机器上。 对于一个新建的slave而言,如果我们在云端计算、存储分离的情况下去新建一个slave的话,新建slave的速度也会比传统的方式快非常多,因为整个数据块是已经存在了,直接去读就行,不需要重新再复制一份。
但是这种情况也带来不少问题。因为底层的文件系统其备份数是有限的,副本数也是有限的,深层次的原因是因为底层的分布式文件系统的IOPS是有限的,对于一个主从关系,像传统模式下的MySQL,其实并没有这种限制。
最后总结一下这几个方案的优缺点:
比如客户端方案,它的优点是可以接多种数据源,只要数据库是兼容JDBC的,都可以去对接,而且性能上比较好。但提高性能的同时也带来另外一个问题,连接数可能会比较多,因为客户端非常多,每个客户端都与MySQL进行连接的话,MySQL连接会特别多。
缺点也非常明显,如果公司中只以Java为主,那没问题,开发一个jar包,大家用起来会非常熟悉;如果说一个公司用Python、C++,还用Go,每个语言都要写一个客户端,其实是非常痛苦的。另外客户端发布了,SDK发布之后升级起来也非常麻烦。
所以绝大部分的厂商会采用Proxy的方案,因为Proxy的方案容易管理,升级也比较容易,而且语法基本兼容,不存在多语言SDK的问题。兼容了MySQL的一些语法之后,传统的MySQL客户端,各种语言的SDK都能用,所有MySQL第三方开发的SDK也都能用,缺点在于不支持事务,或者对事务的支持是有限的。
对于计算与存理分离方案而言,它的优点非常明显且性价比特别高,为什么?
云端购买云数据库的时候,很多时候性能与购买有关系。例如买个1C2G10GB的小型数据库,OPS可能只有几百上千;但对于真正的计算与存储分离的方案而言,开始购买的类型可能是比较小的,购买的规格可能也比较小,但是可以达到很高的IOPS性能,数据库性能并不会受限于你购买的规格。
关于缺点,还是整体的存储空间有限,可能最大只能支持64T或者128T,因为每个数据库的分布式存储中其副本数是有限的,只读节点可能受限于,只能有15个或者14个,但这个都不要紧。我相信对于绝大多数的用户而言,这个还是足够用的。另外一个缺点是开发难度,但这个缺点只是开发厂商的痛点,与用户没有关系。
对于另起炉灶,像Spanner/TiDB这种数据库方案,它的可扩展性就比较高。因为对于像Aurora这种计算与存储分离的数据库而言存储空间毕竟是有限的,但对这种数据库,它的存储空间可以被认为是无限扩展的,可以支持一个上P的数据库也没问题。
依旧谈谈缺点。就是很难去兼容现有的一些SQL语法,是一个比较大的挑战。例如TiDB是国产比较优秀的数据库了,也号称对MySQL的语法兼容是比较完美的,但事实上还是很多语句是没法做到兼容的,不是不想,是因为做起来特别难!因为底层的数据结构决定了一些功能,也决定了要去兼容一些SQL语句其代价是非常大的。
课程问答
为什么云存储有容量限制?技术上的根本原因是什么?
理论上讲我们做分布式存储的时候,既然讲它是个分布式存储,为什么它还有空间上的限制?包括我们做快存储,如果大家用过云都会用像亚马逊的EBS,包括京东云的云硬盘盘,这几个问题的本质是一样的,为什么快存储和计算与存储分离的数据库它的空间都是有容量限制的?我们为什么没法去申请一个无限大的云硬盘?这个问题可以转化成另外一个问题,为什么我们在云厂商里面没法去申请一个超大空间的云硬盘?我们都没有看到过哪个厂商提供10个PB,甚至说100PB的云硬盘,这是为什么?这里面还是要回到我最开始讲的,就是说你的功能设计目标和你底层存储结构决定了你的产品功能,决定了你产品的限制。每一家厂商的底层实现的原理都不太一样,但是结构都还是比较相同的。如果说我们要提供一个非常大的存储空间的话,它的meta信息管理会受一个很大的限制。同时,为什么不会提供超大规模的云硬盘或者说数据库?也受限于你本机的网络带宽也是有限的。
举个例子,给你提供10PB大小的数据库或者给你提供10PB的云硬盘,理论上是可以这么做,但是你往里面写的时候,你写的IO还是受限于你本机网络的带宽。同时对整个云硬盘后端或者数据库后端快信息、TiDB信息的存储也会带来新的挑战。
所以提供10TB的云硬盘是比较容易的,我是说大家为什么不提供10PB的云硬盘,没有一家厂商提供这么大的云硬盘,不是说技术上不可能,只是说这个产品功能上讲,哪怕给你提供了这么大的云硬盘或者说数据库,你在单机使用上也发挥不出这么大容量的优势,主要是这个原因。
有,之前是这样的,京东之前的MySQL分布式解决方案,我们采用的也是用Proxy的方案,为什么会选择用Proxy的方案?原因有以下几点:第一点,Proxy方案开发起来比重新改写引擎还是要相对容易一些的。第二点,你在一个公司内部去推一个分布式方案,业务部门还是比较愿意配合的,因为他碰到了存储空间的问题,他需要有团队去解决这个问题。
为什么我们现在开始要做兼容MySQL的分布式存储引擎?是因为我们在云端给用户提供一个想比单机版性能更好,空间更大的数据库服务的时候,我们必须要考虑到用户的使用场景,用户现有的架构,用户的使用习惯。如果说我们要让用户来用我们这个Proxy的方案,我们的Proxy方案在京东云上叫DRDS服务,如果让用户来用我们DRDS服务的话,其实要针对他自己的业务进行一些分库分表的操作,要限制他的SQL语句,对用户而言他的改造量是比较大的。而且很多时候,对于一些复杂业务而言,它并不具备有这样的能力去改造它的业务。所以对于用户而言,我们还是希望能够提供一个性能也足够好,空间也相对比较大的,比如说空间也能满足它的要求的,又不需要它进行任何代码更改的数据库服务,所以我们也会做针对兼容MySQL的分布式存储引擎做开发。
云存储跟数据库还是两个完全不一样的东西,我理解你想要问的是云存储去存它的meta信息 的时候有没有用到分库分表,我觉得你应该是问这个意思。绝大部分的云存储肯定是要用分布式的存储系统或者K/V,不管是K/V也好,还是MySQL也好,它肯定需要做分布式TiDB信息的存储。因此我们可以看到,它对应的云存储的存储方式是一个文件名对应它相应的数据内容,我们可以理解成它是一个超大规模K/V的持久化程度结构,只是它的value可以变成非常大,可以从一个KB到10个T都行,这个就看你的文件大小了。文件怎么存储,放在哪些机器上?这其实算一个TiDB信息,这个信息是海量的,对于所有的云存储而言它肯定要用到分布式,至少用到分布式K/V的存储。分布式K/V存储涉及到的内容跟你做一个分布式数据库的思路和方法还是非常类似的。
这两个分布式的难度不一样,它解决问题的难度不一样。分布式缓存系统,比如说我们以Codis为例,Codis做的样子还是会很像我们刚才讲的数据库的Proxy的方案,它长的几乎一模一样,只是一个支持MySQL的语法,一个支持Redis的语法而已。分布式缓存它面临的挑战比较小,因为它不需要支持事务,而且分布式缓存系统支持的语法里面比较简单,它并没有复杂的SQL、join查询这种东西,它的语句支持起来比较容易。对于一个数据库而言,你如果真正想做到一个分布式数据库,而且兼容大部分的SQL语法的话,你要做分布式事务,而且要支持各种复杂的SQL,子查询,挑战还是非常大的,这两者的难度完全不一样,可以认为分布式数据库比分布式缓存要做的难度至少乘以10。