什么情况下,需要使用分布式数据库?

目前,服务器的磁盘和内存,cpu都相对较好,一台数据库服务器可以存储好几亿条的数据,在一个什么样的情况下,应该考虑分布式数据库的,百亿?千亿? 如果单…
关注者
512
被浏览
239,819

37 个回答

分别回答下,题主的三个问题:

1. 目前,服务器的磁盘和内存,cpu都相对较好,一台数据库服务器可以存储好几亿条的数据,在一个什么样的情况下,应该考虑分布式数据库的,百亿?千亿?

考虑用分布式数据库,肯定是容量或者性能方面,现有的单机数据库满足不了业务的需求。当然,遇到了容量或者性能的问题,也不一定要用分布式数据库,可以通过scale-up的方式,即升级数据库服务器的CPU、内存、磁盘,将SATA/SAS盘换SSD盘等方式解决。不过相对scale-up来说, 分布式数据库这种scale-out的方式,扩展性会更强一些,一般来说也更具性价比。

普通的X86服务器,一台数据库服务器存好几亿条数据,问题不大,但前提是需要分库或分表,单表几亿条数据,普通服务器基本支撑不了的,毕竟数据量一大,表对应的B树层次就高,写入时B树节点的分裂和调整,开销也大。同时,上亿规模下,单台数据库服务器的恐怕不能支持密集的读请求,性能可能会有问题。

2. “如果单机数据库,直接通过分布式数据库来访问,分布式数据库是否能够提高数据库的效率呢?”

题主这里所说的分布式数据库, 应该是指数据库中间件这样的软件吧。目前比较流行的一种做法,是DBA利用开源中间件,结合自己项目的mysql或pg数据库,来搭建出一套分布式数据库的解决方案。主要的方法有两种:

一种是水平拆分。当数据量大到单机数据库已存储不下时, 可以对数据进行拆分,化整为零,将数据均匀分布到多个数据库节点中。由于对数据进行了拆分,每个数据库节点上的数据量小了,自然读写性能就提高了。

另一种是读写分离。这种方法,主要用在数据量并不大,单机数据库能够hold得住,但读请求很高的情况下。此时,可以配置多个只读数据库节点,来分担主节点的读请求。通过数据复制机制,在主节点和只读节点之间进行数据的实时同步,保证主从节点的数据一致性。

两种方法很好地解决了数据库的容量和性能问题。当然,使用了中间件,相当于在sql的执行路径上,多了一个处理环节,因此单条sql的延时,相对于直连数据库节点,在非满负载的情况下,肯定是要高的。但在实际的业务访问中,sql的性能瓶颈,一般都出在数据库节点上,中间件只是做单纯的sql解析和路由,性能开销不会很大。因此,通过增加数据库节点,提升sql处理的短板,是能够提高系统效率的。

3.“数据库分库后,一些复杂的sql场景,会比较难处理,而且分库之后,sql除了查询分库的数据外,还要进行数据合并操作,那是否是说不分库,比分库更好一些呢?”

基于中间件来进行分库, 确实对 SQL 有阉割的情况,并不是所有sql都能够支持。主要原因是数据被拆分了。而数据一旦被拆分到多个节点,则:

1.复杂的join查询

2. 同时更新多个数据库节点的sql语句

这两类SQL的支持难度,就比较高。这也是目前市面上所有中间件都无法满足的两点。复杂的join查询之所以难以支持,是因为要跨节点join;同时更新多个节点的sql难以支持,是因为很难解决多个节点的并发一致性问题。但是除了这两点之外,其他的sql类型,一款中间件是能够努力做到的。

从中间件实现的角度,我们来对sql做一个分析,以说明这一点。

1. 按操作范围的维度,可以把所有的SQL,分为3类:

1.1 kv类的sql: 这种sql操作很简单,就是简单的set/put某个表的一条记录,大部分insert/delete/update语句,和指定primary key/key的select,都属于这种类型。

1.2 范围更新/查询:这种sql不局限于操作一条记录,但还是作用于一张表。比如update多行记录,或者select某个时间范围的记录等。

1.3 多表join查询:又包括两种:

1.3.1 分库分表键都是同一个的多表join:由于采用同一个划分键,因此join操作其实是发生在单节点

1.3.2 分库分表键不是同一个的多表join: 此时涉及到跨节点的join,实现复杂

2.按是否要在关系运算之后,还要对结果进行聚合,把select sql分为两类:

2.1 不需要进行结果聚合,即select sql中没有集函数、group by、order by、limit等需要在关系运算之后,再对结果进行处理和聚合;

2.2 包括上述结果聚合语法的select sql

