vlambda博客
学习文章列表

再谈:分库分表的那些事

分库分表,是面对海量数据的一种有效的解决方法。特别是随着过去一二十年互联网行业的高速发展,在很多互联网公司得到大量的应用,很好地解决业务问题。但随着近些年来NewSQL的兴起,面对海量数据有了更多的选择。其实从底层逻辑上讲,两者是同质的,但确实在具体实现上有所不同。之前也分享过几篇分库分片的内容,近期对这一问题有了更多认识,特整理本文。




1. 分库分表定位

在考虑分库分表之前,我们先来探讨下分库分表是解决什么问题的一类技术。从大的方向上看,分库分表是解决两类问题:一是资源承载问题,二是开发架构问题。

1).资源承载问题

这里所谈的资源,是指数据库所在的底层基础资源,包括CPU、MEM、DISK等。这里比较突出的问题是来自CPU、IO及容量问题。

  • CPU瓶颈

    这里突出SQL处理问题。一种情况是重数据库计算类(如join、group by、order by、非索引列查询等),上述操作都会对CPU造成较大压力,一般是通过创建索引或将计算移至业务层来解决。通过数据拆分,是可以有效地减少数据处理规模,对CPU资源不足可以起到缓解作用。第二种情况是针对单表的操作,虽然计算不复杂,但需要扫描大量数据或需遍历树的层次过多,SQL效率会很低,也会非常消耗CPU。此时根据业务场景进行一些数据拆分(主要指水平分表),是可以降低对CPU的资源使用。

  • IO瓶颈

    针对IO瓶颈,可分为磁盘IO和网络IO两种情况。前者指磁盘的读写,后者指网络通讯传输。磁盘读IO问题,通常是指热点数据太多,数据库缓存放不下,查询时会产生大量的磁盘IO,查询速度会比较慢,进而会导致产生大量活跃连接,最终可能会发展成无连接可用的后果。解决的方式一种是采用读写分离的策略,通过多个从库来分摊查询流量;另一种是使用数据拆分(可以使分库或分表均可),通过数据拆分减少读取的数据规模。磁盘写IO问题,通常是指数据库写入频繁,频繁磁盘IO操作导致产生大量活跃连接,最终同样会发展成无连接可用的后果。解决可通过数据拆分(主要指分库方式)减少单个库的写入压力;此外辅助库内数据拆分(水平分表),可进一步减少插入数据时索引查找和更新的成本会更低,插入速度自然会更快。至于网络IO瓶颈,则比较简单,通常就是请求数据太多,网络带宽不够,通过数据拆分(主要指分库)方式即可减少传输规模。

  • 容量瓶颈

    这点很好理解,当业务数据很大,已经达到库或表的承载能力,就可考虑进行拆分。可通过库级别、表级别或者两者结合,降低容量的诉求。

2).开发架构问题

从开发架构角度来看,随着SOA、微服务等技术逐步成熟,被更多企业所接受。通过对架构上服务拆分,可满足快速迭代、安全发布、链路降级、主次业务解耦等问题,去解决代码大量冲突、小功能排队等待大版本发布等等问题,将业务按照一定逻辑进行拆解,形成一个个功能完备,独立运行的服务。然而,如果数据库层面不配合,就无法解决根本问题。当上层服务实例拆分后可以被大量横向扩展,以应对高并发的流量冲击,会导致底层数据库的承载压力和连接数急剧增加。所以,通过垂直拆分将业务数据解耦,各管一事,以满足微服务的效能最大化。


2. 分库分表类型

对数据进行拆分,可以有多种方式和粒度。大的方式可分为两种:垂直拆分和水平拆分。从粒度上分,又可以分为分库和分表。两者组合,就有了四种基本情况及组合。

1).垂直分库

垂直分库在“服务化、微服务”盛行的今天已非常普及。基本的思路就是按照业务模块来划分出不同的数据库,而不是像早期一样将所有的数据表都放到同一个数据库中。系统层面的“服务化”拆分操作,能够解决业务系统层面的耦合和性能瓶颈,有利于系统的扩展维护。而数据库层面的拆分,道理也是相通的。与服务的“治理”和“降级”机制类似,我们也能对不同业务类型的数据进行“分级”管理、维护、监控、扩展等。这种做法可以将访问压力分摊到多台服务器上,一方面可提升性能,一方面提高整体的业务清晰度,不同的业务库可根据自身情况定制优化方案,但是它需要解决跨库带来的所有复杂问题。

