一种不停服的数据迁移方案

news2024/11/15 13:26:21

一、前言

好的方案是一步步演进出来的。当前最优的系统方案,可能在下一个月、三个月或半年后,就会遇到瓶颈,需要调整自身以便适应新的业务场景。系统的演进就是一个快进版的人类进化史。

我之前负责的一个系统,一开始基本没啥数据量,短短几个月数据量就达到了30w+/天,也就是1个月后核心业务表就接近1千万(MySQL数据库),为此对系统进行了从单个数据库到分片库的升级改造。

二、演进历程

该系统从20年5月初首次发版以来,由于业务量的增长,在数据存储层,经历过3个阶段的发展,如下图:
系统存储层的架构演进

2.1、单库阶段:

20年5月,系统搭建初期,支持的用户并不多,因此初版只是完成了数据模型的确立和基本功能的实现,采用了1主2从的单库结构,2个从库部署在不同的机房,防止单点故障。数据库cpu/内存/磁盘配置:8C/12G/128G。

2.2、Redis + 读写分离:

20年9月,系统支持了实物+服务的业务场景,跟订单中心打通,打通后系统数据量开始稳步提升,增量数据大约在5万/天。系统流量的增长,一方面让大家看到了系统带来的价值,另一方面也意识到了,需要做些什么来保护数据库。具体措施:

①针对查询接口,增加了主动式缓存:在查询频率较高的场景下,提前把数据放入缓存进行预热,减少回表查询。

②读写分离+从库负载均衡:JED弹性库支持通过不同的账号,进行不同权限的数据库操作:rr账号支持读写,ro账号仅支持读。因此数据层增加了只有读权限的数据源配置,通过逻辑改造,实现读写分离。另外ro账号可以通过DBA调整配置,对读操作进行负载均衡(默认是不支持的),进一步提升数据库读操作的吞吐量。

③数据库配置升级:从原来8C/12G/128G升级到了16C/16G/256G,从库升级为3个。

2.3、ES + Redis + 数据库分片:

20年双11,业务进一步增加了推广力度,增量数据30万+/天,其中11.11当天突破百万。系统流量的再次增加,按照目前的方案,不到1个月单表数据量就会达到千万级别,到了这个级别后,很难保证MySQL的性能,可能原来某个正常在用的功能,第二天就会出现因为慢SQL导致的接口超时、操作无响应、页面白屏等。并且针对业务的发展来说,未来会投放更多的入口,系统会迎来更大的流量。因此在方案上又进行了优化,具体如下:

①排查慢SQL:梳理DAO层SQL,排查未走索引的查询,优化相关SQL语句。避免表的关联查询,统一调整为单表查询,数据的加工处理放在逻辑层,数据库只做存储和简单查询(这点在系统搭建初期贯彻的就比较好,基本没有关联查询)。

②数据库分库:从单库切为24个分片库,按照30万/天的增量规划,支持未来2年的发展。根据用户PIN的hash值做路由,由于面向的是整个京东的C端用户而非特定用户群体,因此可以避免数据倾斜。

③引入ElasticSearch:将数据在ElasticSearch中异构一份,对外提供查询服务,进一步降低对数据库的压力,同时支持更丰富的查询场景。MySQL跟ElasticSearch之间的数据同步,是通过binlog实现监听MySQL数据库变更的日志来完成的。

本文重点围绕第3阶段的数据库分库内容展开。由于系统不是一开始就进行的分片,因此需要将数据从单库,迁移到分片库,并且保证整个迁移过程平滑,不能对现有业务有任何影响。如果迁移过程出现异常,支持快速回滚,并且不能丢失数据,即实现不停服的数据迁移。

三、不停服的数据迁移

3.1、技术选型及对比

数据库分库分表技术已经很成熟,很多互联网公司的系统发展到一定量级后,都会通过垂直拆分、水平扩展的方式,来提升数据库的性能。

垂直拆分:把一个有很多字段的表给拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。
垂直拆分

水平拆分:把一个表的数据给分到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。
水平拆分
水平拆分,可以通过只分表、只分库或分库+分表的方式去做,对于分库分表,有很多成熟的数据库中间件,按照实现原理可以分为2类:应用层依赖中间件 和 代理层依赖中间件。

