当前位置: 首页 > 科技观察

亿级大表分库分表实战总结(万字干货,实战复盘)

时间:2023-03-19 22:27:48 科技观察

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

猜你喜欢