数据库是容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。垂直分库在一定程度上能够突破IO、连接数及单机硬件资源的瓶颈,是大型分布式系统中优化数据库架构的重要手段。但这种拆分方式下,单个用户请求的响应时间变长,原因在于所有业务原本在一个节点内完成,拆分后则需要进行RPC调用后完成。虽然单个请求的响应时间增加了,但是整个服务的吞吐量会大大增加。从数据特点上看,每个库的结构不同,对应的数据也没有交集。所有库数据的合集为全量数据。

2).垂直分表

垂直分表是基于数据库中的"列"进行。可将一个宽表(字段较多),按访问频次、是否是大字段的原则拆分为多个表。这样既能使业务清晰,还能提升部分性能。通过"大表拆小表",更便于开发与维护,也能避免数据存储跨页问题,即一条记录占用空间过大会导致跨页,造成额外的性能开销。另外,数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。拆分后,尽量从业务角度避免关联查询,否则性能方面将得不偿失。

垂直分表的拆分原则是将热点与非热点数据拆分,这样更多的热点数据被缓存下来,进而减少了随机读IO。但需注意的是,拆分之后如想获得全部数据需要关联两个表来取数据。此时关联Join不仅会增加CPU负载,并且将两个表耦合在一起。建议关联数据需求,应该在业务层面解决,分别提取两张表然后通过关联字段关联起来。从数据特点上看,每个表的结构不一样,每个表的数据也不同。一般来说,每个表的字段至少有一列交集,用于关联数据。所有表的并集是全量数据。此外,拆分字段的操作建议在数据库设计阶段就做好。如果是在发展过程中拆分,则需要改写以前的查询语句,会额外带来一定的成本和风险,建议谨慎。

3).水平分库

当一个应用难以再细粒度的垂直切分,或切分后数据行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。水平分库,可把一张表的数据按数据行分到多个不同的库,每个库只有表的部分数据。这些库可分布到不同服务器,从而使访问压力被多服务器负载,大大提升性能。它不仅需要解决跨库所带来的复杂问题,还需解决数据路由的问题。

使用库内分表方式,只能解决单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻数据库资源压力来说,帮助不是很大,大家还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库(分表)来解决。从数据特点上看,每个库结构一样,数据不同没有交集。所有库的数据合集是全量数据。

4).水平分表

水平分表也称为横向分表,比较容易理解,就是将表中不同的数据行按照一定规律分布到不同的数据库表中(这些表保存在同一个数据库中),这样来降低单表数据量,优化查询性能。最常见的方式就是通过主键或者时间等字段进行哈希和取模后拆分。水平分表,能够降低单表的数据量,一定程度上可以缓解查询性能瓶颈。但本质上这些表还保存在同一个库中,所以库级别还是会有IO瓶颈。所以,一般不建议采用这种做法。从数据特点上看,每个表的结构一样,数据不同没有交集,所有表的合集是全量数据。

水平分库分表与上面讲到的水平分表的思想相同,唯一不同的就是将这些拆分出来的表保存在不同的数据中。这也是很多大型互联网公司所选择的做法。某种意义上来讲,有些系统中使用的“冷热数据分离”(将一些使用较少的历史数据迁移到其他的数据库中。而在业务功能上,通常默认只提供热点数据的查询),也是类似的实践。在高并发和海量数据的场景下,分库分表能够有效缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源的瓶颈。当然,投入的硬件成本也会更高。同时,这也会带来一些复杂的技术问题和挑战(例如:跨分片的复杂查询,跨分片事务等)。

再谈:分库分表的那些事


3. 分库分表实施

1).数据拆分时机