3.从是否对分布式事务有要求的角度,可以把SQL分为两类:

3.1 只读写一个节点的sql,无分布式事务要求

3.2 跨多个节点读写的sql,有分布式事务要求

根据之前所述, 目前的业内的中间件,都不能支持1.3.2 和3.2(mycat对分布式事务的支持,只支持最终一致性,还是一个伪支持;阿里DRDS号称内测版本支持分布式事务,但一直未见公测),而除去这两点,对于类型(1.1, 1.2, 1.3.1) × (2.1,2.2)得到的6种sql类型,理论上讲,中间件都是可以做到支持的。

对于OLTP应用来讲,这6种类型能够覆盖绝大部分的业务场景,这也是中间件技术这几年这么流行的原因。遗憾的是,目前业内的各大中间件, 对这6种类型的sql,支持程度往往都有一定的折扣。比如对于这样一条操作单表的sql:

select distinct id, avg(price) from t1 where id>=1 group by concat(id,name) order by avg(price) limit 10;

目前主流的几款中间件,似乎就不能支持。

目前,UCloud 的 UDB 团队,也在打造一款基于中间件和 UDB 的,分布式数据库产品 UDDB,协议和SQL语法,全面兼容MYSQL。我们从零开始,但目标远大。正如 UCloud 一直强调的,用户的需求是我们下一款产品。我们的第一个目标,是做一款业内最好用的分布式数据库,解决用户在使用mysql中间件构建分布式解决方案时的痛点:学习成本高,配置复杂,运维麻烦,扩容不方便。

在系统管理上,UDDB将做到一步创建,开箱即用,无需额外的管理和配置操作,并提供全自动化,无需停服的水平扩展/缩容操作;SQL支持上,我们将对类型(1.1, 1.2, 1.3.1) × (2.1,2.2)这6种sql,进行全面的支持,让用户在遇到带avg集函数、group by,order dy的select sql时, 无需担忧系统是否支持,即可放心使用。

预计在今年9月底,UDDB将开放内测,敬请大家关注。

长期来看,打造这样一款分布式数据库服务,是我们和客户交流,探索未来的一种方式。诚然,相对于 Google F1,Oceanbase 等全新设计的分布式数据库产品, 基于中间件的分布式数据库,在技术上并非最前沿,但是我们相信,这是广大客户目前正需要的。我们相信只有先服务好用户,深入到用户的需求当中,根据用户需求不断迭代,同时团队自身不断成长,才能真正理解公有云环境下,用户对分布式数据库的需求,才能做出接地气和广受欢迎的产品。

FoundationDB一个具有事务语义的分布式KV存储,是最早的一批把NoSQL和分布式事务结合起来的数据库系统,它提供了NoSQL的高扩展,高可用和灵活性,同时保证了serializable的强ACID语义。

这个数据库很有意思,其对于事务/高可用/容错的设计都非常独特,概括来说,整体采用了松耦合的模块化设计,系统分为了3个组件:

  • in-memory 事务管理
  • 分布式的storage管理
  • 分布式的system configuration管理

三个组件各自独立部署 + 配置,因此按照需求,各自provision来保证组件的可用性/扩展性。

另一个独特的地方是,它先用了2年时间开发完成了一套基于deterministic simulation的测试框架,然后才开始做的db产品。这个测试框架将系统中所有非确定性因素用mock模块来模拟,并可注入确定性的错误或事件,使得系统的运行完全可控且可复现,更容易发现bug和调试,也帮助提升新feature的开发速率。

从设计原则上,产品追求的是:提供最小的功能集合,即一个事务语义的分布式KV存储,并不提供其他功能如query language,data model,index,schema。。。这样反而为上层提供了灵活性,可以基于FDB构建各种类型的分布式系统。(Apple cloud/Snowflake/CouchDB...)

首先强的事务语义和最简单的data model对于上层的应用开发来说会更加方便,早期NoSQL为了扩展性牺牲了事务能力,只保证最终一致性,因此应用层必须对并发控制进行处理,大大增加了开发复杂度。而FDB则加上了强的串行化事务语义,因此上层应用可以方便的构建类似全局二级索引/唯一性约束等功能,且由于底层只是KV,所以很灵活,可以存储各种类型的数据,semi-structure/graph…

从FDB的角度,它对上层只提供,有序+事务+KV存储的抽象。

