文章摘要:当单表数据达到千万以上时,通过加索引或者表分区优化提升的效果就比较有限了,应该如何应对呢???
当MySQL数据库的单表数据量达到千万级别以上时,不管是业务逻辑的查询,还是更新,或者删除都会使得数据库的平均响应时间过长。这时再通过(上)篇中的单表SQL优化技术解决方案收效就微乎其微了。对于如此大量数据,我们还可以利用以下几种业务平台的架构方案进行进一步的技术改造。
当单库数据量比较大影响了查询/更新/删除的SQL执行效率时,我们可以直接想到在不影响业务逻辑的前提下,如果可以直接减少数据库中单表的数据量,那就能够达到我们的优化数据库的目标。该种方案称之为“分离热点数据”。
基于最近时间段内写入数据一般会成为热点数据的假设,同时考虑到在业务平台中对于存量历史数据的需求基本都是查询,而对于平台中的最近时段生成的数据一般会涉及查询/更新/删除等操作。因此可以设定一个时间的阀值,比如6个月,根据这个时间点来进行分表。对于6个月以内的数据,存热点库的数据表,6个月以上的数据存在历史库数据表里,业务平台的应用工程通过配置多数据源以及增加代理层即可实现服务对于不同数据库的访问,用以区分对热点数据库和历史库的访问。由于历史存量数据会越来越多,因此可以根据业务需要对历史库内的数据进行实现MYSQL分区表。而业务平台迁移历史数据可以在业务平台访问压力和流量较小的午夜通过设置Quartz定时任务程序的方式执行迁移。该种业务平台架构方案图如下:
为了缓解数据库单库单表的IO压力,尽可能地降低数据库操作(CRUD)平均响应时间。我们可以采用本地缓存或者分布式缓存的技术方案为DB缓解读写压力。
这里的本地缓存主要指通过利用JDK原有的诸如HashMap/ConcruuentHashMap数据结构或者Google guava包中的localCache在应用服务器内部构建的一块缓存区域。分布式缓存即指redis、mencached这一类的缓存中间件(限于篇幅和主题,对于这两类缓存深度技术和应用优化的介绍将在后续的篇幅中会单独介绍)。
对于业务平台中的局数据(比如,资源规格、类型、价格、模板等),该类数据从写入至数据表后就极少改动,其业务需求通常是只读。考虑到该特点,可以将这一部分数据在应用服务启动时就放入本地/分布式缓存中,服务需要读取时直接从缓存中读取,这样即可在一定程度上减少对数据库读的压力。
而对于另外一部分的业务数据(比如订单、资源、性能、用户账户),往往涉及新增/修改/删除等对数据库写的操作,且在一些业务场景中(比如大促/营销活动)这部分数据经常容易成为热点数据。因此可以考虑在数据库并发量较大的情况下,用分布式缓存做为缓冲,在缓存中先完成数据的汇总以及其他业务逻辑操作,然后使用批量插入/更新/删除的方式对数据库完成一次或若干次的批处理操作。这样可以在保证平台业务不受影响的同时,减轻数据库的写压力。下图即为热点数据缓冲汇总统计的示意图:
当数据库中一张单表的数据量达到几千万时,且还有不断增长趋势,同时系统的并发访问量达到一定规模的时,前面篇幅中介绍的数据表的B-Tree/B+ Tree索引就无法起作用了。除非是索引覆盖查询,否则数据库服务器需要根据索引扫描的结果回表,查询所有符合条件的记录。如果数据量巨大,这将产生大量随机I/O,随之,数据库的平均响应时间将大到不可接受的程度。另外,索引维护(磁盘空间、I/O操作)的代价也非常高。
为了解决单台数据库服务器的性能问题,提高系统的吞吐量时,就需要根据业务逻辑把表拆分成若干个,分别放在不同的数据库服务器中以降低单台DB的负载和缓解单库IO的读写压力,降低访问数据库的平均响应时间。
这里主要是指按业务平台的不同类型功能模块进行拆分,比如分为订单库、资源库、用户库。拆分之后,每个业务平台中的应用工程只访问对应的业务数据库,一方面将单点数据库拆分成了多个,分摊了单库的读写IO压力;另一方面,拆分后的数据库各自独立,实现了业务隔离,不再互相影响。下图为“垂直拆分”方式的结构示意图:
按业务垂直切分后,可能有些单表数据还是很大,访问性能低下,这时需要对这个单库单表上的数据进行水平切分。按照一定的分片算法(比如按照主键ID的Hash)将同一个表的数据进行切分并分别保存到不同数据库的数据表中,且这些数据库中的表结构完全相同。以下为“水平拆分”示意图:
在SOA/微服务架构普遍流行的今天,从某种意义上来说,业务平台的建设基本都是按照“垂直拆分”的思路来的,即不同业务子系统访问对应不同的业务数据库。但是,“垂直拆分”的方案并没有根本解决当某一业务微服务的数据量剧增后对单库带来的读写IO压力。因此同一个业务平台中,比如资源/性能数据库,在“垂直拆分”后,我们一般会考虑“水平拆分”来应对单库单表不断增加的场景。
在这里讲到的“水平拆分”其实跟上篇中提到的MySQL分区表有些类似,只是不同之处在于分区表是在单库的情况下,通过MYSQL存储引擎来实现水平拆分,平台系统本身的业务逻辑不需要感知这一改变。
“水平拆分”优点:
a、经过拆分后,不存在单库数据量大和高并发的性能瓶颈;
b、提高了系统的稳定性和负载能力;
c、缓解单库IO压力;
“水平拆分”缺点:
a、分库事务的一致性难以解决;
b、跨库Join表性能问题,逻辑复杂;
c、跨库count/order by/group by以及聚合函数问题;
d、切分策略如何选择,策略问题很可能导致数据分布不均匀问题;
e、全局主键问题;
a、跨库事务问题:
解决跨库事务问题主要可以通过两种方法。第一种方法,使用前面篇幅介绍的分布式事务,简单有效但是性能代价高。第二种方式,应用程序和数据库共同控制保证,将一个跨多个数据库的分布式事务分拆成多个仅处于单个数据库上面的小事务,并通过应用程序来总控各个小事务。该方案优点在于能够灵活控制,缺点在于改造量较大。
b、跨库Join表的问题
对于业务平台的数据持久层来说,涉及复杂的Join多表查询在所难免。解决这一问题的普遍做法是分两次查询解决。在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。
c、跨节点的count,order by,group by以及聚合函数问题
与解决跨节点join问题的类似,只需要分别在各个单库上得到结果后在业务应用端进行合并。和join不同的是每个结点的查询可以采用多线程方式并行执行(在jdk8中可以用CompletableFuture解决),因此很多时候它的合并速度要比单个大数据量的表快很多。
d、数据分片策略选择
水平拆分中一个比较重要的问题就是按照什么逻辑策略来进行数据分片(即为拆分库表)。一种方案是按照地域类的属性进行拆分;另外一种方案则是按照订单ID/用户ID进行拆分。按照地域类的属性进行拆分会使得数据聚合度比较高,做聚合查询比较简单,实现也相对简单,缺点是数据分布不均匀。按订单ID拆分则正相反,优点是数据分布均匀,不会出现一个数据库数据极大或极小的情况,缺点是数据太分散,不利于做聚合查询。比如,按订单ID拆分后,一个客户的订单可能分布在不同的数据库中,查询一个客户下面的所有订单,可能需要查询多个数据库。对于这种情况,一种解决方案是将需要聚合查询的数据做冗余表,冗余的表不做拆分,同时在业务开发过程中,减少聚合查询。
e、全局主键问题
原本依赖数据库生成主键(比如自增)的表在拆分后需要自己实现主键的生成,因为一般拆分规则是建立在主键上的,所以在插入新数据时需要确定主键后才能找到存储的表。在实际应用中,可以参考flickr的全局主键生成方案和uuid的全局主键生成方案。
那么问题来了:采用分库分表技术方案的话,用开源方案还是自研?
一般在业务平台建设前期综合评估系统容量、性能、吞吐量等因素后,会考虑采用分库分表的解决方案,那么就会遇到以上的问题。如果自主研发的话,时间周期长、成本高、项目风险大,因此都会考虑采用目前业界比较成熟的开源分库分表方案。下面主要阐述几种开源的分库分表解决方案:
Sharding-JDBC是当当网开源的分布式数据库中间件,其代表了客户端类的分库分表框架。它定位为轻量级java框架,由客户端直连数据库,以jar包形式提供服务,未使用中间层,无需额外部署,无其他依赖,数据库运维人员无需改变原有的运维方式,即为增强版的JDBC驱动,旧代码迁移成本几乎为零。同时,Sharding-JDBC完整的实现了分库分表,读写分离和分布式主键功能,并初步实现了柔性事务。下面是Sharding-JDBC的框架图:
从上面的框架图中可以看出Sharding-JDBC这一款分库分表的中间件分为分片规则配置、SQL解析、SQL改写、SQL路由、SQL执行以及结果归并等模块。
a.分片规则配置
Sharding-JDBC的分片逻辑非常灵活,支持分片策略自定义、复数分片键、多运算符分片等功能。举个例子:根据用户ID分库/订单ID分表这种分库分表结合的分片策略;或根据年分库,月份+用户区域ID分表这样的多片键分片。Sharding-JDBC除了支持等号运算符进行分片,还支持in/between运算符分片,提供了更加强大的分片功能。Sharding-JDBC提供了spring命名空间用于简化配置,以及规则引擎用于简化策略编写。
b.JDBC规范重写
Sharding-JDBC中间件对标准JDBC规范的重写思路是针对DataSource、Connection、Statement、PreparedStatement和ResultSet五个核心接口封装,将多个JDBC实现类集合(如:MySQL JDBC实现/DBCP JDBC实现等)纳入Sharding-JDBC实现类管理。
c.SQL解析
SQL解析作为分库分表类中间件框架的核心之一,其性能和兼容性是最重要的衡量指标。目前常见的SQL解析器主要有fdb/jsqlparser和Druid。Sharding-JDBC采用解析速度最快的Druid作为SQL解析器。
d.SQL改写
这里一部分是将分表的逻辑表名称替换为真实表名称。另一部分是根据SQL解析结果替换一些在分片环境中不正确的功能。
e. SQL路由
SQL路由是指根据分片规则配置,将待执行的SQL定位至真正的DB数据源。
f. SQL执行
这里指的是路由至真实的DB数据源后,Sharding-JDBC将采用多线程并发执行SQL,并完成对addBatch等批量方法的处理。
g.结果归并
Sharding-JDBC支持通遍历类、排序类、聚合类和分组类四种结果并归方式。普通遍历类最为简单,只需按顺序遍历ResultSet的集合即可。排序类结果则将结果先排序再输出,因为各分库的分片结果均按照各自条件完成排序,所以采用归并排序算法整合最终结果。聚合类分为3种类型,比较型、累加型和平均值型。分组类相对最为复杂,需要将所有的ResultSet结果放入内存,使用Map-Reduce算法分组,最后根据排序和聚合条件做相关处理。最为消耗内存,损失性能的地方就是这里了,可以考虑使用limit合理的限制分组数据大小。
鉴于以上讲的几个Sharding-JDBC中间件框架特点,该方案的优点在于,业务平台中的服务应用可以直接连数据库,降低外围系统依赖所带来的不稳定风险,系统集成难度较低,无需额外运维组件。
其框架的缺点是,限制服务应用只能在业务逻辑层中进行定制化的开发和优化,扩展性一般。对于,比较复杂的系统可能会力不从心,将分片逻辑的压力放在应用服务器上,造成额外风险。(由于本文篇幅所限,对于Sharding-JDBC中间件开源代码的深度分析将在后续篇幅中会讲到)
MyCat是一个开源的分布式数据库系统(属于代理类框架),是一个实现MySQL协议的服务器,用户可以把它看作是一个数据库代理,用MySQL客户端工具和命令行访问,而其后端可以用MySQL原生协议与多个MySQL服务器通信,也可以用JDBC协议与大多数主流数据库服务器通信,其核心功能是分表分库,即将一个大表水平分割为N个小表,存储在后端MySQL服务器里或者其他数据库里。其框架如下:
MyCat分库分表方案的优点在于,能够处理非常复杂的需求,不受数据库访问层原来实现的限制,扩展性强且对于应用服务透明且没有增加任何额外负载。其缺点是上线部署需要额外的中间件,增加运维成本;应用服务需经过代理来连接数据库,网络上多了一层链接的开销,性能有损失且有额外风险。
本文从几个不同的应用开发视角,分别阐述了作者自己工作中用到过的业务平台数据库架构优化方案,包括分离热点数据、本地/分布式缓存、分库分表的三种技术架构。限于作者的才疏学浅,可能对几种方案的理解不够深入,如有阐述不合理之处还望留言一起探讨。