针对何时考虑数据拆分,取决于多种因素,包含以下几种。

  • 业务发展

    从业务长期发展角度来看,当数据增长到一定程度,虽然没有达到极限,还能凑活,但是遇到活动型流量冲击下,已经无法完全支持业务需求;而业务需要进行迭代增加模式时,修改数据表带来的风险又比较大。

  • 业务耦合性

    这点更多是从架构设计角度出发考虑,往往随着业务改造完成。

  • 性能瓶颈

    如果当前业务已经出现较为明显性能瓶颈,且通过常规的优化手段已经无法根本上解决该问题。

  • 维护需求

    当数据体量达到一定量级时,对数据表的单体操作将非常困难。例如数据库的备份、单体大表的DDL、热点大表的更新访问等,都容易出现问题。

  • 可用性

    在业务层面进行垂直拆分,将不相关业务分隔。因为每个业务的数量量、访问量都不同,不能因为一个业务把数据库搞挂而牵连到其他业务。而采用水平拆分后,当一个数据库出现问题时,也不会影响到100%用户,每个库只承担业务的一部分数据,整体的可用性提高。

 最佳实践 

在分库分表时机的判断上,没有明确的判断标准,比较依赖实际业务情况和经验判断。除了考虑当前的数据量和性能情况外,还需要考虑系统在未来1~3年的业务增长情况,对数据库QPS、连接数、容量等做合理评估和规划。这里需要尽量避免“过度设计”和“过早优化”。在真正考虑分库分表之前,不要为了分而分,先尽力从其他角度进行考虑。例如:升级硬件、升级网络、读写分离、索引优化、分区优化等。毕竟数据分片对于业务、研发、运维等都存在较高成本。

2).数据拆分原则

一般来说,在系统设计阶段就应该根据业务耦合情况来确定垂直分库、垂直分表方案。在数据量及访问压力不是特别大的情况,优先考虑缓存、读写分离、索引优化等手段解决性能问题。若数据量过大,且持续增长,再考虑水平分库及分表方案。其中,垂直拆分方面更多是来自架构层面的工作,这里不做展开;后面重点谈到关于水平拆分的一些内容。针对水平拆分,通常遵循如下的步骤。后文将针对重点几步进行说明。

再谈:分库分表的那些事

  • 分库or分表:明确采用水平分片的策略,是采用分库、分表还是分库分表。不同策略能解决的问题不同。需要根据当前面临的痛点来考虑。这两者之间的差异,可参考前面的说明。

  • 评估分片数:根据当前容量、增量(空间、性能)要求,确定分片数。结合前面的分片策略,通过单体大小的评估,完成分片数的估算。为确保实施效果,可采取必要的验证测试工作。

  • 选择分片键:选择合适的分片键。这里需要考虑的因素较多,要确保数据分布均匀,未来发展的影响,避免局部热点问题。这部分也会直接影响到后面分片应用改造工作,需要综合考量。

  • 确定分片规则:明确分片字段后,确定分片规则。一方面保证数据分布均匀,一方面还需考虑未来扩展问题。如果分片规则对业务有较多依赖,还需考虑业务因素。如未来业务可能的变化等。

  • 实施分片策略:真正实施分片动作,将单体拆分成多分片。这一过程中,涉及到如何做好数据迁移等问题。要实现真正的业务无感、数据准确等,还是非常有难度的。通常会考虑应用双写、日志补偿等方法或多种方法组合使用。

  • 维护分片工作:日常的分片维护工作,包括可能存在的扩容、缩容等。上述操作原则上需本着尽量减少数据移动,减少对应用影响的原则。

3).选择拆分字段

在开始分片之前,首先需要确定分片字段(即分片键)。很多例子会使用自增ID或时间戳字段进行拆分,这些不是绝对的;更好的建议是结合业务,进行选择。可考虑按如下策略进行选择:

  • 对系统中执行的SQL进行统计分析,选择出需要分片那个表中最频繁被使用到或最为重要的字段类分片。这其中可能包含一些来自OLAP类的查询,可将此部分SQL排除在外。

  • 如业务非常复杂,存在多种维度划分可能,可以考虑适当多维度字段拆表。必要时可考虑通过业务逻辑进行拆分(而非简单字段划分)。

  • 如涉及到多维度拆分,需维护一定数据冗余。一般主维度可以由程序写入,次维度则异步写入,保证两者最终一致。

  • 如涉及到关联查询的问题,可配合分片策略来实施。如考虑ER表、广播表等配合策略。如非常复杂,也可考虑在OLAP系统中提供支持。数据同步策略上,可考虑推拉结合。

  • 最终选择拆分字段,应是稳定的,不变更的,避免跨片移动问题。

4).选择拆分算法