基本概念

  • 总体的架构是松耦合的,分为control plane + data plane
  1. control plane负责系统的管理和监控,使用active disk paxos[1]来存储系统metadata。
  2. data plane负责用户+系统数据的存储,其中事务管理系统负责写路径,storage系统负责读路径,是解耦的,可以各自独立扩展。
  • 利用OCC + MVCC的策略来实现串行化事务。
  • 使用failure -> restart -> recovery的策略来处理failure,即快速失败快速恢复。
  • 采用了类似Aurora的log is database的思路,对log进行多副本同步复制,是一种特例的quorum,所有副本都同步落盘,才认为提交成功,才能返回客户端!因此f+1个副本,最多可允许f个失效。

设计

paper中主要讨论了transaction/replication/fault tolerance(recovery) 三个方面

基本设计原则

  • 模块化分割,尽量细分且模块之间相互解耦

例如事务系统内,其提交(write path)和读取(read path)是可以独立扩展的,系统中根据事务功能的不同,区分了很多角色(timestamp管理,冲突检测,提交的协调,logging..),每个角色也可以独立配置和扩展。而全局来看也把各个功能尽量拆分开来,减少耦合。

  • 快速failure快速恢复

与一般的数据库系统不同,它处理各种类型的failure方式都一致,就是发现failure后重启整个事务系统,通过recovery机制来修复failure,因此必须做到快速检测和快速恢复才行。生产环境中从出现问题->发现->主动shutdown->recovery,一般在5s以内。

  • 确定性模拟测试系统

提升质量,使得bug可被reproduce。

对外接口是典型的key/value(get/set/getrange..),事务机制是典型的OCC,开始时基于系统的快照做读取+修改,所有修改在client本地缓存,结束时带着read set/write set发起提交。由于需要缓存修改,系统对于Key/Value/事务的大小都是有限制的,这和client的缓存+in-memory的事务管理系统的缓存机制有关。

整体架构

  • Control plane

存储系统一些关键的metadata,通过Active Disk paxos group保证高可用性,并选举出单个的Cluster Controller,controller会创建另外几个单实例进程:Data Distributor,Rate Keeper,Sequencer

Sequencer是事务系统的领头节点,它负责创建事务系统其它服务进程

Data Distributor 负责监控Storage Server的集群状态,并做负载均衡

Rate Keeper 其流控作用,避免系统overload

  • Data plane

FDB本身针对的负载类型是OLTP + 小事务 + 读为主 + 高并发但低冲突。其内部的3层也都是松耦合的。

TS层负责in-memory内存处理,由Sequencer领头,创建Proxy / Resolver,整个TS层是无状态的,便于发生failure时,快速整体重启。 (如果Sequencer重启了,timestamp怎么单调递增?)

LS层负责WAL的存储,按照key range做分片存储,且每个分片有多个副本。

SS层负责实际数据的存储,和LS的分片对应,每个分片有自己的WAL日志,底层目前使用的是SQLite,后续会考虑Rocksdb。

从上面的架构图可以看到,读写路径是分离的,TS层+LS层和write path相关,而SS层和read path相关。这是其设计的核心思想,将功能尽可能细分为不同role,由不同服务进程负责,不同role各自独立配置和扩展。例如如果想提高读吞吐,扩展storage server,如果想提高写吞吐,扩展Resolver/proxy/LS。

FDB的boostrap/reconfiguration

可以看到FDB内部各个组件是有着相互的关联的:

Coordinator中存储着系统核心metadata,包括LS Server的配置,LS Server中则存储了Storage Server的配置。

运行中,Controller监控Sequencer,Sequencer监控Proxy / Resolver / LogSevers的状态。

  • bootstrap

系统启动时,Coordinator会选举出Controller,后者启动Sequencer,Sequencer则启动另外3组进程,然后从Coordinator中获取老的LS的配置,并从老的LS中获取SS的配置,利用老的LS执行必要的recovery过程,完成后老的TS系统就可以退休了,新的LS的信息写入Coordinator中,系统完成启动,开始对外提供服务。

  • reconfiguration

当TS系统发生failure或者配置变化时,Sequencer检测到后会主动shutdown,Controller检测到后会重启新的Sequencer从而形成新的TS,新的Sequencer会阻止老的TS再提供服务,然后走和bootstrap类似的recovery流程即可。

为了标识不同的TS系统,引入了epoch的概念,任何时候新老TS交替,epoch就要+1。

事务管理

  • 并发控制

采用了比较经典的OCC套路,思路和SQL Server Hekaton[2]的并发控制有些类似:

客户端连接Proxy,获取读时间戳,Proxy向Sequencer获取read version返回client,client利用read version从storage server直接读取目标版本数据,在本地做修改并缓存,事务提交时带着read set+write set发给proxy,proxy首先向Sequencer获取commit version,然后发送Resolver做冲突检测,检测失败则返回client,client可以重试(所以要小事务)。

