分库分表在微保的应用实践
编辑:edwinzeng 曾鑫鹏
1
为什么要分库分表
微保是腾讯控股的保险代理平台,致力于为用户提供最佳的保险服务体验。自2017年11月上线以来,推出了微医保、药神保等广受欢迎的产品。随着业务的快速增长,传统的单机数据库已无法满足业务需求。
具体表现在以下几个方面:
无法满足海量数据存储需求。
单机存储量有限,磁盘空间无法继续扩展。与此同时,海量数据还会带来数据库备份恢复时间长、单表变更风险高等问题。
无法满足高并发读写需求。
单机数据库性能有限,当并发数超过其负载的临界值,数据的读取与写入需求很难被满足。
于是,我们考虑对数据进行拆分来突破单机局限。数据拆分有两种类型,一种是垂直拆分,一种是水平拆分:
垂直拆分。
按业务类型对表进行分类,将表分布到不同的数据库上。
水平拆分。
选定表的某个字段,按照特定规则拆分到多个库表中。
其中字段是我们通常所说的Shard key,而规则则是按照什么方式(哈希/范围)拆分,拆分多少个表。
随着微服务的流行,垂直拆分已经非常普及了,微保在公司成立之时已经按照业务模块垂直拆分成几大模块。本文重点探讨一下如何进行 水平拆分 及 水平拆分过程中容易遇见的问题。
2
水平拆分方案
在介绍具体方案前,必须先提一下水平拆分的第一原则:能不拆分尽量不拆分。
因为当数据水平拆分后,原来很容易实现的order by、group by等都变得复杂了,而且跨分片的事务的性能也难以保证,业务实现复杂度和维护工作将呈指数上升。
所以当数据库出现瓶颈时,我们可先升级硬件,升级网络,升级数据库版本,增加缓存,采用读写分离等优化方案。当这些优化手段都用上了还不能解决问题时,再进行水平拆分。
2.1 如何实施水平拆分
当对一个大表进行水平拆分时,我们需要做两件事:
容量评估
根据业务发展的趋势,评估3到5年后表的行数、占用空间大小,确定表拆分的个数。
拆分依据:控制单表在10G以内(使用工具变更,变更时间控制在10分钟以内)。当数据库版本为MySQL 8.0以上,可快速加列时,这个分表标准可再放宽。
业务分析 分析业务特点,确定「拆分字段 Shard key」及「路由规则 Routing Rules」
选取Shard key 依据:
分析业务查询及更新语句,采用二八原则,找出最常用的查询字段,选择Shard key
数据尽可能平均拆分
尽量减少跨库事务
路由规则包含:
范围拆分:如按日期拆分
取模拆分:根据数值取模。当字段为数值类型,直接取模;当字段为字符类型,则通过哈希算法计算出数值,再取模(微保采用的是CRC32算法)。
下面总结了范围拆分和取模拆分的优缺点及其适用场景:
2.2 拆分案例
下面我们通过一些实际案例来了解拆分的步骤。
案例一:当前需要拆分的表为流水型数据
流水型数据,建议取时间字段为Shard key,根据数据量大小决定是按年、按月还是按日拆分。
注意:使用这种方式拆分方式后,数据表一定要提前建好,否则时间到了,却没有相应的库表可写,就会发生故障了。
案例二:当前需要拆分的为用户维度数据
1. 业务类查询,用户登录时,通过login_name/phone/email查询,占比10% 2. 业务类查询,用户信息查询,通过user_id 查询,占比90% 可以看见,业务类有百分之九十多都是通过user_id查询,根据二八原则,选取user_id为Shard key,采用hash取模拆分。
案例三:当前需要拆分的为混合型数据
当查询条件都是五五分成,则选择一个尽可能保证数据平均拆分,尽可能减少跨库事务的字段作为Shard key。再根据需求,选取范围/取模拆分方法将数据进行拆分。
总结:由上可知,水平拆分必须从业务的角度入手,如果不清楚业务,将无法做出好的分片策略。
当我们选好了Shardkey及路由规则,分库分表的工作是不是已经做完了呢?并非如此,我们还需考虑:
非Shard key查询方案
跨分片事务的实现方式
水平拆分采用客户端方案还是代理方案
如何将单表切换为分库分表等一系列问题
2.3 非Shard key查询解决方案
对于Shard key字段上的查询,我们可以直接路由到库进行查询,而对于非Shard key的查询,由于不知道数据落在哪个库上,往往需要遍历所有库,当分库数量多起来,性能会显著降低,我们该如何高效的实现查询呢?
方案一:建立映射表
拿用户体系为例,我们选择了user_id 做Shard key,当业务想通过login_name进行查询,先查找映射表,通过登录名查出user_id,再通过路由去查用户库。
方案二:基因法
比如订单号和user_id,我们可以把user_id的一部分(gen) 揉进订单号里,我们按照user_id 分库分表的话,订单号也已经确定分库分表的位置。如上图,如果采用这种基因法,就必须分16个表,因为是Gen为4bit。
方案三:异构数据库
对于用户模块,日常还会存在一些运营类查询,一般通过年龄、性别、登录时间、注册时间等条件进行查询。运营类查询与业务类查询相比,存在以下特点:
基本上是批量分页的查询
访问量低
可用性要求不高
一致性要求不那么严格
这种类型的查询,我们可以通过数据冗余,将数据异构在Hive或ES内,供运营后台查询
方案四:使用数据库中间件,直接查询
部分中间件做得比较完善,可以很好的兼容复杂SQL。但是查询条件中不含有Shard key,会导致中间件去扫描所有的分表,查询效率较低,如果拉取大量数据在中间件上做聚合,对中间件的冲击也会比较大,所以这种方式,仅限于查询频率比较低,匹配数据比较少的情况。
2.4 水平拆分实现方案
水平拆分当前有不少成熟的解决方案,这些方案一般分为两大类:
客户端方案
这里的客户端,指的是应用程序通过修改应用数据访问层,如JDBC、Data Source ,以客户端的方式直连数据库,并在模块内完成数据的分片整合。无需额外部署组件。从图中可以看出来分片的逻辑和应用是部署在一起的。
中间件代理方案
过独立的中间件来统一管理所有数据源和数据分片整合,后端数据库集群对应用程序透明,需要独立部署和运维代理组件。
这两种架构方案机制不同,各有优缺点:
2.5 客户端方案实际应用
微保对于上述这两种方案均有应用,下面分别详细说一下这两种方案在实际应用中踩过的坑。
某项目,对性能要求较高,采用了客户端架构 Sharding-JDBC。我们在使用过程中遇到过如下问题:
连接数过高。在客户端分片架构下,应用与数据库的连接是由 Shading JDBC 管理的。由于分片规则的影响,应用在执行查询前,并不知道具体要访问哪个数据库。这导致它必须与所有数据库建立连接。举个例子,当业务拆分成 64 个逻辑库,每个库的连接池大小为 8,当我们部署 40 个应用实例后,总连接数就变成 20480 条。假设这 64 个逻辑库对应到真实 4个 mysql 库,那么每台 MySQL 将会分配到 5120个连接。如果中间部署一个proxy中间件,连接复用,能大大减少后端数据库的连接数。
触发疑似bug,导致QPS严重放大。在日常使用Sharding-JDBC时发现,出现一些特殊事件时(如进程重启),程序内已建立的路由规则会偶发失效,导致查询被放大。如:本应下推到具体某个分表的查询,却查询了所有分表,如果表被拆分成100个分表,就会有100个查询打到数据库上(QPS被放大了100倍)。不过这个问题在我们将Shading JDBC版本升级至4.0.1版本之后就没有再出现了。
基于客户端的分片方案还有一个很大的缺点 -- 只支持特定编程语言。比如 Sharding JDBC 仅支持 java 语言,所以使用其他语言的开发,还是比较偏向于使用对应用透明的中间件代理方案。
2.6 数据库中间件选型
对于中间件代理方案,我们调研了比较出名的开源中间件Mycat和腾讯游戏CROS DBA团队研发的TSpider,发现:
Mycat的安全性较低,普通账号只要知道管理端口号为多少,就能上去做路由配置更新,这种操作是比较危险的。
Mycat对某些SQL兼容性不够好,因为Mycat的 SQL解析是由特定的SQL解析模块来解析的,这就要求Mycat的开发者考虑到各种SQL用法。如果对每种SQL都支持和兼容,工作量会非常庞大。
而TSpider,以MySQL引擎的方式提供服务,兼容性非常好。它与我们平时使用的InnoDB引擎一样,SQL解析、优化、执行都交给 MySQL 的 Sever层处理,几乎支持任何类型SQL。
对DBA非常友好,运维简单,权限的语法与普通MySQL基本相同。
对开发非常友好,无任何学习门槛,可以像操作普通数据库表一样操作Tspider表。
综合对比,微保最终选择Tspider作为数据库中间件代理来实施分库分表。
3
TSpider在微保的应用实践
3.1 TSpider 介绍
TSpider是由腾讯游戏CROS DBA团队基于开源存储引擎Spider定制研发而成的分布式MySQL存储引擎。
支持哈希、范围、列表等算法,将数据水平分布到各个节点上,提高应用的存储和处理性能。
从架构图中可以看出,应用程序连接TSpider,TSpider充当中间件,将客户端查询的请求,按事先定义好的分片规则,分发给后端数据库,之后返回的数据在TSpider内存里做聚合,最后返回给客户端请求。
3.2 TSpider的基本用法
TSpider是基于MySQL的分区表功能开发,但不同的是,它将每个分区的数据拆分存储到了远端MySQL实例中。具体配置方法如下:
1.配置后端数据库连接信息
此条信息表明:
10.0.0.1:3306 的数据库别名为 SPT1
10.0.0.2:3306 的数据库别名为 SPT2
10.0.0.3:3306 的数据库别名为 SPT3
10.0.0.4:3306 的数据库别名为 SPT4
2.支持哈希、范围、列表等算法
按哈希算法拆分
通过comment注释来调用后端的表,其中pt3 分区表明 对应的实际逻辑库为tendb_test_3,逻辑表为t1, 对应的后端数据库别名为SPT3,由上面我们可知为10.0.0.4:3306数据库。
按范围拆分
其中p20200401分区则表明对应的实际逻辑库为test_log,逻辑表为 realtime_log_20200401 , 对应的后端数据库别名为SPT0。
看到这,你或许会有疑问,既然是用的分区表的架构,那我直接使用分区表就OK了,何必使用 TSpider 呢?
我们一起来看一下TSpider表与分区表的区别:
分区表本身是一个大表,它由 MySQL Server 层决定使用哪个分区。
表结构变更时,仍是个大表
不能分库,不能跨实例部署
TSpider 将每个分区的数据拆分存储到了远端 MySQL 实例中。
表结构变更时,已分解成多个小表
可分库,可跨实例部署
3.3 TSpider在微保的应用
目前微保使用了数十个TSpider节点,其中核心的保单、用户服务在灰度使用中。
虽然TSpider很强大,MySQL的原生命令几乎全部都支持,但基于性能考虑,我们在TSpider层做了一些限制:
1.禁止使用分布式事务 ,建议使用TCC事务代替(Tips:分布式事务比普通事务至少慢一半,节点越多,性能损耗越大。)
2.在线业务禁止跨shard查询
不带Shard key 的查询,会遍历所有的分片,IO性能相对最差。
返回的数据会在TSpider层再做一次聚合,如果返回的数据过多,对Tspider内存的冲击也比较大。
因此跨shard的查询越多,对系统的影响越大,所以我们要尽量避免这种 SQL 的产生
3.部署查询节点
由于在线节点禁止跨shard查询,我们还部署了查询节点(允许跨shard查询)供少量跨节点查询需求。仅限于查询频率比较低,匹配数据比较少的情况
以下是微保部署的Tspider架构图:
3.4 如何将单表切换为分库分表
在单库单表到分库分表的改造过程中,需要做大量的数据迁移。我们在实际迁移过程中,针对不同的场景使用了不同的数据迁移方案,下面逐一介绍。
表结构保持不变
当表结构保持不变时,我们可采用双写部署方式进行数据迁移。
前提:微保的所有表结构都包含两个系统字段,其中sys_ctime表示创建时间,sys_utime表示修改时间,字段的定义如下:
sys_ctime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
sys_utime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间'
阶段一:程序数据双写,读旧库,迁移历史数据。DBA在业务低峰期使用mysqldump或mydumper导出并导入数据
阶段二:程序数据双写,读旧库,新旧两库数据追平。
根据sys_utime 导出最近变化的数据,进行数据修复,并通过数据比对工具核对数据
多次重复第三步骤,直至新旧库数据一致
阶段三:程序双写,读新库,数据比对工具仍然继续比对,为程序回退做准备。
阶段四:观察一周,分库分表业务运行正常,停止对旧库进行写入,数据迁移完成。
表结构有变化
TSpider 不支持这类数据迁移,需要业务开启双写或采用异步流程,将两边数据进行同步。
4
分库分表常见知识点
4.1 分库分表与普通单表的主要区别
4.2 中间件方案对数据库的性能影响
相比传统的单机数据库,应用需经过中间件来连接数据库,网络上多了一跳,性能将会有额外损耗。
select * from user where user_id = xxx
经测试,上述点查询,使用Tspider损耗大概为20%。假设原单机数据库的QPS能达到10w/s ,那么经过Tspider后,QPS只能达到8W/s 。使用中间件这种架构都会存在这种问题,但是使用中间件后,数据可分布到不同数据库实例中,所以整体性能还是提升的。
5
总结
本文介绍了分库分表的一些基础知识,以及微保在分库分表实施过程中对一些思考、选型和应用实践,希望抛砖引玉,共同探讨数据库分库分表的最佳实践。
最后,再提一下分库分表的第一原则:能不拆分尽量不拆分。分库分表在解决性能和扩展问题的同时,会为维护工作和业务逻辑带来一系列复杂性和性能损耗,尽量避免过度设计,过早优化。