针对分片算法,常规的有LIST、RANGE、HASH等。根据各拆分算法特点,可进行选择。若范围均匀可采用HASH,冷热数据明显可采用RANGE等。同时可配合一些特性化设计,如采用二级映射方式解决扩缩容问题、特征编码字段满足多特征拆分等。

再谈:分库分表的那些事

这其中最为常见的就是RANGE、HASH,下面说明下。

  • RANGE

    通过数据的范围进行分库分表,是最朴实的一种分库方案,它也可以和其他分库分表方案灵活结合使用。当需要使用分片字段进行范围查找时,RANGE分片策略可快速定位数据进行高效查询。大多数情况下有效避免跨分片查询的问题。在后期扩容时,也比较方便,只需要添加节点即可,无需对其他分片的数据进行迁移。但这种分布方式容易存在数据热点问题。

  • HASH

    虽然分库分表的方案众多,但是Hash分库分表是最大众最普遍的方案。随机分片其实并不是随机,也遵循一定规则。通常采用HASH取模的方式进行分片拆分,所以有时候也称为离散分片。随机分片的数据相对均匀,不容易出现热点和并发访问的瓶颈。但涉及后面数据迁移的话,不太方便。可使用一致性HASH算法在很大程度上避免此问题。此外,离散分片也容易面临跨分片查询的复杂问题。


4. 分库分表问题及解法

数据经过拆分后,相较于传统集中式必然面临一些问题。这也是大家针对分库分表比较诟病的地方。下面针对这些常见问题,统一梳理下。

1).基本CRUD功能

对于开发人员而言,虽然分库分表了,但是其还希望能和单库单表那样的去操作数据库。但从实际使用上看,还是会存在功能不完全兼容问题。这里是需要提前给出兼容性列表,方便开发人员查询,并提供SQL审核能力,规避问题。

2).数据离散问题

分库分表的初衷就是将数据打散,将大表化为小表。因此,针对拆分后的数据离散程度是需要关注的,避免出现热点数据集中存储在某个特定库/表之中,造成读写压力不均、存储空间暴增问题。从上面拆分算法可见,在HASH、LIST容易出现此类问题。一方面,在前期设计中就需考虑热点打散问题;另一方面,针对可能出现的热点也要有必要的解决方案(如重分布)。很多分布式数据库、分库分表中间件产品也内置了热点监控能力,可以方便我们找到潜在热点,及时采取措施化解。

3).跨库关联查询

如果数据拆分后,已经存在不同实例主机上,关联操作将很困难。无论是从架构规范,性能,安全性等方面考虑,一般是禁止跨库关联的。以往类似Oracle DB Link的方式,是不推荐在现实中使用。要解决此问题,一方面可参考后面会谈到的跨片查询的一些解决方案,一方面可考虑在应用层解决数据合并问题或利用汇总库的方式完成。

4).跨片关联查询

如数据拆分后保存在多个分片中,可考虑下面一些策略。

  • 全局表:也叫做数据字典表。可将此类表在每个分片中都保存一份,这样关联就可以在片内完成。这种适用于数据变化不大,不用担心一致性问题。数据规模也不大,不会占用过多空间。

  • 字段冗余:典型的空间换时间,冗余保存字段数据。这种方法适用场景相对有限,适用于依赖字段较少的情况。而冗余字段的数据一致性也较难保证。

  • ER分片:如能先确定表之间存在关联关系,可以将那些存在关联关系的表数据存在同一个分片上,这样能较好避免跨分片问题。

  • 数据组装:通过两次查询得到数据,在上层进行关联拼接。很多分布式数据库、中间件产品都内置了这一能力。但需要关注资源消耗,避免被打爆。

5).跨片复杂查询

当需要对跨片的数据执行分页、排序、函数处理时,也是比较复杂的。

  • 分页:分页是需要按照指定字段排序的。当排序字段是分片字段的时候,通过分片规则可以很容易定位到指定分片;而当排序字段为非分片字段时,情况就比较复杂。为了得到准确结果,需要在不同分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。

  • 排序:类似分页处理。

  • 函数:在使用诸如Max、Min、Sum、Count之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。

6).非分片查询