Resovler也是根据Key range分片启动多个实例,这样可以并发做冲突检测。

FDB实现的是可串行化事务,commit version的顺序就是事务提交顺序,因此可以认为事务需要在commit点瞬时完成,冲突检测就是判断在 [read version -> commit version ]之间,是否并发事务写入了冲突的数据。从下图可以看到,理论上Ti是在commit ts那一时刻瞬间完成了事务,因此Ti读取到的也应该是在commit ts时刻系统的快照,而在Ti start -> Ti end这个过程中,其他事务Tj修改了Ti读取的数据,read ts时刻看到的快照与commit ts时刻的就不再相同了,串行化被破坏。

Resolver的冲突检测算法:

为了检测,其内部使用skip-list维护一个LastCommit结构,即修改的key range -> commit version的一个映射,记录某个key range最近一次的提交version,便于找到对某个key/key_range,最近一次commit_ts是否在当前事务的[read_ts, commit_ts]之间(即冲突),不冲突即用当前事务commit version更新LastCommit。

这里有一个问题,由于是多个resovler并发检测冲突,可能一些resolver局部认为是无冲突的,因此更新了自己维护的LastCommit结构,导致后续不应该失败的事务发生冲突(false-positive)。FDB认为这不是大问题,首先它面向多租户应用,冲突较少,一般事务都会落入一个resovler。此外即使失败后重试,新的ts的read version增长后,超过这个伪提交事务的commit ts即可。

除了read-write事务,还有read-only事务,只获取read version从SS读取数据,然后client直接提交就可以,等同于在read ts瞬时完成,不需要检测冲突。 此外在read-write事务中还允许做放松串行化要求的snapshot read,这种read不放入read-set中,不做冲突检测。

  • 持久化

所有Resovler返回成功时,Proxy认为事务可以提交,向LogServer发送log做多副本同步复制,所有副本的日志都落盘后,Proxy认为事务提交成功,返回给客户端。同时Storage Server异步的从LS获取log entry并apply,即使log entry还没有落盘也会apply,采用这种激进策略来保证data和log之间的低延迟。

可串行化的并发控制使得log entry之间形成了严格的顺序,大大简化了log管理的逻辑,可以用version来表示LSN,针对每个key,它所面对的实际是一个有序的log entry队列,依次apply就可以了。

Proxy写log的流程也比较特殊:

Proxy本地有缓存一份key range -> SS的映射关系,这样就可以知道要写入哪些SS和对应的LS。例如上图中,LS1 + LS4是要写入的LS,因此把这个事务的log都写入(形成副本),此外由于是3副本,再额外写一个LS,其余的LS也要发送,但只传递Log Header,其中包含的最主要信息是当前的LSN和Proxy上的KCV,即本Proxy已知的最大已提交事务,LS收到后会更新自己本地的KCV,这个KCV在recovery时会使用。

LS上的WAL -> SS和apply redo并不在commit path上,是异步持续完成,因此可以说FDB也遵循了”log is database”的思想。这种方式client做read一般可以读到目标version的数据,如果不行就等待或者向其他副本请求,都不行的话,可以超时后重试。

由于是异步apply,可以做batching,将一批更新缓存在SS上,批量刷盘提高IO效率。但这里也有个问题,由于LS中在内存中(未提交)的entry也可能被apply,因此SS是有脏数据的,在recovery时要rollback。

  • Recovery

FDB做恢复是最为与众不同的,由于其基于recovery来做failure处理,因此recovery是常规操作,需要快速恢复。

由于redo log apply是在后台持续进行的,因此本质上它将redo apply从recovery中解耦出来,等于持续在checkpointing,在recovery期间不需要做redo/undo apply,只是确认当前的log序列需要恢复到哪个位置即可!!后续基于log -> data的过程仍然是异步。这保证了recovery的速度。

具体流程:

发现failure后,老Sequencer退出,新Sequencer启动,并从Coordinator获取老的TS的配置信息,包括老的LS的位置等,同时给Coordinator加个全局锁,避免并发recovery,然后关闭老的LS,禁止接收新的log写入,开始恢复,恢复完成后启动TS系统接收用户请求。

Proxy和Resolver都是stateless的,直接重启就可以,只有LogServer有log信息,恢复如下:

由于在日常提交写日志时,Proxy会把本地记录的KCV广播给所有LS(见持久化一节),LS中就记录了自己见过的最大的KCV。选取所有LS中KCV的最大值,在这个值之前的事务,其日志已经完全复制并落盘,且已告知Proxy,可以作为上一个epoch的终点,称为PEV(previous epoch’s end version)。