分类应用层依赖中间件代理层依赖中间件
原理介绍重新实现JDBC的API,通过重新实现DataSource、PrepareStatement等操作数据库的接口,让应用层在基本不改变业务代码的情况下透明的实现分库分表的能力。在应用和数据库的连接之间搭起代理层,上层应用以标准的MySQL协议来连接代理层,然后代理层负责转发请求到底层的MySQL物理实例,这种方式对应用只有一个要求,就是只要用MySQL协议来通信即可。
优点不用额外部署,运维成本低;不需要代理层的二次转发请求,性能很高;应用层无感知,接入成本低;如果遇到升级之类的,proxy代理层改造即可,业务系统不需要升级发布;
缺点不能跨语言,比如Java写的sharding-jdbc显然不能用在C#项目中;如果遇到升级,各个系统都需要重新升级版本再发布;需要有专门的中间件团队维护,运维成本高,一般只有大中型企业有自己能力开发、维护;
代表产品当当的sharding-jdbc、蘑菇街的TSharding、携程开源的Ctrip-DAL等阿里的MyCat、京东内部的JED弹性库

最终方案:采用JED弹性库,有独立的运维团队支持,并且公司内部很多核心业务都在使用。实现方式上,采用了只分库不分表的方式,以用户PIN做为分库的路由(之前考虑到会分库,因此从系统搭建时每个表就保留了该属性)。

3.2、关键点

①数据迁移:存量数据需要从单库迁移到分片库。增量数据需要实现双向同步。
②灰度切量:为保证整个过程平稳,需要做灰度切量。即按照一定的流量比例(比如千分比、万分比等),将流量逐步切到分片库上,保证有较长的窗口期进行充分验证,验证通过后可以全量切到分片库。
③数据校验:验证两边数据源数据是否一致。

整个过程,可以用下图来表示:
数据迁移概览

3.3、如何做数据迁移

3.3.1、数据单向同步

通过DBA进行存量&增量数据的迁移。如下图:
存量数据同步逻辑

从单库迁移到分片库,在这个过程中迁移了1200万份用户数据,用时不到1个小时,不得不说还是很给力。正常情况下,DBA迁移完数据,业务系统将DAO层数据源改为分片库,直接上线,就完事了。但,这只是理想情况……,会有以下问题:

①必须要停服务:为了防止上线过程中单库跟分片库都在写数据造成两边不一致,因此要先把服务停下来,不能再往单库里写新数据,等上完线后,再启动服务。会影响到正常的业务操作。

②只能一刀切,切换风险大:如果上线后发现某些SQL未覆盖到,不支持分片库操作,只能进行回滚,因此在上线到回滚期间,会造成数据丢失。
鉴于以上情况,我们决定在DBA完成历史数据的迁移后,由我们自己的业务系统承接迁移任务,实现数据双向同步,以便支持回滚,保证迁移过程不发生任何一笔数据丢失。

3.3.2、数据双向同步

业务系统改造,主动监听单库、分片库的binlake,进行增量数据正向、逆向同步。如下图:
数据双向同步

在进行数据双向同步过程中,有以下几点需要注意:

①由DBA同步切换为业务系统同步,要保证无缝衔接,即这个切换过程既不能丢失数据,又不能插入重复数据:在前者停掉后,后者立即启动,实际中很难保证,所以需要并行混跑一段时间(比如30分钟)来保证切换的过程中不会有数据丢失。

②重点:Binlake双向同步,容易导致数据循环更新,直到把数据库打挂,需要识别出重复的binlake数据。可以使用update_time字段,如果binlake中的值,比数据库新,则在数据库中更新该条记录;如果update_time的值,比数据库中的旧或相等,说明是重复binlake,可以忽略

③重点:DAO层XML中的update_time字段,不能使用now()函数,这样会造成循环更新。比如单库执行了update table set update_time = now(),分片库监听到binlake后,发现该条记录时间比自己新,需要执行update进行更新,也会set updat_time=now(),这时候时间就又变成了最新的,导致循环更新!

④数据双向同步,不涉及业务逻辑的改造,可以建立新集群,只承担数据同步的功能。

3.4、如何做灰度切量

3.4.1、灰度流程

业务系统改造,入口处增加AOP切面,支持灰度切读、写接口。如下图:
业务侧改造后流程