理想的查询,都是按照分片键进行查询,这样效率也是比较高的。对于非分片键的查询,由于不知道数据落在哪个库上,往往需要遍历所有库,当分库数量多起来,性能会显著降低。解决的方法一般有:

  • 映射表:创建非分片键与分片键的映射关系表,并借此查询出对应分片键,然后再路由查询。

  • 基因法:在非分片键的字段设计上,揉入分片规则,即可通过非分片键反推出分片位置进行查询。

  • 异构数据库:对于一致性要求没那么高的查询,可通过数据冗余将数据存储在异构平台上,供查询使用。

  • 上层汇聚:有些产品做得比较完善,可以很好兼容复杂SQL。但是查询条件中不含有分片键,会导致扫描所有的分片,并在上端做汇聚,效率较低。所以这种方式,仅限于查询频率比较低,匹配数据比较少的情况。

7).分布式ID

在分库分表环境下,数据分布在不同分片上,不能再借助数据库自增长特性直接生成,否则会造成不同分片上的数据表主键重复。常规做法是采用第三方独立的ID服务或在不同分片上使用单独策略,避免ID冲突。

8).分布式事务

由于涉及到的数据变更分布在不同库中,不可避免会带来跨库事务问题。常规分布式事务做法是使用XA协议+两阶段提交来处理。分布式事务能最大限度保证了数据库操作的原子性。但是在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着节点数的增多,这种趋势会愈发严重,从而成为系统在数据库层面上水平扩展的瓶颈。对于那些性能要求很高,但对一致性要求不高的系统,可不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误后立即回滚的方式不同,事务补偿是一种事后检查补救的措施。常见的实现方式有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源同步等。

9).迁移&扩容

很少有项目在初期就考虑使用分库分表设计,一般都是在业务高速发展面临性能、存储瓶颈时才考虑。因此,不可避免涉及到历史数据库迁移。常规做法是通过程序读取历史数据,然后按照指定的分片规则写入到各分片节点中。此外,运行一段时间后也会考虑扩容问题。如果是采用随机分片,则需要重分布策略,会比较麻烦;如果是采用范围分片,只需要添加节点就可以自动扩容。


5. 无法回避的问题:NewSQL vs 分库分表

如前面所谈到的,NewSQL的出现使得面对海量数据有了新的选择。也经常看到在各种选型场合将分库分表与NewSQL做对比。这是一个不可回避的问题。那两者到底各有何所长呢?

再谈:分库分表的那些事

从上面两者的对比可见,分库分表模式体现出一种平衡、妥协的架构风格,是一个面向应用型的设计;而NewSQL数据库则是通用底层技术软件要求。

 企业如何选择 

企业在做技术选择之前,需关注自身痛点,结合上述两种技术方向差异做出选择。这里可通过几个问题,判断下核心诉求是什么?

  • 分片是否必须做到对应用完全透明?

  • 缺省的分片策略可否满足基本需要?

  • 强一致事务是否必须在数据库层解决?

  • 需要扩容频率是否已超出了自身运维能力?

  • 相比响应时间,更看重吞吐?

  • 是否有熟悉NewSQL数据库的DBA团队?

如果以上大部分问题是肯定答案,那么可考虑用NewSQL数据库。虽然前期可能需要一定的学习成本,但未来收益也会更高。当然选择NewSQL数据库,也要做好承担一定风险的准备。如果还未做出抉择,不妨再想想下面几个问题:

  • 最终一致性是否可以满足实际场景?

  • 扩容、DDL等操作是否有系统维护窗口?

  • 对响应时间是否比吞吐更敏感?

  • 是否有超大规模高并发的需求?

  • 是否已有传统数据库DBA人才的积累?

  • 是否可容忍分库分表对应用的侵入?

  • 是否对技术风险容忍度很低?

  • 是否已经有较为成熟数据库方案,不考虑底层技术栈更换?

如果这些问题有多数是肯定的,那还是分库分表吧。在软件领域很少有完美的解决方案,NewSQL数据库也不是数据分布式架构的银弹。相比而言分库分表是一个代价更低、风险更小的方案,它最大程度复用传统关系数据库生态且定制化能力更强。在当前NewSQL数据库还未完全成熟阶段,分库分表可以说是一个上限低但下限高的方案,尤其传统行业的核心系统,不失为一种更佳稳妥的选择。



韩锋频道:

关注技术、管理、随想。


长按扫码可关注