vlambda博客
学习文章列表

分库分表在微保的应用实践

编辑: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。我们在使用过程中遇到过如下问题:

  1. 连接数过高。在客户端分片架构下,应用与数据库的连接是由 Shading JDBC 管理的。由于分片规则的影响,应用在执行查询前,并不知道具体要访问哪个数据库。这导致它必须与所有数据库建立连接。举个例子,当业务拆分成 64 个逻辑库,每个库的连接池大小为 8,当我们部署 40 个应用实例后,总连接数就变成 20480 条。假设这 64 个逻辑库对应到真实 4个 mysql 库,那么每台 MySQL 将会分配到 5120个连接。如果中间部署一个proxy中间件,连接复用,能大大减少后端数据库的连接数。

  2. 触发疑似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导出并导入数据

阶段二:程序数据双写,读旧库,新旧两库数据追平。

  1. 根据sys_utime 导出最近变化的数据,进行数据修复,并通过数据比对工具核对数据

  2. 多次重复第三步骤,直至新旧库数据一致

阶段三:程序双写,读新库,数据比对工具仍然继续比对,为程序回退做准备。

阶段四:观察一周,分库分表业务运行正常,停止对旧库进行写入,数据迁移完成。

表结构有变化

TSpider 不支持这类数据迁移,需要业务开启双写或采用异步流程,将两边数据进行同步。

4

分库分表常见知识点

4.1 分库分表与普通单表的主要区别

1. 不能使用自增id,可使用id生成器的方案生成id,微保采用的是雪花算法。
2. 基于性能考虑,join 类查询及统计类查询均不建议使用。
3. 基于性能考虑,查询建议带上Shard key。
4. 多维度业务查询,特别是非 Shard key 高频查询,在线业务建议使用映射法或基因法做业务改造,运营类查询建议使用异构法做改造。
5. 不允许使用分布式事务,建议使用TCC事务代替。


4.2 中间件方案对数据库的性能影响

相比传统的单机数据库,应用需经过中间件来连接数据库,网络上多了一跳,性能将会有额外损耗。

select * from user where user_id = xxx

经测试,上述点查询,使用Tspider损耗大概为20%。假设原单机数据库的QPS能达到10w/s ,那么经过Tspider后,QPS只能达到8W/s 。使用中间件这种架构都会存在这种问题,但是使用中间件后,数据可分布到不同数据库实例中,所以整体性能还是提升的。

5

总结

本文介绍了分库分表的一些基础知识,以及微保在分库分表实施过程中对一些思考、选型和应用实践,希望抛砖引玉,共同探讨数据库分库分表的最佳实践。

最后,再提一下分库分表的第一原则:能不拆分尽量不拆分。分库分表在解决性能和扩展问题的同时,会为维护工作和业务逻辑带来一系列复杂性和性能损耗,尽量避免过度设计,过早优化。


分库分表在微保的应用实践