说明:
①AOP切面解析入口方法的参数,识别出用户PIN,根据用户PIN做路由,根据一系列规则的ducc配置,决定当前请求走哪个数据源。
路由

②重点:读写接口的路由规则要一致,否则会存在数据延迟的情况。比如用户A的请求,在写接口路由到了分片库,那么用户A的查询也必须路由到分片库。因为数据的同步是异步进行的,在接口实时性要求较高的场景下,用户A的查询路由到单库,可能数据还没同步过来,导致查询不到数据!另外为了进一步降低延迟,单库跟分片库都不再进行读写分离,统一走主库。

③重点:业务侧系统需改造SQL,保证所有SQL都会通过路由key进行查询、修改操作,否则SQL命令需要在所有分片上执行,增加了执行时间。

3.4.2、数据源动态切换:

现在有了2个数据源,在获取链接的时候,就需要做有机制来实现数据源的路由,可以利用spring的AbstractRoutingDataSource来实现,流程及数据库配置如下:
数据源路由
多数据源配置
AbstractRoutingDataSource原理如下图。在我们的场景中,根据用户PIN解析后,将对应的数据源信息放入到ThreadLocal线程本地变量中,执行数据库操作时,从ThreadLocal中获取当前请求对应的数据源,然后执行相应SQL。
选择数据源抽象类
从ThreadLocal变量中获取数据源
ThreadLocal线程变量

3.5、数据一致性校验

关于数据库对比验证,前提是不管单库,还是分片库,严格情况下都需要查询全量数据做对比校验(当然实际情况可以根据业务场景来确定对比数据的范围,比如是否只关注最近半年、3个月还是1个月的数据,以及允许的误差是万分之一,还是百万分之一等),保证数据同步的一致性,这点非常重要,是项目平稳上线的重要保证!

3.5.1、存量数据

DBA通过transfer工具完成数据迁移,并且有自己的校验机制:CDC校验。是一种消息摘要算法(信息指纹),将2个数据源的数据,分别生成对应的信息摘要,然后对比是否一致。

3.5.2、增量数据

根据指定时间范围,将对应每条数据转化为相应的HashCode,然后对比2个数据源中指定记录的Hashcode是否相等。为了提升对比的效率,这里使用了Redis的有序集合(sorted set),它的跳跃表结构,能更高效的进行2个数据源的查询和对比。

关于数据校验,这里遇到了非常有意思的问题:要对比数据,首先要查询,当然一次查询出上千万条数据肯定是不可行的,直接导致数据库服务器OOM,因此只能分页查询。但是针对千万级别的分库数据而言,分页查询也并非易事。

对于单库可以对limit查询改造,比如改为select * from table_1 a inner join(select id from table_1 limit pageNo, pageSize) b where a.id = b.id,但是对于分片库却无法分页查询,即不能使用limit。
原因:分析下这样的SQL:select * from table limit #offset, #pageSize

①深分页的查询中,查询代价非常大:这样的SQL在分片库上查询,由于不走路由,需要在每个分片上执行,执行的逻辑是每个分片查询前N页的所有数据,而非只查询第N页,然后在网关层把所有分片的前N页数据汇总,假如M个分片,即要从 (N * pageSize ) * M 条记录中,取出第N页的数据,类似于ES的分页查询。

②结果集不是稳定的:这样的SQL在网关层做汇总时,由于没有排序,从(N * pageSize ) * M条记录中取出第N页数据,由于SQL语义不明确,所以并不能保证是一个稳定的结果集。如果要保证结果集稳定,必须要增加order by,这样会更增加每个分片、网关层的查询代价

解决方案:使用MySQL的流式查询,流式查询与普通查询不同之处在于并不是一次性将所有数据加载到内存,在调用next()方法时,MySQL驱动只从网络数据流获取到1条数据,然后返回应用,这样就避免了内存溢出问题。相关原理介绍
以下展示了如何开启流式查询
MySQL流式查询
流式查询

四、注意事项

1、所有的SQL语句都要梳理一遍,保证①所有查询语句,都带路由PIN;②所有update语句,都会更新update_time字段。③所有insert语句,都带update_time字段。

