共计 9333 个字符,预计需要花费 24 分钟才能阅读完成。
本篇内容介绍了“大表分库分表总结”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让丸趣 TV 小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
1. 前言
为什么需要做分库分表。这个相信大家多少都有所了解。
海量数据的存储和访问成为了 MySQL 数据库的瓶颈问题,日益增长的业务数据,无疑对 MySQL 数据库造成了相当大的负载,同时对于系统的稳定性和扩展性提出很高的要求。
而且单台服务器的资源 (CPU、磁盘、内存等) 总是有限的,最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。
目前来说一般有两种方案。
1)一种是更换存储,不使用 MySQL,比如可以使用 HBase、polarDB、TiDB 等分布式存储。
2)如果出于各种原因考虑,还是想继续使用 MySQL,一般会采用第二种方式,那就是分库分表。
文章开头就说了,网上分库分表文章很多,对知识点讲解比较多,因此,本文将不再过多赘述分库分表方案的范式处理。
而是专注于梳理分库分表从架构设计 到 发布上线的完整过程,同时总结其中的注意事项和最佳实践。包括五个部分:
业务重构
存储架构设计
改造和上线
稳定性保障
项目管理
尤其是各个阶段的最佳实践,都是血与泪凝聚的经验教训。
2. 第一阶段:业务重构(可选)
对于微服务划分比较合理的分库分表行为,一般只需要关注存储架构的变化,或者只需要在个别应用上进行业务改造即可,一般不需要着重考虑“业务重构” 这一阶段,因此,这一阶段属于“可选”。
本次项目的第一大难点,在于业务重构。
而本次拆分项目涉及到的两张大表 A 和 B,单表将近八千万的数据,是从单体应用时代遗留下来的,从一开始就没有很好的领域驱动 /MSA 架构设计,逻辑发散非常严重,到现在已经涉及 50+ 个在线服务和 20+ 个离线业务的的直接读写。
因此,如何保证业务改造的彻底性、全面性是重中之重,不能出现有遗漏的情况。
另外,表 A 和 表 B 各自有二、三十个字段,两表的主键存在一一对应关系,因此,本次分库分表项目中,还需要将两个表进行重构融合,将多余 / 无用的字段剔除。
2.1 查询统计
在线业务通过分布式链路追踪系统进行查询,按照表名作为查询条件,然后按照服务维度进行聚合,找到所有相关服务,写一个文档记录相关团队和服务。
这里特别注意下,很多表不是只有在线应用在使用,很多离线算法和数据分析的业务也在使用,这里需要一并的梳理好,做好线下跨团队的沟通和调研工作,以免切换后影响正常的数据分析。
2.2 查询拆分与迁移
创建一个 jar 包,根据 2.1 的统计结果,与服务 owner 合作将服务中的相关查询都迁移到这个 jar 包中(本项目的 jar 包叫 projected)。
此处为 1.0.0-SNAPSHOT 版本。
然后将原本服务内的 xxxMapper.xxxMethod() 全部改成 projectdb.xxxMethod()进行调用。
这样做有两个好处:
方便做后续的查询拆分分析。
方便后续直接将 jar 包中的查询替换为改造后 中台服务 的 rpc 调用,业务方只需升级 jar 包版本,即可快速从 sql 调用改为 rpc 查询。
这一步花了几个月的实际,务必梳理各个服务做全面的迁移,不能遗漏,否则可能会导致拆分分析不全面,遗漏了相关字段。
查询的迁移主要由于本次拆分项目涉及到的服务太多,需要收拢到一个 jar 包,更方便后期的改造。如果实际分库分表项目中仅仅涉及一两个服务的,这一步是可以不做的。
2.3 联合查询的拆分分析
根据 2.2 收拢的 jar 包中的查询,结合实际情况将查询进行分类和判断,把一些历史遗留的问题,和已经废弃的字段做一些整理。
以下举一些思考点。
1)哪些查询是无法拆分的? 例如分页(尽可能地改造,实在改不了只能以冗余列的形式)
2)哪些查询是可以业务上 join 拆分的?
3)哪些表 / 字段是可以融合的?
4)哪些字段需要冗余?
5)哪些字段可以直接废弃了?
6)根据业务具体场景和 sql 整体统计,识别关键的分表键。其余查询走搜索平台。
思考后得到一个查询改造总体思路和方案。
同时在本项目中需要将两张表融合为一张表,废弃冗余字段和无效字段。
2.4 新表设计
这一步基于 2.3 对于查询的拆分分析,得出旧表融合、冗余、废弃字段的结果,设计新表的字段。
产出新表设计结构后,必须发给各个相关业务方进行 review,并保证所有业务方都通过该表的设计。有必要的话可以进行一次线下 review。
如果新表的过程中,对部分字段进行了废弃,必须通知所有业务方进行确认。
对于新表的设计,除了字段的梳理,也需要根据具体查询,重新设计、优化索引。
2.5 第一次升级
新表设计完成后,先做一次 jar 包内 sql 查询的改造,将旧的字段全部更新为新表的字段。
此处为 2.0.0-SNAPSHOT 版本。
然后让所有服务升级 jar 包版本,以此来保证这些废弃字段确实是不使用了,新的表结构字段能够完全覆盖过去的业务场景。
特别注意的是,由于涉及服务众多,可以将服务按照 非核心 与 核心 区分,然后分批次上线,避免出现问题导致严重故障或者大范围回滚。
2.6 最佳实践
2.6.1 尽量不改变原表的字段名称
在做新表融合的时候,一开始只是简单归并表 A 和 表 B 的表,因此很多字段名相同的字段做了重命名。
后来字段精简过程中,删除了很多重复字段,但是没有将重命名的字段改回来。
导致后期上线的过程中,不可避免地需要业务方进行重构字段名。
因此,新表设计的时候,除非必不得已,不要修改原表的字段名称!
2.6.2 新表的索引需要仔细斟酌
新表的索引不能简单照搬旧表,而是需要根据查询拆分分析后,重新设计。
尤其是一些字段的融合后,可能可以归并一些索引,或者设计一些更高性能的索引。
2.6 本章小结
至此,分库分表的第一阶段告一段落。这一阶段所需时间,完全取决于具体业务,如果是一个历史包袱沉重的业务,那可能需要花费几个月甚至半年的时间才能完成。
这一阶段的完成质量非常重要,否则可能导致项目后期需要重建表结构、重新全量数据。
这里再次说明,对于微服务划分比较合理的服务,分库分表行为一般只需要关注存储架构的变化,或者只需要在个别应用上进行业务改造即可,一般不需要着重考虑“业务重构” 这一阶段。
3. 第二阶段:存储架构设计(核心)
对于任何分库分表的项目,存储架构的设计都是最核心的部分!
3.1 整体架构
根据第一阶段整理的查询梳理结果,我们总结了这样的查询规律。
80% 以上的查询都是通过或者带有字段 pk1、字段 pk2、字段 pk3 这三个维度进行查询的,其中 pk1 和 pk2 由于历史原因存在一一对应的关系
20% 的查询千奇百怪,包括模糊查询、其他字段查询等等
因此,我们设计了如下的整体架构,引入了数据库中间件、数据同步工具、搜索引擎 (阿里云 opensearch/ES) 等。
下文的论述都是围绕这个架构来展开的。
3.1.1 mysql 分表存储
Mysql 分表的维度是根据查询拆分分析的结果确定的。
我们发现 pk1\pk2\pk3 可以覆盖 80% 以上的主要查询。让这些查询根据分表键直接走 mysql 数据库即可。
原则上一般最多维护一个分表的全量数据,因为过多的全量数据会造成存储的浪费、数据同步的额外开销、更多的不稳定性、不易扩展等问题。
但是由于本项目 pk1 和 pk3 的查询语句都对实时性有比较高的要求,因此,维护了 pk1 和 pk3 作为分表键的两份全量数据。
而 pk2 和 pk1 由于历史原因,存在一一对应关系,可以仅保留一份映射表即可,只存储 pk1 和 pk2 两个字段。
3.1.2 搜索平台索引存储
搜索平台索引,可以覆盖剩余 20% 的零散查询。
这些查询往往不是根据分表键进行的,或者是带有模糊查询的要求。
对于搜索平台来说,一般不存储全量数据(尤其是一些大 varchar 字段),只存储主键和查询需要的索引字段,搜索得到结果后,根据主键去 mysql 存储中拿到需要的记录。
当然,从后期实践结果来看,这里还是需要做一些权衡的:
1)有些非索引字段,如果不是很大,也可以冗余进来,类似覆盖索引,避免多一次 sql 查询;
2)如果表结构比较简单,字段不大,甚至可以考虑全量存储,提高查询性能,降低 mysql 数据库的压力。
这里特别提示,搜索引擎和数据库之间同步是必然存在延迟的。所以对于根据分表 id 查询的语句,尽量保证直接查询数据库,这样不会带来一致性问题的隐患。
3.1.3 数据同步
一般新表和旧表直接可以采用 数据同步 或者 双写的方式进行处理,两种方式有各自的优缺点。
一般根据具体情况选择一种方式就行。
本次项目的具体同步关系见整体存储架构,包括了四个部分:
1)旧表到新表全量主表的同步
一开始为了减少代码入侵、方便扩展,采用了数据同步的方式。而且由于业务过多,担心有未统计到的服务没有及时改造,所以数据同步能避免这些情况导致数据丢失。
但是在上线过程中发现,当延迟存在时,很多新写入的记录无法读到,对具体业务场景造成了比较严重的影响。(具体原因参考 4.5.1 的说明)
因此,为了满足应用对于实时性的要求,我们在数据同步的基础上,重新在 3.0.0-SNAPSHOT 版本中改造成了双写的形式。
2)新表全量主表到全量副表的同步
3)新表全量主表到映射表到同步
4)新表全量主表到搜索引擎数据源的同步
2)、3)、4)都是从新表全量主表到其他数据源的数据同步,因为没有强实时性的要求,因此,为了方便扩展,全部采用了数据同步的方式,没有进行更多的多写操作。
3.2 容量评估
在申请 mysql 存储和搜索平台索引资源前,需要进行容量评估,包括存储容量和性能指标。
具体线上流量评估可以通过监控系统查看 qps,存储容量可以简单认为是线上各个表存储容量的和。
但是在全量同步过程中,我们发现需要的实际容量的需求会大于预估,具体可以看 3.4.6 的说明。
具体性能压测过程就不再赘述。
3.3 数据校验
从上文可以看到,在本次项目中,存在大量的业务改造,属于异构迁移。
从过去的一些分库分表项目来说,大多是同构 / 对等拆分,因此不会存在很多复杂逻辑,所以对于数据迁移的校验往往比较忽视。
在完全对等迁移的情况下,一般确实比较少出现问题。
但是,类似这样有比较多改造的异构迁移,校验绝对是重中之重!!
因此,必须对数据同步的结果做校验,保证业务逻辑改造正确、数据同步一致性正确。这一点非常非常重要。
在本次项目中,存在大量业务逻辑优化以及字段变动,所以我们单独做了一个校验服务,对数据的全量、增量进行校验。
过程中提前发现了许多数据同步、业务逻辑的不一致问题,给我们本次项目平稳上线提供了最重要的前提保障!!
3.4 最佳实践
3.4.1 分库分表引起的流量放大问题
在做容量评估的时候,需要关注一个重要问题。就是分表带来的查询流量放大。
这个流量放大有两方面的原因:
索引表的二次查询。比如根据 pk2 查询的,需要先通过 pk2 查询 pk1,然后根据 pk1 查询返回结果。
in 的分批查询。如果一个 select…in… 的查询,数据库中间件会根据分表键,将查询拆分落到对应的物理分表上,相当于原本的一次查询,放大为多次查询。(当然,数据库会将落在同一个分表的 id 作为一次批量查询,而这是不稳定的合并)
因此,我们需要注意:
业务层面尽量限制 in 查询数量,避免流量过于放大;
容量评估时,需要考虑这部分放大因素,做适当冗余,另外,后续会提到业务改造上线分批进行,保证可以及时扩容;
分 64、128 还是 256 张表有个合理预估,拆得越多,理论上会放大越多,因此不要无谓地分过多的表,根据业务规模做适当估计;
对于映射表的查询,由于存在明显的冷热数据,所以我们又在中间加了一层缓存,减少数据库的压力
3.4.2 分表键的变更方案
本项目中,存在一种业务情况会变更字段 pk3,但是 pk3 作为分表键,在数据库中间件中是不能修改的,因此,只能在中台中修改对 pk3 的更新逻辑,采用先删除、后添加的方式。
这里需要注意,删除和添加操作的事务原子性。当然,简单处理也可以通过日志的方式,进行告警和校准。
3.4.3 数据同步一致性问题
我们都知道,数据同步中一个关键点就是 (消息) 数据的顺序性,如果不能保证接受的数据和产生的数据的顺序严格一致,就有可能因为 (消息) 数据乱序带来数据覆盖,最终带来不一致问题。
我们自研的数据同步工具底层使用的消息队列是 kakfa,,kafka 对于消息的存储,只能做到局部有序性(具体来说是每一个 partition 的有序)。我们可以把同一主键的消息路由至同一分区,这样一致性一般可以保证。但是,如果存在一对多的关系,就无法保证每一行变更有序,见如下例子。
那么需要通过反查数据源获取最新数据保证一致性。
但是,反查也不是“银弹“,需要考虑两个问题。
1)如果消息变更来源于读写实例,而反查 数据库是查只读实例,那就会存在读写实例延迟导致的数据不一致问题。因此,需要保证 消息变更来源 和 反查数据库 的实例是同一个。
2)反查对数据库会带来额外性能开销,需要仔细评估全量时候的影响。
3.4.4 数据实时性问题
延迟主要需要注意几方面的问题,并根据业务实际情况做评估和衡量。
1)数据同步平台的秒级延迟
2)如果消息订阅和反查数据库都是落在只读实例上,那么除了上述数据同步平台的秒级延迟,还会有数据库主从同步的延迟
3)宽表到搜索平台的秒级延迟
只有能够满足业务场景的方案,才是合适的方案。
3.4.5 分表后存储容量优化
由于数据同步过程中,对于单表而言,不是严格按照递增插入的,因此会产生很多”存储空洞“,使得同步完后的存储总量远大于预估的容量。
因此,在新库申请的时候,存储容量多申请 50%。
具体原因可以参考我的这篇文章 为什么 MySQL 分库分表后总存储大小变大了?
3.5 本章小结
至此,分库分表的第二阶段告一段落。
这一阶段踩了非常多的坑。
一方面是设计高可用、易扩展的存储架构。在项目进展过程中,也做了多次的修改与讨论,包括 mysql 数据冗余数量、搜索平台的索引设计、流量放大、分表键修改等问题。
另一方面是“数据同步”本身是一个非常复杂的操作,正如本章最佳实践中提及的实时性、一致性、一对多等问题,需要引起高度重视。
因此,更加依赖于数据校验对最终业务逻辑正确、数据同步正确的检验!
在完成这一阶段后,可以正式进入业务切换的阶段。需要注意的是,数据校验仍然会在下一阶段发挥关键性作用。
4. 第三阶段:改造和上线(慎重)
前两个阶段完成后,开始业务切换流程,主要步骤如下:
1)中台服务采用单读 双写 的模式
2)旧表往新表开着数据同步
3) 所有服务升级依赖的 projectDB 版本,上线 RPC,如果出现问题,降版本即可回滚(上线成功后,单读新库,双写新旧库)
4)检查监控确保没有 中台服务 以外的其他服务访问旧库旧表
5)停止数据同步
6)删除旧表
4.1 查询改造
如何验证我们前两个阶段设计是否合理? 能否完全覆盖查询的修改 是一个前提条件。
当新表设计完毕后,就可以以新表为标准,修改老的查询。
以本项目为例,需要将旧的 sql 在 新的中台服务中 进行改造。
1)读查询的改造
可能查询会涉及以下几个方面:
a)根据查询条件,需要将 pk1 和 pk2 的 inner join 改为对应分表键的新表表名
b)部分 sql 的废弃字段处理
c)非分表键查询改为走搜索平台的查询,注意保证语义一致
d)注意写单测避免低级错误,主要是 DAO 层面。
只有新表结构和存储架构能完全适应查询改造,才能认为前面的设计暂时没有问题。
当然,这里还有个前提条件,就是相关查询已经全部收拢,没有遗漏。
2) 写查询的改造
除了相关字段的更改以外,更重要的是,需要改造为旧表、新表的双写模式。
这里可能涉及到具体业务写入逻辑,本项目尤为复杂,需要改造过程中与业务方充分沟通,保证写入逻辑正确。
可以在双写上各加一个配置开关,方便切换。如果双写中发现新库写入有问题,可以快速关闭。
同时,双写过程中不关闭 旧库到新库 的数据同步。
为什么呢? 主要还是由于我们项目的特殊性。由于我们涉及到几十个服务,为了降低风险,必须分批上线。因此,存在比较麻烦的中间态,一部分服务是老逻辑,一部分服务是新逻辑,必须保证中间态的数据正确性,具体见 4.5.1 的分析。
4.2 服务化改造
为什么需要新建一个 服务来 承载改造后的查询呢?
一方面是为了改造能够方便的升级与回滚切换,另一方面是为了将查询收拢,作为一个中台化的服务来提供相应的查询能力。
将改造后的新的查询放在服务中,然后 jar 包中的原本查询,全部替换成这个服务的 client 调用。
同时,升级 jar 包版本到 3.0.0-SNAPSHOT。
4.3 服务分批上线
为了降低风险,需要安排从非核心服务到核心服务的分批上线。
注意,分批上线过程中,由于写服务往往是核心服务,所以安排在后面。可能出现非核心的读服务上线了,这时候会有读新表、写旧表的中间状态。
1) 所有相关服务使用 重构分支 升级 projectdb 版本到 3.0.0-SNAPSHOT 并部署内网环境;
2) 业务服务依赖于 中台服务,需要订阅服务
3) 开重构分支(不要与正常迭代分支合并),部署内网,内网预计测试两周以上
使用一个新的 重构分支 是为了在内网测试两周的时候,不影响业务正常迭代。每周更新的业务分支可以 merge 到重构分支上部署内网,然后外网使用业务分支 merge 到 master 上部署。
当然,如果从线上线下代码分支一致的角度,也可以重构分支和业务分支一起测试上线,对开发和测试的压力会较大。
4)分批上线过程中,如果碰到依赖冲突的问题,需要及时解决并及时更新到该文档中
5)服务上线前,必须要求业务开发或者测试,明确评估具体 api 和风险点,做好回归。
这里再次提醒,上线完成后,请不要漏掉离线的数据分析业务! 请不要漏掉离线的数据分析业务! 请不要漏掉离线的数据分析业务!
4.4 旧表下线流程
1)检查监控确保没有中台服务以外的其他服务访问旧库旧表
2)检查数据库上的 sql 审计,确保没有其他服务仍然读取旧表数据
3)停止数据同步
4)删除旧表
4.5 最佳实践
4.5.1 写完立即读可能读不到
在分批上线过程中,遇到了写完立即读可能读不到的情况。由于业务众多,我们采用了分批上线的方式降低风险,存在一部分应用已经升级,一部分应用尚未升级的情况。未升级的服务仍然往旧表写数据,而升级后的应用会从新表读数据,当延迟存在时,很多新写入的记录无法读到,对具体业务场景造成了比较严重的影响。
延迟的原因主要有两个:
1)写服务还没有升级,还没有开始双写,还是写旧表,这时候会有读新表、写旧表的中间状态,新旧表存在同步延迟。
2)为了避免主库压力,新表数据是从旧表获取变更、然后反查旧表只读实例的数据进行同步的,主从库本身存在一定延迟。
解决方案一般有两种:
1)数据同步改为双写逻辑。
2)在读接口做补偿,如果新表查不到,到旧表再查一次。
4.5.2 数据库中间件唯一 ID 替换自增主键(划重点,敲黑板)
由于分表后,继续使用单表的自增主键,会导致全局主键冲突。因此,需要使用分布式唯一 ID 来代替自增主键。各种算法网上比较多,本项目采用的是数据库自增 sequence 生成方式。
数据库自增 sequence 的分布式 ID 生成器,是一个依赖 Mysql 的存在,它的基本原理是在 Mysql 中存入一个数值, 每有一台机器去获取 ID 的时候,都会在当前 ID 上累加一定的数量比如说 2000, 然后把当前的值加上 2000 返回给服务器。这样每一台机器都可以继续重复此操作获得唯一 id 区间。
但是仅仅有全局唯一 ID 就大功告成了吗? 显然不是,因为这里还会存在新旧表的 id 冲突问题。
因为服务比较多,为了降低风险需要分批上线。因此,存在一部分服务还是单写旧表的逻辑,一部分服务是双写的逻辑。
这样的状态中,旧表的 id 策略使用的是 auto_increment。如果只有单向数据来往的话(旧表到新表),只需要给旧表的 id 预留一个区间段,sequence 从一个较大的起始值开始就能避免冲突。
但该项目中,还有新表数据和旧表数据的双写,如果采用上述方案,较大的 id 写入到旧表,旧表的 auto_increment 将会被重置到该值,这样单写旧表的服务产生的递增 id 的记录必然会出现冲突。
所以这里交换了双方的区间段,旧库从较大的 auto_increment 起始值开始,新表选择的 id(也就是 sequence 的范围)从大于旧表的最大记录的 id 开始递增,小于旧表 auto_increment 即将设置的起始值,很好的避免了 id 冲突问题。
1)切换前:
sequence 的起始 id 设置为当前旧表的自增 id 大小,然后旧表的自增 id 需要改大,预留一段区间,给旧表的自增 id 继续使用,防止未升级业务写入旧表的数据同步到新库后产生 id 冲突;
2)切换后
无需任何改造,断开数据同步即可
3)优点
只用一份代码;
切换可以使用开关进行,不用升级改造;
如果万一中途旧表的 autoincrement 被异常数据变大了,也不会造成什么问题。
4)缺点
如果旧表写失败了,新表写成功了,需要日志辅助处理
4.6 本章小结
完成旧表下线后,整个分库分表的改造就完成了。
在这个过程中,需要始终保持对线上业务的敬畏,仔细思考每个可能发生的问题,想好快速回滚方案(在三个阶段提到了 projectdb 的 jar 包版本迭代,从 1.0.0-SNAPSHOT 到 3.0.0-SNAPSHOT,包含了每个阶段不同的变更,在不同阶段的分批上线的过程中,通过 jar 包版本的方式进行回滚,发挥了巨大作用),避免造成重大故障。
5. 稳定性保障
这一章主要再次强调稳定性的保障手段。作为本次项目的重要目标之一,稳定性其实贯穿在整个项目周期内,基本上在上文各个环节都已经都有提到,每一个环节都要引起足够的重视,仔细设计和评估方案,做到心中有数,而不是靠天吃饭:
1)新表设计必须跟业务方充分沟通、保证 review。
2)对于“数据同步”,必须有数据校验保障数据正确性,可能导致数据不正确的原因上文已经提到来很多,包括实时性、一致性的问题。保证数据正确是上线的大前提。
3)每一阶段的变动,都必须做好快速回滚都预案。
4)上线过程,都以分批上线的形式,从非核心业务开始做试点,避免故障扩大。
5)监控告警要配置全面,出现问题及时收到告警,快速响应。不要忽略,很重要,有几次出现过数据的问题,都是通过告警及时发现和解决的。6)单测,业务功能测试等要充分
6. 项目管理之跨团队协作
关于“跨团队协作”,本文专门拎出来作为一章。
因为在这样一个跨团队的大型项目改造过程中,科学的团队协作是保障整体项目按时、高质量完成的不可缺少的因素。
下面,分享几点心得与体会。
6.1 一切文档先行
团队协作最忌“空口无凭”。
无论是团队分工、进度安排或是任何需要多人协作的事情,都需要有一个文档记录,用于追踪进度,把控流程。
6.2 业务沟通与确认
所有的表结构改造,必须跟相关业务方沟通,对于可能存在的历史逻辑,进行全面梳理;
所有讨论确定后的字段改造,必须由每个服务的 Owner 进行确认。
6.3 责任到位
对于多团队多人次的合作项目,每个团队都应该明确一个对接人,由项目总负责人与团队唯一对接人沟通,明确团队完整进度和完成质量。
“大表分库分表总结”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注丸趣 TV 网站,丸趣 TV 小编将为大家输出更多高质量的实用文章!