数据库性能优化丨分库分表设计实战经验篇

徐进
原创 2294       2019-04-19  

随着互联网的发展,之前企业级应用面对待处理的数据量越来越大,这对数据库资源的性能与成本提出了相当大的挑战,为之,业界便提出了分库分表的技术解决方案。

顾名思义,分库分表就是将原先集中在一张表或一个库的数据,按照一定规则进行拆分,然后将拆分后的数据通过分布式方式存储到多个表上。

这样,当数据量增加时,我们可以通过平行扩展分库和分表的数量来进行系统扩容。然而,任何一种技术都是一柄双刃剑,在给你带来某些便利的同时,也会为你捎上这里或那里的不便。分库分表在解决海量数据存储问题的背后,也引入了数据存储成本控制、数据聚合查询效率、数据一致性保障等方面的问题。接下来,为大家分享一下我们在用户中心项目中使用分库分表技术的一些架构经验,希望可以帮助设计人员更完整的了解分库分表技术的设计应用。

在设计之前,先理解一句话:任何一种技术的选择都是需要支付成本的。这里指的成本包含:简单直接的购买成本、掌握新技术的学习成本、技术替代所需的重构成本、产品上线后的部署和运维成本、技术引入过程产生的时间和机会成本。因此,若是一个仅仅百万量级的数据处理系统,你为之投入的亿级数据分库分表设计在日后可能会面临无法收回成本的尴尬境地。在用户中心项目中,我们面临4亿的用户存量数据,并且,后期该系统日后需要对接10亿级的用户交易日流水数据。针对这一场景,其一,目前没有一个数据库支持这一量级的单实例、单表存储;其二,用户愿意为4亿用户数据支付分库分表带来的额外部署成本。于是,架构组决定引入分库分表技术,解决实时交易相关的联机数据存储问题。

由于分库分表会将原本单表的数据拆分后,分布式存储在不同的分表和分库实例上,因此,原本可以简单的通过数据库事务确保的数据一致性,但在分库分表设计中,则需要引入代价高昂的分布式事务,而且即便采用了分布式事务,你依然需要为分布式存储支付额外的一致性和性能成本。为此,我们需要严格的估算系统中各类库表的实际容量,将那些小规模的库表结构(例如:系统类表、ToB类表)留在单独的数据库实例下,这样做可以降低后期此类数据处理功能的开发和运维成本。留下来,将那些热存活数据超过千万级的库表结构(主要是ToC类表)设计成分库分表形式。

选好了需要分库分表的数据,便可以按照起始的数据量估算来设计分表数和分库结构。用户中心系统面临亿级存量账户数据和十亿级日交易流水数据,我们先估算单个分表的最大存储上线为百万级数据,简单计算,1000张分表可以容纳十亿级日交易流水数据,100张分表可以容纳亿级存量账户数据。考虑到一个系统采用统一的规则可以降低系统设计的复杂度,因此,我们统一使用1000张分表规模来分布式存储所有分库分表数据。但是,该如何来分配这1000张分表的库和实例呢?此时,我们需要与用户协商系统上线初期的部署预算,以及日后的扩容方案。用户希望初期部署投入2个数据库实例,后期可以在不做太大调整的前提下进行两次翻倍形式的硬件资源扩容。于是,我们为用户设计:


  • 上线阶段:2实例X20分库X25分表
  • 第一次扩容:4实例X10分库X25分表
  • 第二次扩容:8实例X5分库X25分表


这样,我们在扩容时只需要简单的申请硬件资源,迁移分库数据文件即可,无需进行分表迁移。


完成了分库分表结构设计,我们需要解决分表数据操作引入的数据一致性问题。由于前面约定了分表上只存储ToC类业务的热存活数据,因此,面对分表的操作被限制在联机实时类业务上。脱机类的查询与统计类业务,则被我们移到了数仓等离线存储中。但是,联机交易对数据的一致性要求非常高,数据校验、多表更新等操作面临事务一致的挑战。我们再次与业务与用户进行协商,以了解系统的一致性需求级别,最终,大家达成一致:在基金行业的账户类系统上,针对单一账户的多次日间开销户等操作,返回确定的处理结果可以满足系统的一致性要求。我们放弃了成本较高的分布式事务控制,采用支持开销户操作重入得方式来确保一致。简单的举例:如用户需要开户,第一次开户流程进行了一半由于存在不确定的问题中断了,此时,无需回滚已入数据(数据生效标志为“未生效”)。之后,用户发现开户处理并未成功,继续开户,系统可以使用前一流程的部分数据,然后继续处理后半部分逻辑直至开户流程全部结束。这样的设计,开户过程中的每个环节都可以重入,如果该环节未生成数据则新增,否则就使用已生成的数据进入下一个环节。但该设计会导致系统中存留大量的“未生效”记录,浪费了系统的存储资源。所以,我们设计在系统日终的批处理环节,对当日的“未生效”记录进行统一清理。这个设计大大提高了分库分表系统下的系统单位时间处理量,同时,也可以在需求范围内确保数据的一致性。当然,我们也讨论过,在资金、份额交易类业务中,这样的方案是无法满足一致性要求的,还需要采用TCC方式的分布式事务进行保障。


解决了系统一致性,我们又面临聚合类查询统计和关联表查询类交易的问题。在单库单表下,我们设计一张数据表可以为之添加多个索引,已确保不同维度的查询均能得到可以接受的性能支持。但是,在分库分表下,为了消除热点,用户表与账户表本身的分表逻辑就不一样,前者使用客户号规则分表,后者使用账号规则分表。这样可以确保每一张表的数据能被均匀分配到各个分表当中。但是面临知道客户号进行查询账户信息类的业务,便无法简单的采用关联查询和索引来实现高效检索了。解决这个问题,我们设计了单独的索引体系,系统额外建立了客户号与资金账号、客户号与交易账号、身份证号与客户号、电话与客户号、邮箱号与客户号等等关系索引表,索引表独立进行分库分表。这样如果用户知道身份证号要查客户信息,可以先通过身份证号计算索引表的分片号进行检索获取客户号,然后通过客户号计算客户信息表的分片号进行最终的信息查询,从而解决了分库分表下的关联查询问题。对于聚合类查询统计,则将所有的统计类查询都归入数仓离线计算,避免分库分表在日间交易时因统计类查询导致数据库负载过高影响在线业务。至于聚合类流水查询,我们通过约定查询结果数量、限制流水关联表查询避免数据库查询压力。


在分库分表设计过程中,我们还对分表规则的选型展开过交流,用户提供了历史系统的运维经验告诉我们,在现有的数据量下之前用户使用分表字段Hash取模方案存在一些问题:1、极端情况下Hash取模会出现热点不均,2、Hash算法在TPS非常大的情况下会导致分库分表依赖的数据代理节点的CPU资源成为瓶颈。为了解决上述问题,我们最终决定采用独立的整形分表字段来标识数据分片,而该字段的生成规则简单的采用递增取模方式,这样既可以做到热点均匀,也降低了数据代理节点的CPU资源处理代价。

现在看来,我们的分库分表方案基本完整了,方案的产生过程中,我们与业务和客户针对数据的特性、算法的选择、业务的重构、一致性的要求、部署的成本等方面展开了深入的交流。最后,还是强调一下:在架构与设计领域,不存在完美的银弹,只存在合适的方案。

恒生技术之眼原创文章,未经授权禁止转载。详情见转载须知

联系我们

恒 生 技 术 之 眼