同时每个LogServer都记录了本地已持久化的version (DV),选取所有DV中的最小值,作为Recovery Version(RV),在PEV -> RV之间的日志,已持久化且不在内存中,但不确定是否已提交(因为proxy没有该信息,可能崩溃的那个没持久化),因此这部分需要进行恢复(redo),而 > RV的log entry,肯定没有多副本都持久化,因此不可能提交,这部分要undo。

因此整个的recovery流程,就是将老的LS中的[PEV+1 , RV]之间的部分,copy到新的LogServer中并完成log复制即可。这样这部分事务已成功排好队,后续在开始接受用户请求前,先启动一个事务将RV之后的log对应的数据rollback,然后就可以处理用户请求了(log已准备好继续append)。

复制

系统中不同组件,复制策略不同

  1. Metadata,存储在Coordinator中的,通过Paxos变体实现复制,因此适用于quorum策略。
  2. Log,存储在LogServer中,是全量同步复制,因此允许f个失败 (f+1个副本)。
  3. Storage,由于Log已同步复制,存储就是异步复制到f+1个副本

为了做fault tolerent,storage的各个副本是有一定策略来分布到各个fault domain的,防止多个副本同时失效的情况,这个和Spanner的调度策略类似。

另外2个小的优化:

1. Transaction batching

在Proxy上为了减少与Sequencer/LS的交互成本,可以把不冲突的并发事务合并,获取同一个commit version,并一起下发到LogServer。相当于group commit。

这个策略是可以自适应的,在系统负载不大时,为了减少延迟,可以减小patch大小,当系统重负载时,为了保证吞吐则可以加大patch。

2. Atomic operations

对于一些只写不读的操作,其相互之间可以不做冲突检测,直接获取提交时间戳就可以,这对于某些counter类型的操作会提高效率,因为避免了从storage的一次读,也避免了resolve confilct。

Failover

对于跨Region的高可用和failover,采用了一种巧妙的策略:

在一个region内,建立多个AZ,各个AZ之间是独立失效的,因此整个region失效概率是很低的。region内分为Data center和Satellite site,主DC中允许TS + LS + SS,standby中的DC只保留LS + SS,而Satellite中只运行LS的副本,因此消耗的资源会少很多。

在primary region内,log还是同步复制的,跨region,则通过LogRouters,从主region的LS中异步拉取,同时LogRouter保证同一个log entry不会重复复制(怎么做没有细讲)。

DC之间有有优先级,例如上图DC1 > DC2。

每个region可以各自配置自己的复制策略:

  1. 选择一个最高优先级的Satellite,做同步复制(k = 2),如果这个satellite失效了,则再选次级satellite补上
  2. 选择2个最高优先级的Satellites,做同步复制(k = 3),如果一个satellite失效了,则fallback为选项1
  3. 由于前2个都是完全同步复制,所以如果有那种长尾 的网络延迟,则commit会延迟,为此可以配置3副本,但复制时,允许一个异步复制,因此减小了长尾的概率。响应会更快些。

primary DC失效时,standy 的DC会启动一套TS,从主region中的logserver中拉取尚未同步的日志,并根据本region的复制策略,建立Satellites上的LS副本。如果完成后,primary DC已经恢复了且满足了复制要求,则fallback回primary DC,否则secondary DC开始提供服务。

Simulation test

这是FDB一个非常有特色的地方,很可惜这里的很多细节我没有搞清楚,只知道是基于Flow的异步编程框架,每个process都是单线程+callback处理。虽然他们在cmu的talk中也着重讲了下这个测试框架,但我听得模模糊糊。。。基本没体感,所以就不误人子弟了,希望有了解的大神指导下。

总结

总的来说,FDB有几大特色:

  1. 非常松耦合的系统,read/write path是分开的,每个功能组件可以独立扩展来避免成为系统瓶颈。
  2. log is database,redo apply + undo和recovery解耦,减少commit path的负载
  3. 通过OCC + MVCC实现串行化事务,简化log的处理
  4. 快速恢复,通过fast failure + fast recovery,来统一对于failure的处理方式,这个recovery路径比较快且稳定

但缺点也很明显:

  1. 有限的功能集
  2. 串行化事务对事务大小的限制
  3. 面对场景比较确定,就是可扩展的,支持小事务高并发的KV存储,且冲突不能太多,不然重试+回滚太多,影响系统吞吐。

参考

  1. ^active disk paxos https://groups.csail.mit.edu/tds/papers/Chockler/podc-02.pdf
  2. ^hekaton http://web.eecs.umich.edu/~mozafari/fall2015/eecs584/papers/hekaton.pdf