2、SQL语句不支持update路由字段,可能会导致数据逻辑丢失,因此JED会在语法上进行约束,update路由字段时,直接提示不支持。即使更新前后路由key的值一样也不行,proxy层不会判断是否相等,所以直接拒绝。

3、表变更的binlake消息,是共用一个topic,还是每个表一个topic?
建议每个表一个binlake的topic,减少消息积压,以及数据同步的延迟。或者量比较小的表,可以共用一个topic,量比较大或者关键的表,单独一个topic

4、表变更的binlake消息,是无序的,有可能第2次变更的消息先到,目标数据库提前更新成最新值。当第1个变更的消息到达时,通过消息里update_time会早于目标库的update_time,因此可以直接忽略;

5、源库、目标库都开启了binlake,因此1次表变更,源库会发出binlake消息,目标库也会发出binlake消息,但是binlake消息中的update_time跟对方库里的update_time是一致的,因此可以直接忽略;

6、如果只更新了数据,但是未更新update_time,会导致在另一个数据源的表中丢失变更,因为两侧时间一致,无法确认谁是最新的数据;

7、针对历史数据的update_time为空,可以约定为旧数据,前提是所有变更都更新了update_time,这样源库中update_time有值,目标库中update_time为空的话,作为旧数据来更新即可。

(END)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/642870.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

46 最佳实践-性能最佳实践-内存大页

文章目录 46 最佳实践-性能最佳实践-内存大页46.1 概述46.2 操作指导 46 最佳实践-性能最佳实践-内存大页 46.1 概述 相比传统的4K内存分页,openEuler也支持2MB/1GB的大内存分页。内存大页可以有效减少TLB miss,显著提升内存访问密集型业务的性能。ope…

证券行业异构系统众多,微服务和网格如何全都要

在携手网易数帆取得中间件云原生化的创新成果之后,安信证券已在谋划大规模微服务化的布局,以确保信息系统架构走在现代金融科技的前列,支撑业务在未来数智金融竞争中把握主动权。 架构未动,思想先行。安信证券近日在内部组织了一…

安全左移DevSecOps开源工具链建设

开发安全相关技术和产品受到越来越多的关注。行业共识认为,应用系统上线之后进行软件漏洞修复,其修复成本是需求设计阶段修复成本的几十倍。因此,在开发环节,引入相应的安全工具,能够有效的降低漏洞的修复成本&#xf…

vue+el-select下拉实现:全选、反选、清空功能

问题描述: el-select下拉框要求实现全选功能。具体功能包括: 当选择【全选】时,所有选项全部被勾选;当选择【反选】时,已选择选项变为未选择选项,未选项变为已选项当选择【清空】时,所有选项变…

SpringBoot进阶学习?看这篇就够了

相信从事Java开发的朋友都听说过SSM框架,老点的甚至经历过SSH,说起来有点恐怖,比如我就是经历过SSH那个时代未流。当然无论是SSM还是SSH都不是今天的重点,今天要说的是Spring Boot,一个令人眼前一亮的框架,…

作用域详解

作用域详解 1、概念2、分类2.1 全局作用域2.2 局部作用域2.2.1 函数作用域2.2.2 块级作用域2.2.3 块级作用域与函数声明 1、概念 JavaScript中的作用域是指变量、函数和对象在代码中可访问的范围。作用域规定了代码中的标识符(变量名、函数名等)在何处和…

ansible自动部署zabbix监控平台

目录 ansible端部署 使用ansible配置zabbix-mysql端 使用ansible配置zabbix-server端 使用ansible配置zabbix-agent端 一键部署zabbix Ansible是一款开源的自动化运维工具,可以通过SSH协议远程自动化地执行一些复杂的IT工作,例如程序部署、配置管理、…

Python自动化测试——postman,jmeter接口测试

关于众所postman,jmeter,做自动化测试的我想对这两个词并不陌生。大家都知道postman用来做接口测试很方便,下面我们就用一些例子来演示一下它该如何进行接口测试: 首先我们来介绍一下接口测试的概念: 1、什么是接口测试&#xf…

【裸机开发】内核时钟 PLL1 配置实验(一)—— 寄存器分析篇

