随着用户数据量的不断增长,基于传统共享存储的纵向扩展能力渐渐变得力不从心,分布式存储成为应对用户海量数据的标配。
作为一位架构师,在设计系统的分布式存储架构时,需要关注哪些方面呢?或者我们换句话说,对于客户来说,一个理想中的分布式存储产品应该具有哪些特性呢?
我们认为完善的分布式存储架构应该关注这五个方面:
架构设计服务于业务,再完美的系统架构都需要有业务进行使用才能创造价值。对于业务来说,当然希望我们的产品能够同时具备高扩展、高可用、强一致、低成本以及零门槛的易用性,但对于系统架构师和开发者来说,这五个特性之间存在相互矛盾的不少,同时在实现这些特性时也会面临巨大的复杂性,这要求我们在做系统设计及实现时需要有所权衡。
下面我们就OceanBase创立九年多以来存储架构的演进历程,来回顾每一次架构变更背后的权衡与思考。
1)OceanBase 0.1版本(2010年)
OceanBase由阳振坤于2010年在淘宝创立,当时淘宝大多数业务都已经按照用户维度做了分库分表,一个全新的分布式存储系统似乎很难有用武之地。最终我们找到了OceanBase的第一个业务:淘宝收藏夹,也就是我们今天打开手淘看到喜欢的商品点收藏时用到的收藏夹,直到今天它仍然跑在OceanBase数据库上面。
当时收藏夹面临了一个分库分表难以解决的问题,它的核心业务主要包括两张表,一张是用户表,记录一个用户收藏的商品条目,数量从几条到几千条不等;另一张是商品表,记录一件商品的描述、价格等明细信息。如果一个用户增加/删除收藏,那么相应的就向用户表中插入/删除数据就可以了;同时如果一个商家需要修改商品描述,例如修改商品价格等信息,那么相应的更新商品表就可以了。
当用户打开收藏夹时,通过用户表和商品表的连接查询,就可以展现给用户最新的商品信息。最开始的时候,这两张表是在一个数据库里面,也一直运行地很好,但随着用户数据量的增长,单个数据库放不下了,一般常用的做法是将表按照用户维度进行拆分,用户表是可以这样拆,但是商品表中没有用户字段,如果按照商品条目进行拆分,那么在用户打开收藏夹时,就需要对多个不同的库进行查询并做连接,当时的数据库中间件并没有这样的能力,即使可以这么做,一次查询也会耗费非常长的时间,会极大的影响用户体验,业务遇到了很大的困难。
OceanBase接下了用户的这个难题,如果我们分析扩展性、高可用、一致性、低成本和易用性这五个特性,那么什么是业务的刚需,什么是业务可以放弃的呢?业务最强的刚需是扩展性,因为传统的单机模式已经走到了尽头;最可以放弃的其实是易用性,因为业务对写入查询的使用非常简单,提供简单的读写接口就可以满足业务需求,业务甚至不需要在表上构建索引。同时我们也注意到业务对一致性也有一定的需求,业务可以容忍一定的弱一致读,但不能容忍数据出错。这些特性决定了OceanBase从诞生的第一天起,就是一个支持在线事务处理的关系型分布式数据库。
我们注意到收藏夹这个业务的特性,它的存量数据比较大,但是每天的增量并不大,毕竟每天新增收藏的用户并不是特别多。它更关心数据存储的扩展性,而对写入的扩展性要求并不是很高。我们将数据分为两部分:基线数据和增量数据。基线数据是静态的,分布式地存储在ChunkServer上。增量数据写在UpdateServer上,通常是存储在内存里面,通过Redo Log支持在线事务,在每天的业务低峰期,UpdateServer上的数据会与ChunkServer上的数据做合并,我们称之为“每日合并”。MergeServer是一个无状态的Server,提供数据写入的路由与数据查询的归并;RootServer负责整个集群的调度与负载均衡。这是一个类似于LSM Tree的存储架构,这也决定了今后OceanBase的存储引擎都是基于LSM Tree的。
我们回过头来看OceanBase0.1的架构,它实际上具有很强的一致性,因为写入是个单点,读到的数据一定是最新写入的数据,同时成本也并不高,也具有一定的扩展性,存储空间可以很容易地做扩展,很好满足了当时业务的需求。
2)OceanBase 0.2-0.3版本(2011年)
很快OceanBase 0.1版本上线了,并为收藏夹业务提供了读服务,但业务不能把所有流量都切到OceanBase上面来,因为OceanBase 0.1版本的架构有着一个很大的缺陷:它不是高可用的。任何一台服务器的宕机都会造成数据的不可访问,这对于收藏夹这样的业务是无法接受的。很快我们带来了OceanBase 0.2版本的架构,补上了高可用的短板。
在OceanBase 0.2版本中我们引入了主备库模式,这也是当时传统数据库常用的容灾模式,数据通过redo log从主库同步到备库,当主库发生问题时,可以把备库切换为主库继续提供服务。redo log的同步是异步的,这意味着主备的切换是有损的,可能会丢失若干秒的数据。
我们将OceanBase 0.2版本和OceanBase 0.1版本的架构进行对比,会发现OceanBase 0.2版本终于有了高可用这一重要特性,但高可用的获得不是没有代价的,首先系统不再是强一致的了,我们不能够保证业务总是能够读到最新的数据,在宕机场景下,数据可能会有部分丢失;其次主备库的引入极大地增加了成本,我们使用的机器数量翻番了。之后的OceanBase 0.3版本基于OceanBase 0.2版本做了很多代码上的优化,进一步降低了系统成本,但从架构上来说和OceanBase 0.2版本并没有显著的差别。
3)OceanBase 0.4版本(2012年)
随着收藏夹业务的成功,很快我们接到了更多新的业务,淘宝直通车是一个面向商家的业务,也面临着分库分表难以解决的问题。首先淘宝直通车的数据量越来越大,单库难以支撑,同时它又是一个OLAP类型的业务,有很多多表间的关联查询,每张表的维度又各不相同,无法统一按照用户id进行拆分。对于OceanBase的扩展性、高可用以及低成本业务都很满意,但是接口使用确实是太痛苦了。那么问题来了,什么是最好的接口语言?对于编程来说,可能不同的语言都有不同的拥趸,但对于数据操作来说,我们认为SQL一定是最好的语言。对于简单的KV查询,你可能会觉得SQL过于沉重了,但当你的业务慢慢复杂起来后,SQL一定是使用最简单轻便的。
在OceanBase 0.4版本,我们对SQL有了初步的支持,用户可以使用标准SQL来访问OceanBase,支持简单的增删改查以及关联查询,但对SQL的支持并不完整。同时OceanBase 0.4版本也是我们最后一个开源版本。
对比OceanBase 0.4版本和OceanBase 0.2版本的架构,在OceanBase 0.4版本我们最终补上了易用性的白板,开始慢慢有了一个标准分布式数据库的样子。
4)OceanBase 0.5版本(2014年)
2012年底的时候,OceanBase团队来到了支付宝。当时支付宝面临着全面去掉IOE的强烈需求,IOE的成本太高了,但PC服务器的稳定性难以和高端存储相比,如果我们使用MySQL这样的开源数据库基于PC服务器进行替代,业务就面临着可能会丢数据的潜在风险。当时基于MySQL的容灾方案仍然只是主备同步,对于支付宝交易支付这样的核心系统来说,丢失一笔订单造成的损失难以估量。业务对数据库的强一致和高可用提出了更高的要求,也使得我们搭建了OceanBase 0.5版本的新一代架构。
在OceanBase 0.5版本中,我们引入了Paxos一致性协议,通过多数派选举来保障单点故障下的数据一致性。一般情况下,OceanBase 0.5版本的部署模式会是三副本,当有一个副本出现问题时,另外两个副本会补齐日志并重新选出一个主提供服务,我们可以做到单点故障下不丢失任何数据,同时故障恢复时间小于30s。同时为了更好地支撑业务,在OceanBase 0.5版本中,我们全面兼容了MySQL协议,支持了二级索引并有了基于规则的执行计划,用户可以用MySQL的客户端来无缝连接OceanBase,可以像使用MySQL一样来使用OceanBase。
对比OceanBase 0.4版本和OceanBase 0.5版本的架构,我们发现OceanBase 0.5版本基于Paxos,有了更强的高可用以及强一致,基于SQL有了更好的易用性,但代价是从两副本变成三副本,系统成本进一步增加了50%。
5)OceanBase 1.0版本(2016年)
在OceanBase 0.5版本的架构下,业务对强一致、高可用以及易用性的需求都得到了很好的支持,痛点慢慢集中在扩展性和成本上。随着用户写入量的不断增长,UpdateServer的写入单点总是会成为瓶颈,同时三副本也带来了过高的成本消耗。OceanBase 1.0版本带来了全新的架构,重点解决了扩展性和成本的痛点。
在OceanBase 1.0版本中,我们支持了多点写入,从架构上将UpdateServer、ChunkServer、MergeServer和RootServer都合并为一个OBServer,每一个OBServer都可以承担读写,整体架构更加优雅,运维部署也更加简单。一张表可以被划分为多个分区,不同分区可以散布在不同的OBServer上,用户的读写请求通过一层代理OBProxy路由到具体的OBServer上进行执行。对于每个分区都仍然通过Paxos协议做三副本高可用,当有一台OBServer出现故障时,这台OBServer上的分区会自动切到其他包含对应分区的OBServer上提供服务。
在成本方面,我们注意到在Paxos协议中,需要三副本同步的只是日志,日志需要写三份,但是数据并不是,和数据相比日志量总是小的。如果将日志和数据分开,我们就可以使用两副本的存储开销实现三副本高可用。在OceanBase 1.0版本中,我们将副本分为两种类型:全功能副本和日志副本,其中全功能副本既包含数据也包含日志,提供完整的用户读写;日志副本只包含日志,只进行Paxos投票。
同时在OceanBase 1.0版本中,我们引入了多租户的概念,在同一个OceanBase集群中,可以支持多个不同的租户,这些租户共享整个集群资源,OceanBase会对不同租户的CPU、内存、IO及磁盘使用进行资源隔离。租户可以根据自己的需要配置不同的资源容量,集群会根据不同OBServer的负载做动态的负载均衡。这使得我们可以把很多个小租户部署到同一个大集群中来,降低整体的系统成本。
和OceanBase 0.5版本相比,OceanBase 1.0版本的扩展性有了大幅提升,而且由于引入了日志副本和多租户技术,在成本上有了大幅降低;但扩展性的提升不是没有代价的,多点写入带来了巨大的复杂性。首先用户的写入并不一定会只写单个分区,对于多个分区的写入不可避免会带来分布式事务,我们使用两阶段提交协议来完成分布式事务。其次在一个分布式系统中获取一个全局单调递增的时间戳是极其困难的,由于没有全局时钟,我们基于局部时间戳做单分区内的读写并发控制,这使得系统的一致性有了一定的限制,虽然单分区的查询仍然是强一致的,但跨分区的查询无法保证读的强一致,这对于用户而言会是一个不小的限制。同时由于分区的关系,二级索引成为了分区内的局部索引,这要求索引键中一定需要包含分区键,无法支持全局的唯一索引,这对于用户使用也造成了一定的不便。
6)OceanBase 2.0版本(2018年)
OceanBase 2.0版本的外部整体架构与OceanBase 1.0版本没有太大差别,仍然是一个share nothing的三副本架构,但在内部我们对扩展性、高可用、一致性、低成本和易用性都做了极大的提升。
在扩展性方面,我们实现了分区分裂的功能。在建表的时候,用户对合适的分区数可能没有很好的估计,当分区过大时,可以通过分区分裂来使得分区数变多。尽管分区分裂是一个比较重的DDL操作,在OceanBase 2.0版本中,分区分裂是可以在线进行的,对用户的正常业务读写并不会造成太大影响。
在高可用方面,我们支持了主备库功能,对于某些只有双机房的用户,可以在机房内通过三副本做机房内的无损容灾,通过主备库做跨机房的有损容灾。
在一致性方面,我们支持了全局快照,真正意义上实现了分布式读写下的强一致。基于全局快照,我们也完成了对全局索引以及外键的支持。
在低成本方面,我们在事务层支持了TableGroup,允许把一组相近的表“绑定”在一起,减少分布式事务的开销。在存储层引入了数据编码,通过字典、RLE、Const、差值、列间等值、列间前缀等算法进一步压缩存储空间的占用,并且对于数据的编码是自适应的,会根据数据特征来自动选择合适的编码算法。
在易用性方面,我们支持了Oracle租户,允许用户在同一套ObServer集群中同时使用MySQL租户与Oracle租户,并且支持存储过程、窗口函数、层次查询、表达式索引、全文索引、ACS、SPM、回收站等功能。
尽管今天的OceanBase 2.0版本在扩展性、高可用、一致性、低成本以及易用性方面做到了更好的平衡,但这样的架构并不是一蹴而就的,从OceanBase 0.1版本到OceanBase 2.0版本的发展历程来看,OceanBase的架构总是在一直进化,为了能够更好地服务于业务,很多事物总是面临着许多权衡取舍,一项特性的提升会以其他特性的降低为代价。架构的优化演进没有终点,未来为了更好满足业务的需求,OceanBase的架构还会不断进行演化。
原文链接
本文为云栖社区原创内容,未经允许不得转载。