本文介绍了ERP系统因数据量增大导致性能下降的问题,以及苍穹平台如何通过分库分表解决这些问题。首先讨论了数据库优化措施,如硬件升级、SQL优化、表结构优化、架构优化和数据库配置调整。随后,文章阐述了随着系统架构的演变,从单应用单数据库到多应用独立数据库,分库分表的必要性。接着,文章详细介绍了分库分表的实现方案,包括客户端分片、代理层分片和服务端分片,以及分库分表的类型,如垂直分表和水平分表。此外,还讨论了分库分表可能带来的问题,如跨库查询、分布式事务、全局ID和数据迁移,并提供了相应的解决方案。最后,文章介绍了苍穹平台的分库分表方案,包括垂直分库、水平分表、数据归档和读写分离,并详细描述了苍穹自研的水平分表工具XDB的功能特性和实现方式。
1 前言
ERP系统运行一段时间,部分表单会积累大量的数据,对应的数据库表记录数急剧上升,导致查询效率降低,引发系统性能问题。性能优化专题(二)中提到苍穹平台为解决单数据库存储,单表数据量过大、慢查询等性能问题,提供了分库分表的解决方案。
分库分表并非新技术,但实际使用中,由于理解不够深入,开发人员可能会存在一些疑问:什么时候应该做分库分表,也就是系统做了哪些优化后才考虑分库分表?为什么要分库分表?分库分表有哪些类型,会带来什么问题,如何解决这些问题?业界主流的开源组件是如何实现分库分表的,苍穹分库分表又是如何实现?本篇指南将围绕以上问题展开分析,帮助苍穹定制化开发者深入理解分库分表相关技术,从而更高效合理的使用分库分表。
2分库分表技术演进
2.1数据库优化
并不是系统一开始出现性能瓶颈就考虑使用分库分表,毕竟分库分表有一定的复杂性和开销,很多时候应用系统出现性能问题,大多是数据库变慢了或者数据库宕机导致,如高并发时数据库连接数不够,数据量太大导致查询效率降低,或因存储空间问题数据库主机性能下降了等等,下面先来了解在使用分库分表前,可以在数据库层面做哪些优化,对数据库性能优化有一个整体的认识。
2.1.1硬件层面
上述提及的数据库性能瓶颈,归根结底是受系统硬件的限制,如CPU,内存,磁盘,网络等。集中式系统架构,只能通过增加系统配置缓解硬件带来的性能瓶颈,如更换CPU,扩展内存,扩展磁盘空间,增加网络带宽等。但这种优化方式依然面临两个问题,一是容易遇到新的瓶颈,二是收效和投入比太低。
2.1.2SQL 与索引
当SQL 语句写得非常复杂,如关联的表多,查询条件多,查询消耗时间长,因SQL 语句是开发者自己编写的,可控性是最高的,所以首先检查SQL是否使用了索引或索引使用是否最佳。(SQL优化相关知识,可参考苍穹定制化开发性能优化系列(一))
2.1.3表与存储引擎
如果SQL 语句本身没有问题,就需要考虑表结构的设计,比如字段类型和长度的选择是否准确,表结构是否可以更好的进行拆分或者合并,不同的表选择的存储引擎是否最佳,是否可以分区等。
2.1.4架构
表结构未发现问题,则需上升到数据库服务的层面,从架构上进行优化。
1. 因为数据在磁盘上存储,如果创建了索引依然很慢,可将数据在内存缓存,如使用Redis。查询数据先查缓存,再查数据库,这样既可缓解数据库压力,又可提升查询速度;
2. 为减少单数据库服务器的读写压力,在架构层面可考虑集群部署数据库服务器(一主一从或一主多从)(为保证读写数据一致性问题,可通过数据库自动同步机制,如Mysql通过binlog日志实现主从一致)。配置主从同步之后,可实行读写分离,将写操作都写到主节点,读操作通过从节点查询,但是读写分离还是存在一些问题:
(1)单个主节点,写的操作压力并未得到分摊;
(2)所有节点都存储相同数据,当单个节点出现存储瓶颈的时候,磁盘空间不够,其他的节点也会遇到同样问题。这种情况下,需要用到分布式中比较重要的思想-分片,即每个节点只存储全量数据的一部分,也就是分库分表。
2.1.5数据库配置
如果通过架构层面未解决问题,或者机器配置很高,但资源利用不充分,性能未发挥到极致,还可以优化数据库的配置,比如连接数,缓冲区大小等等。
2.2架构演进与分库分表
分库分表技术是伴随着系统架构的演变而发展的,从最初的单应用单数据库到现在的多应用独立数据库,下面结合架构的演进过程剖析分库分表是如何随之诞生的。
2.2.1 单应用单数据库
单体架构应用的特点是所有的代码在一个工程里,以一个war包形式部署到tomcat,最后运行在一个java进程中。如下图,核心系统包含多个应用,只有一个数据库,数据库中可能存在上千张表。为了适应业务的发展,核心系统不断增加功能,导致程序代码越来越多,系统越来越臃肿。通过搭建集群,负载均衡,增加缓存,优化数据库,业务代码等都无法解决系统压力,这时系统拆分就势在必行了,也就是进入了多应用单数据库的阶段。
2.2.2多应用单数据库
对代码解耦,职责拆分之后,改造成多子系统共用一个数据库,这种架构从性能和存储的角度来说,随着业务的持续增长,会有更多的系统来访问核心数据库,但是单个物理数据库能够支撑的并发量是有限的,各业务系统之间还会产生竞争,最终会导致应用性能下降,甚至拖垮整个系统。这种情况下,有必要对各个子系统的数据库拆分,也就是进入多应用多数据库的阶段。
2.2.3 多应用独立数据库
1. 多应用独立数据库即每个业务系统都有自己的数据库,也就是分库,分库其实是解决系统性能问题对系统进行拆分的必然结果。如当前微服务架构,很多时候只拆应用不拆分数据库,不能解决根本问题。
2.分库之后,部分表的数据快速增长也会导致查询效率明显下降,所以还进一步进行分表。分表主要是为了减少单张表的数据量,提升单表查询性能。
那么对于分表,一张表的数据量达到多少的时候,需要做分表?
参考业界规范,阿里巴巴Java开发手册推荐单表记录达到500万或单表存储大小超过2GB。在苍穹平台中,目前是单表行数5000万以上或表单总行数1亿以上推荐使用分表。如果可预见的业务增长达到不了这个数量级,不建议做分表。
开发设计时应考虑业务实际情况,如果表结构合理,字段不太多,且索引使用正确的情况下,单表存储几千万的数据一般是完全没有问题的,如其他优化方案效果不大,依然存在性能瓶颈,才考虑分库分表。
2.3实现方案
根据不同层级的分片逻辑,目前分库分表的主要实现方案可分为三大类:客户端分片,代理分片,服务端分片。
2.3.1客户端分片
客户端分片是指在数据库的应用层直接操作分片逻辑,分片规则需要在同一应用的多个节点之间进行同步,每个应用层都嵌入一个操作切片的逻辑实现(分片规则),一般通过依赖jar包形式。具体实现方案有三种:
1. 代码层:这种方式相对比较简单,在数据连接层(比如SSM项目中的DAO层),连接到某一个数据源之前,可以先根据配置的分片规则,判断需要连接到哪些数据库节点和数据库表等等,再建立连接;
2. 框架层:ORM层,在使用持久化框架连接数据库时指定数据源,也就是把分片规则在ORM框架中实现或者通过ORM框架支持的扩展机制完成分库分表的逻辑。比如MyBatis,可以基于MyBatis的拦截机制(拦截query 和update 方法),实现数据源的选择,一般不建议对原生ORM框架进行改动。
3. 驱动层:无论主流的MyBatis、Hibernate,还是自研的ORM框架,本质上都是对JDBC的封装,因此可对JDBC的核心对象(DataSource、Connection、Statement、ResultSet)进行封装或者拦截实现分库分表。目前主流的 Sharding-JDBC就是基于此方式实现的。
2.3.2代理层分片(中间层)
客户端分片虽然简单,但不同的项目都要做同样的改动,不同编程语言也有不同的实现。代理层的实现,类sidecar的设计思想,在应用与DB之间增加代理层,把分片的路由规则配置在代理层。该代理层实现后端数据分片与路由查询,,将自己伪装成数据库,接收业务端连接,处理业务端请求,解析或者转发到真正的数据库中。目前主流的中间件Mycat 、Sharding-Proxy、Cobar都属于这一层。
2.3.3服务端分片
服务端分片是通过使用某些分布式数据库,如TiDB、SequoiaDB等开源NewSQL,在存储引擎层实现了数据分片,对上层应用保持透明或者是通过修改关系型数据库的源码,将其改造成分布式数据库。
2.4分库分表类型
数据库拆分方式有两种:垂直拆分和水平拆分。垂直拆分基于表或字段的划分,表结构不同;水平拆分是基于数据的划分,表结构相同,数据不同。
2.4.1垂直分表
垂直分表在日常开发和设计中比较常见,通俗的叫法是“大表拆小表”,是基于关系型数据库中的“列”(字段)进行拆分的。通常情况,某个表中的字段比较多,可以新建一张“扩展表”,将不经常使用或者较大长度的字段拆分到“扩展表”中,是同一个库内的分表。
2.4.2垂直分库
垂直分库也就是前面所提到的多应用多数据库,多数情况是从业务的维度进行划分,将原来存储在一个库的不同表拆分至不同的数据库中。
2.4.3水平分表
这里的水平分表指的是单库的水平分表,将表中不同的数据行按照一定的规律分布到不同的表中,通过该方式降低单表数据量,如费用明细表,还款记录表等按照日期维度,每个月建立一张表。同一数据库实例分表的方式虽然可以一定程度缓解单表查询性能问题,但是并不能解决单机存储的瓶颈,因为所有的表占用的是相同的磁盘存储空间。
2.4.4水平分库
水平分库可以理解成是多库的水平分表,与单库的水平分表思想相同,只是将这些拆分出来的表保存在不同的数据库中。
2.5局限性和解决方案
前面提到过数据只有达到一定量级,并且尝试过其他优化方式依然存在瓶颈才考虑使用分库分表,因为分库分表会让系统更复杂,产生如跨库查询、分布式事务、全局唯一ID等等问题。
2.5.1跨库关联查询
例如,查询还款信息时需要关联客户的信息,但还款数据和客户信息是在不同的数据库中的,无法使用join的方式关联查询。对于跨库的查询,有以下几种通用的解决方案:
1. 字段冗余:若还款表只保存了客户ID,要获取客户姓名,需要关联客户库中的客户表,那么可以将客户姓名直接保存到还款表中,通过这种方式避免跨库关联查询(反范式设计)。
2. 数据同步: 比如A系统要查询B系统的某一张表,可以在A系统建立B系统对应的表,通过ETL、MQ或者canal定时同步B系统的表数据。
3. 全局表:一些基础信息表,如数据字典表、行政区划表,很多业务系统都会用到,如果放在核心系统,每个系统都需要调核心系统的接口做跨库查询,为减少接口调用次数,可以在所有分库都保存相同的基础数据,各系统自行维护,保持数据的同步。
4. 系统层组装:在不同的数据库节点,各自利用查询条件,把符合条件数据的数据查询出来,然后在内存中重新组装,返回给客户端。
2.5.2分布式事务
如果在一个流程操作中,涉及到两个应用的数据更新操作,两个操作必须同时成功或者同时失败,否则会出现数据一致性问题。如果两个应用共用数据库中时可通过本地事务进行控制,但是分库之后,在不同的数据库中,必须有一个协调者角色,而且要分成多个阶段,一般是先确定都能成功才成功,只要有一个不能成功,则全部失败。基于CAP理论和BASE理论,目前比较常见的分布式解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知等,或者可考虑使用开源的分布式事务解决方案,如seta,基于MQ实现最终一致性等。
2.5.3排序、分页、函数计算问题
跨库查询时,会出现分页,排序的问题。例如有两个数据库节点,节点1 存的是奇数;节点2 存的是偶数,如需查询10 条数据,需要在两个节点上各取出10 条(可能存在需要的10 条数据都在第2 个节点上的情况),然后合并数据,重新排序。max、min、sum、count 之类的函数在进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。
2.5.4全局ID问题
在同一数据库中,是可以保证ID不重复的,但水平分表之后,每个表都按照自己的规则自增,不同表之间有可能出现ID 重复。目前解决分布式全局ID的方案有雪花算法,UUID、数据库生成方式、Redis等(具体的实现方式此处不展开,需要了解的可自行查阅相关资料或关注后续篇幅)
2.5.5数据迁移问题
一般项目初期很少考虑分片设计,多数是在业务快速发展,遭遇性能和存储的瓶颈时才提上日程,因此需要考虑历史数据迁移,一般做法就是通过程序先读出历史数据,然后按照指定的分片规则将数据写入到各个分片节点中,在分片过程中产生新的数据,需要再次做同步处理。
3苍穹分库分表
3.1总体介绍
为解决大数据量带来的存储和性能问题,苍穹的解决方案主要有垂直分库、水平分表、数据归档和读写分离,整体框架如下图所示:
3.1.1垂直分库
把业务应用模块按一定粒度进行垂直拆分(云+应用),存储到对应的独立数据库,以解决单库过大问题。
3.1.2水平分表
水平分表是苍穹自主研发的针对苍穹产品的库内水平分表,提供了大数据量表水平切分存储的整体解决方案,通过对大数据量的表单进行水平切分存储,以减少单个物理表的数据量,提高增删改查性能。
3.1.3数据归档(水平分库)
苍穹平台没有在技术层面上做水平分库,而是在业务层面上做,解决时间维度引起的数据累积问题,把已经完结的、不常用的业务数据转移到独立的存储(数据库)中,即数据归档,这其实也是水平分库。
3.1.4读写分离
由于大多数项目在实际使用中的场景都是读远多于写的,数据库的“写”相较于“读”更加耗时,且写入操作会影响查询的效率,苍穹平台提供了读写分离功能以分摊数据库的压力,提高性能。目前苍穹中列表、F7、报表等已默认启用读写分离功能。
3.2水平分表实现
3.2.1实现方案
1. 由于目前主流的中间件不支持多属性分片、3+级ER分片关联,提供的分片定位条件比较少等,平台基于苍穹动态领域模型,动态表单,多属性分片等特性自研了水平分表(XDB),XDB是以jar模式运行的,而不是一个独立运行的服务。相比于主流的中间件,XDB小而精,更适配苍穹和符合企业级特性。
XDB功能特性:
(1)适配苍穹领域模型:多级关联(4 级:表头-分录-子分录-扩展表),理论上不限级别。
(2)分片策略:多属性分片、多种分片策略。
(3)内置PK 索引、自定义全局索引。
(4)支持最大限度分片条件定位: =、!=、>、>=、<、<=、in、not in、between and、like、not like。
(5)支持大部分SQL 特征:union、union all、order by、distinct、top(KSQL 分页)、sum、count、min、max 等。
(6)支持分片SQL Hint:适配优化。
(7)支持更新分片属性。
(8)自适应并发查询。
(9)在线数据迁移与管理。
(10)分布式运行:缓存、锁、表缓存版本等。
(11)支持多种数据库:目前支持MySQL、Oracle、PostgreSQL。
(12)辅助功能:按条件计算分片、表名映射、操作日志、数据量统计、指标采集与监控。
2. XDB是在SQL层支持的,在系统运行期动态分表,表单是否分表不影响功能的使用,业务通过ORM组件查询,依旧使用原始表名(逻辑表)进行访问,数据访问最终都会调用平台的DB接口,在这个接口执行过程中,分析SQL中的表进行处理,实现对SQL进行解析,路由和改写等。
(1)ORM数据访问流程图:
(2)数据访问部分源码:
a. DB.class:判断是否开启分片,分片查询与没有分片查询的分界点
如果是分片,初始化配置,执行XDBImpl.query()
b. sql处理过程:
ShardingEngineImpl.slice()对不同的查询语句做处理
其他语句处理,以select语句为例:
ConditionShardingSQL.sharding:
(3)分片过程部分源码:
a. 分片任务线程:ShardTaskMovingHandler.class
b. ShardActionEnableAnalysis.doDataMoving():获取分片属性,分片策略开始执行分片
注:由于调用链路较长,以上仅为小部分源码调试,主要是方法入口和出口,为开发者在调试的过程中提供思路,感兴趣的可以进一步调试和交流。
3.2.2跨库查询
使用平台提供的ORM组件解决跨库查询,核心是通过DB的routeKey进行路由查询,routeKey在表单设计时已自动指定。对于开发者而言,查询业务数据通过平台封装的BusinessDataServiceHelper或QueryServiceHelper,方法中已实现路由选择封装。获取routeKey核心源码如下:
(1)入口:ORMImplStandard.class
(2)查询元数据表T_META_ENTITY,FDATA 字段 获取routeKey
3.2.3分布式事务
使用苍穹平台自研TCC框架实现最终一致性解决分布式事务问题(分布式问题具体的实现方式此处不展开,需要了解的可自行查阅相关资料或关注后续篇幅)
3.2.4数据迁移
苍穹实现数据迁移的实现思路是在完成分片的配置后,启用分片时首先会将对应的表进行读写锁定,确保在此过程中无法操作数据库,避免操作中产生数据差异后续处理数据同步,然后将分表表单的相关信息和任务存至任务表,通过定时任务扫描该表进行数据迁移操作。
(1)部分源码解析:
a. 读写锁定:ShardingConfigEditPlugin.beforeDoOperation
b. 存入分片任务表t_cbs_shard_task:
c. 系统启动时,加载水平分表配置,启动数据迁移任务:
d. ShardTaskService.class数据迁移任务执行:
3.2.5排序、分页问题
(1)分页:查多个分表时,修改SQL从第一页开始查询,合并后再分页;
(2) 排序:下沉到每个分片进行查询,再用多流贪婪merge+二分法比较merge;
3.3水平分表使用
详见附件《水平分表使用手册》
水平分表使用手册.pdf(2.01MB)
推荐阅读