本章主要会回答以下问题 ? imx6u 的时钟源来自于哪 ?为什么一个起始时钟源,最终分成了多路?不同的时钟源是如何与外设对应起来的?(时钟树)要配置内核时钟频率 有哪些步骤 ?涉及到哪…

NLP学习笔记十一-word2vec模型

NLP学习笔记十一-word2vec模型 再介绍word2vec模型之前,我们需要先介绍一些背景知识。 我们只知道,NLP这一领域在ward2vec出现之前肯定也是有很大程度发展的,那么想要用将自然语言用计算机进行处理,进行计算,我们必须…

JQuery全部详细笔记-下

JQuery全部详细笔记-下 jQuery 的 DOM 操作 查找节点, 修改属性 查找属性节点: 查找到所需要的元素之后, 可以调用 jQuery 对象的 attr() 方法来获取它的各种属性值 应用实例 <!DOCTYPE html> <html lang"en"> <head><meta charset"UT…

RK3288 Android8.1添加lvds以及gt9触摸屏(二)

现在先说gt9触摸屏如何配置 首先拿到硬件厂商提供的cfg以及gt9xx文件夹 驱动源码路径&#xff1a;kernel/drivers/input/touchscreen/gtxx 注&#xff1a;可以自己定义最后把gt9xx.h以及gt9xx.c文件放在哪&#xff0c;放在哪就在makefile里指定对应位置 1.touchscreen文件夹…

耗时108天,阿里P8总结了 1000 道 Java 工程师面试题

半年前还在迷茫该学什么&#xff0c;怎样才能走出现在的困境&#xff0c;半年后已经成功上岸阿里&#xff0c;感谢在这期间帮助我的每一个人。 面试中总结了 1000 道经典的 Java 面试题&#xff0c;里面包含面试要回答的知识重点&#xff0c;并且我根据知识类型进行了分类&…

写一个自定义View你都需要注意什么

本文主要是记录一下继承子View&#xff0c;所需要实现的方法&#xff0c;以及对自己的知识做一下梳理和记录&#xff0c;其中不少内容觉得自己应该是会的&#xff0c;但是实际写起来&#xff0c;还是遇到不少阻碍 构造方法 首先构造先了解一下构造方法&#xff0c;一般来说&a…

和悦未来社区:助力共同富裕,三思打造智慧社区新样板

“共同富裕是社会主义的本质要求&#xff0c;是中国式现代化的重要特征&#xff0c;是人民群众的共同期盼。” 2021年5月20日&#xff0c;《中共中央 国务院关于支持浙江高质量发展建设共同富裕示范区的意见》正式发布。 浙江省作为共同富裕先行示范省份&#xff0c;行而不辍…

SpringCloud microservice-student-consumer-80服务消费者项目建立(四)

新建一个服务器提供者module子模块&#xff0c;类似前面建的common公共模块&#xff0c;名称是 microservice-student-consumer-1001 pom.xml修改&#xff1a; <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSc…

01 UML概述

UML概述 (1) 规约系统的制品–UML适用于对所有重要的分析、设 计和实现决策进行详细描述 (2) 构造系统的制品–UML描述的模型可与各种编程语言直接相关联 UML应用范围 (1)可用于对象方法和构件方法&#xff1b; (2)可用于 ●所有应用领域(例如&#xff0c;航空航天、财政、通…

重生之我测阿里云U1实例(通用算力型实例)

官方福利&#xff01;&#xff01;&#xff01;&#xff01;大厂羊毛你确定不薅&#xff1f;&#xff1f;&#xff1f; 参与ECSU实例评测&#xff0c;申请免费体验机会&#xff1a;https://developer.aliyun.com/mission/review/ecsu 参与ECSU实例评测&#xff0c;申请免费体验…

CVPR 2023 | 基于金字塔模型的异常检测方法

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 论文链接&#xff1a;https://arxiv.org/pdf/2211.11317 0.背景&#xff1a; 工业异常检测旨在发现产品的异常区域&#xff0c;在工业质量检测中发挥着重要作用。在工业场景中&#xff0c;很容易获得大量的正…

阿里云RAM凭据插件应用纪实

官方传送 官方文档传送门 官方源码传送门 记录日期 2023-06-13 背景简介 项目中主要使用了OSS&#xff0c;本文记录在OSS SDK中的使用方法 引入依赖 <dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactI…