导读
上一篇文章《ShardingJdbc分库分表实战案例解析(上)》中我们初步介绍了使用ShardingJdbc实现订单数据分散存储的分库分表方法,在本篇文章中将重点介绍在不停服的情况下实现数据分片存储的在线扩容。具体将以如下两个常见的场景进行演示:1)、尚未进行分库分表的单库单表系统如何平稳的实施分库分表方案;2)、已经实施过分库分表方案的系统,由于数据量的持续增长导致原有分库分表不够用了,需要二次扩容的情况。
实施方案概述
在具体演示之前,我们先简单聊一聊关于分库分表数据迁移中,几种常见的思路,具体如下:
1)、停服迁移
停服迁移方案,是数据迁移方案中最常见、也是相对安全,但是也是最不能让人接受的方案。之所以说它安全,是因为停服之后就不会有新的数据写入,这样能保证数据迁移工作能够在一个相对稳定的环境中进行,因而能较大程度上避免迁移过程中产生数据不一致的问题。
但停服务方案会比较严重伤害用户体验、降低服务的可用性,而如果数据量较大迁移需要大量的时间的话,长时间的停服会严重影响业务,对于强调"9999"服务体验的互联网公司来说,停服方案绝对是让人不能接受的。
一般来说停服方案更适合于哪些非7X24小时,且对自身数据一致性要求非常高的系统,例如社保基金、银行核心系统等。当然,这也并不是说非停服方案,做不到数据迁移的绝对准确,仅仅在于这些系统出于管理、规则等非技术因素上的考虑是否能够容忍小概率的风险事件罢了。
停服迁移的流程,一般如下:
1、提前进行演练、预估停服时间,发布停服公告;
2、停服,通过事先准备好的数据迁移工具(一般为迁移脚本),按照新的数据分片规则,进行数据迁移;
3、核对迁移数据的准确性;
4、修改应用程序代码,切换数据分片读写规则,并完成测试验证;
5、启动服务,接入外部流量;
2)、升级从库方案
关于升级从库的方案,一般是针对线上数据库配置了主从同步结构的系统来说的。其具体思路是,当需要重新进行分库分表扩容时,可将现有从库直接升级成主库。例如原先分库分表结构是A、B两个分库为主库、A0、B0分别为A、B对应的从库,具体如下图所示:
假设此时如果需要扩容,新增两个分库的话,那么可以将A0、B0升级为主库,如此原先的两个分库将变为4个分库。与此同时,将上层服务的分片规则进行更改,将原先uid%2=0(原先存在A库)的数据分裂为uid%4=0和uid%=2的数据分别存储在A和A0上;同时将uid%2=1(原先存在B库)的数据分裂为uid%4=1和uid%=3的数据分别存储在B和B0上。而因为A0库和A库,B0库与B1库数据相同,所以这样就不需要进行数据迁移了,只需要变更服务的分片规则配置即可。之后结构如下:
而之前uid%2的数据分配在2个库中,此时分散到4个库,由于老数据还在,所以uid%4=2的数据还有一半存储在uid%4=0的分库中。因此还需要对冗余数据进行清理,但这类清理并不影响线上数据的一致性,可以随时随地进行。
处理完成后,为保证高可用,以及下一步扩容的需求,可以为现有主库再次分配一个从库,具体如下图所示:
升级从库方案的流程,一般如下:
1、修改服务数据分片配置,做好新库和老库的数据映射;
2、DBA协助完成从库升级为主库的数据库切换;
3、DBA解除现有数据库主从配置关系;
4、异步完成冗余数据的清理;
5、重新为新的数据节点搭建新的从库;
这种方案避免了由于数据迁移导致的不确定性,但也有其局限性。首先,现有数据库主从结构要能满足新的分库分表的规划;其次,这种方案的主要技术风险点被转移至DBA,实施起来可能会有较大阻力,毕竟DBA也不见得愿意背这个锅;最后,由于需要在线更改数据库的存储结构,可能也会出现意想不到的情况,而如果还存在多应用共享数据库实例情况的话,情况也会变得比较复杂。
3)、双写方案
双写方案是针对线上数据库迁移时使用的一种常见手段,而对于分库分表的扩容来说,也涉及到数据迁移,所以也可以通过双写来协助分库分表扩容的问题。
双写方案实际上同升级从库的原理类似,都是做"分裂扩容",从而减少直接数据迁移的规模降低数据不一致的风险,只是数据同步的方式不同。双写方案的核心步骤是:1)、是直接增加新的数据库,并在原有分片逻辑中增加写链接,同时写两份数据;2)、与此同时,通过工具(例如脚本、程序等)将原先老库中的历史数据逐步同步至新库,此时由于新库只有新增写入,应用上层其他逻辑还在老库之中,所以数据的迁移对其并无影响;3)、对迁移数据进行校验,由于是业务直接双写,所以新增数据的一致性是非常高的(但需要注意insert、update、delete操作都需要双更新操作);4)、完成数据迁移同步,并校验一致后就可以在应用上层根据老库的分裂方式重新修改分片配置规则了。以前面的例子为例,双写方案如下图所示:
如上图所示原先的A、B两个分库,其中uid%2=0的存放在A库,uid%2=1的存放在B库;增加新的数据库,其中写入A库是双写A0库,写入B库时双写B0库。
之后分别将A库的历史数据迁移至A0库;B库的历史数据迁移至B0库。最终确保A库与A0库的数据一致;B库与B0库的数据一致。具体如下图所示:
之后修改分片规则,确保原先uid%2=0存放在A库的数据,在分裂为uid%4=0和uid%4=2的情况下能分别存储在A库和A0库中;原先uid%2=1存放在B库的数据,在分裂为uid%4=1和uid%4=3的情况下分别存在到B库和B0库之中。具体如下图所示:
双写方案避免了像升级从库那样改变数据库结构的风险,更容易由开发人员自己控制,但双写方案需要侵入应用代码,并且最终需要完成数据迁移和冗余数据删除两个步骤,实施起来也不轻松。
那么到底有没有一个绝对完美的方案呢?
答案其实是没有!因为无论哪种方案都无法避免要迁移数据,即便像升级从库那样避免了数据迁移,也无法避免对冗余数据进行删除的额外操作。但我们可以在数据迁移手段和重新处理数据分片的方式上进行优化,目前在分库分表领域著名的开源项目ShardingSphere本质上其实就是在这两方面进行了优化,从而提供了一组解决方案工具集。
具体来说ShardingSphere是由"Sharding-JDBC Sharding-Proxy Sharding-Scaling"三个核心组件组成的分库分表开源解决方案,Sharding-JDBC在《ShardingJdbc分库分表实战案例解析(上)》中我们已经介绍过。而Sharding-Proxy Sharding-Scaling则是专门用于设计处理分库分表扩容数据迁移问题的组件,其运行原理如下:
如上图所示,在ShardingSphere的解决方案中,当需要对Service(旧)服务进行分库分表扩容时,我们可以先部署Sharding-Scaling Sharding-Proxy组件进行数据迁移 数据分片预处理。具体来说步骤如下:
1)、在Sharding-Proxy中按照扩容方案配置好分片规则,启动服务并提供JDBC协议连接机制,此时通过Sharding-Proxy连接写入的数据会按照新的分片规则进行数据存储;
2)、部署Sharding-Scaling服务,并通过HTTP接口的形式,向其发送数据迁移任务配置(配置数据中有需要迁移的数据库连接串,也有Sharding-Proxy的数据连接串);
3)、启动Sharding-Scaling迁移任务后,Sharding-Scaling将根据目标数据源的Binlog日志变化,读取后重新发送至Sharding-Proxy进行分片数据的重新处理;
4)、当Sharding-Scaling迁移数据任务完成,检查数据迁移结果,如果没有问题,则可以修改Service(新)服务的数据分片规则,并完成Service(旧)服务的替换;
5)、确认扩容无误后,停止Sharding-Proxy Sharding-Scaling服务;
6)、异步完成冗余数据的清理(目前ShardingSphere还不支持数据迁移后自动完成冗余数据的清理,所以需要自己根据数据分裂规则,编写清除脚本);
上述过程完全自动,在完成数据迁移及重新分片前,旧服务保持持续服务,不会对线上造成影响;此外由于Sharding-Scaling一直处于监听目标数据源Binlog日志状态,所以即便在服务切换过程中,旧Service服务仍有数据写入,也会自动被Sharding-Proxy重新分片处理,所以不用担心会出现不一致。
此外Sharding-Scaling Sharding-Proxy通信方式采用的是JDBC连接原生连接方式以及基于Binlog日志的同步方案,所以在迁移效率上也是有保证的。接下来将基于Sharding-Scaling Sharding-Proxy方案具体演示在不停服的情况下进行分库分表在线扩容。
ShardingSphere分库分表在线扩容
还是以上一篇文章中的订单分库分表存储为例,将其原有的分库分表规划:1)、数据库节点2个(ds0、ds1);2)、每个库的分表数为32张表(0~31)。扩容为:1)、数据库节点4个(ds0、ds1、ds2、ds3);2)、每个库的分表数仍为32张表(0~31)。
首先我们部署Sharding-Scaling Sharding-Proxy进行在线数据迁移及数据分片处理,具体如下:
1)、部署Sharding-Proxy
该服务的作用是一个数据库中间件,我们在此服务上编辑好分库分表规则后,Sharding-Scaling会把原数据写入Sharding-Proxy,然后由Sharding-Proxy对数据进行路由后写入对应的库和表。
我们可以在Github上下载ShardingSphere对应的版本的源码(演示所用版本为4.1.1)自行编译,也可以下载已经编译好的版本文件。本次演示所用方式为源码编译,其执行程序目录如下:
代码语言:javascript复制/shardingsphere-4.1.1/sharding-distribution/sharding-proxy-distribution/target/apache-shardingsphere-4.1.1-sharding-proxy-bin.tar.gz
找到编译执行程序后进行解压!之后编辑"conf/server.yaml"文件,添加连接账号配置,具体如下:
代码语言:javascript复制authentication:
users:
root:
password: 123456
该配置主要是供Sharding-Scaling连接Sharding-Proxy时使用!之后编辑Sharding-Proxy的分库分表配置文件"conf/config-sharding.yaml ",按照新的扩容方案进行配置,具体如下:
代码语言:javascript复制#对外暴露的数据库名称
schemaName: order
dataSources:
ds_0:
url: jdbc:mysql://127.0.0.1:3306/order_0?serverTimezone=UTC&useSSL=false
username: root
password: 123456
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 300
ds_1:
url: jdbc:mysql://127.0.0.1:3306/order_1?serverTimezone=UTC&useSSL=false
username: root
password: 123456
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 300
ds_2:
url: jdbc:mysql://127.0.0.1:3306/order_2?serverTimezone=UTC&useSSL=false
username: root
password: 123456
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 300
ds_3:
url: jdbc:mysql://127.0.0.1:3306/order_3?serverTimezone=UTC&useSSL=false
username: root
password: 123456
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 300
shardingRule:
tables:
t_order:
actualDataNodes: ds_${0..3}.t_order_$->{0..31}
databaseStrategy:
inline:
shardingColumn: user_id
algorithmExpression: ds_${user_id % 4}
tableStrategy:
inline:
shardingColumn: order_id
algorithmExpression: t_order_${order_id % 32}
keyGenerator:
type: SNOWFLAKE
column: id
完成分库分表配置后,启动Sharding-Proxy服务,命令如下:
代码语言:javascript复制sh bin/start.sh
为了验证ShardingProxy的部署正确下,可以通过Mysql命令进行连接,并插入一条数据,验证其是否按照新的分库分表规则进行存储,具体如下:
代码语言:javascript复制#使用mysql客户端检查是否能链接成功
mysql -h 127.0.0.1 -p 3307 -uroot -p123456
mysql> show databases;
----------
| Database |
----------
| order |
----------
1 row in set (0.02 sec)
#执行以下脚本,写入成功后, 检查order_0~3的分表数据是否按照规则落库
mysql> insert into t_order values('d8d5e92550ba49d08467597a5263205b',10001,'topup',100,'CNY','2','3','1010010101',63631722,now(),now(),'测试sharding-proxy');
Query OK, 1 row affected (0.20 sec)
按照新的分库分表规则uid->63631722%4=2;orderId->100012=17,测试数据应该落在ds_2中的t_order_17表中!如正确插入,则说明新的分库分表规则配置正确。
2)、部署Sharding-Scaling
源码编译的Sharding-Scaling执行程序包路径为:
代码语言:javascript复制/shardingsphere-4.1.1/sharding-distribution/sharding-scaling-distribution/target/apache-shardingsphere-4.1.1-sharding-scaling-bin.tar.gz
解压后,启动Scaling服务,命令如下:
代码语言:javascript复制sh bin/start.sh
Sharding-Scaling是一个独立的数据迁移服务,其本身不与任何具体的环境关联,在创建迁移任务时,具体信息由接口传入。接下来我们调用Sharding-Scaling创建具体的迁移任务。
在此之前,原有分库中的数据有:
代码语言:javascript复制1)、userId=63631725;orderId=123458存储在ds_1中的t_order_2号表中;
2)、userId=63631722;orderId=123457存储在ds_0中的t_order_1号表中;
而按照新的规则数据1不需要迁移,数据2需要迁移至ds_2中的t_order_1号表中,具体创建Sharding-Scaling迁移任务的指令如下:
代码语言:javascript复制#提交order_0的迁移数据命令
curl -X POST --url http://localhost:8888/shardingscaling/job/start
--header 'content-type: application/json'
--data '{
"ruleConfiguration": {
"sourceDatasource": "ds_0: !!org.apache.shardingsphere.orchestration.core.configuration.YamlDataSourceConfigurationn dataSourceClassName: com.zaxxer.hikari.HikariDataSourcen properties:n jdbcUrl: jdbc:mysql://127.0.0.1:3306/order_0?serverTimezone=UTC&useSSL=false&zeroDateTimeBehavior=convertToNulln driverClassName: com.mysql.jdbc.Drivern username: rootn password: 123456n connectionTimeout: 30000n idleTimeout: 60000n maxLifetime: 1800000n maxPoolSize: 100n minPoolSize: 10n maintenanceIntervalMilliseconds: 30000n readOnly: falsen",
"sourceRule": "tables:n t_order:n actualDataNodes: ds_0.t_order_$->{0..31}n keyGenerator:n column: order_idn type: SNOWFLAKE",
"destinationDataSources": {
"name": "dt_1",
"password": "123456",
"url": "jdbc:mysql://127.0.0.1:3307/order?serverTimezone=UTC&useSSL=false",
"username": "root"
}
},
"jobConfiguration": {
"concurrency": 1
}
}'
以上我们提交了针对老库order_0的数据迁移任务,如果提交成功Sharding-Scaling会返回如下信息:
代码语言:javascript复制{"success":true,"errorCode":0,"errorMsg":null,"model":null}
此时可通过命令查看任务详情进度,命令及结果显示如下:
代码语言:javascript复制#curl http://localhost:8888/shardingscaling/job/progress/1;
{
"success": true,
"errorCode": 0,
"errorMsg": null,
"model": {
"id": 1,
"jobName": "Local Sharding Scaling Job",
"status": "RUNNING",
"syncTaskProgress": [{
"id": "127.0.0.1-3306-order_0",
"status": "SYNCHRONIZE_REALTIME_DATA",
"historySyncTaskProgress": [{
"id": "history-order_0-t_order_24#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_25#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_22#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_23#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_20#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_21#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_19#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_17#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_18#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_15#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_16#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_13#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_14#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_8#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_11#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_9#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_12#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_6#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_31#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_7#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_10#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_4#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_5#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_30#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_2#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_3#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_0#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_1#0",
"estimatedRows": 1,
"syncedRows": 1
}, {
"id": "history-order_0-t_order_28#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_29#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_26#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_27#0",
"estimatedRows": 0,
"syncedRows": 0
}],
"realTimeSyncTaskProgress": {
"id": "realtime-order_0",
"delayMillisecond": 8759,
"logPosition": {
"filename": "mysql-bin.000001",
"position": 190285,
"serverId": 0
}
}
}]
}
}
同样针对其他老库,如order_1数据库的迁移,也可以以类似的方式提交迁移任务!如果此时观察数据2,就会发现其已经被重新分片到ds_2的t_order_1号表中;但其之前的历史分片数据仍然会冗余在ds_0的t_order_1号表中(需要清理)。
假设此时,有一条通过老服务写入的"userId=63631723&orderId=123457"数据,那么其会被存储在ds_1的t_order_1号表中,而其最新存储规则应该在ds_3的t_order_1号表中。如果之前也同时开启了order_1库的Scaling数据迁移任务,那么此时该数据将会被自动重新迁移并分片至ds_3的t_order_1号表中。
完成数据迁移后,就可以将之前旧服务的分片规则进行调整并重新发布了,具体如下:
代码语言:javascript复制#SQL控制台打印(开发时配置)
spring.shardingsphere.props.sql.show = true
# 配置真实数据源
spring.shardingsphere.datasource.names=ds0,ds1,ds2,ds3
# 配置第1个数据源
spring.shardingsphere.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://127.0.0.1:3306/order_0?characterEncoding=utf-8
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456
# 配置第2个数据源
spring.shardingsphere.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://127.0.0.1:3306/order_1?characterEncoding=utf-8
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456
# 配置第3个数据源
spring.shardingsphere.datasource.ds2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds2.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds2.url=jdbc:mysql://127.0.0.1:3306/order_2?characterEncoding=utf-8
spring.shardingsphere.datasource.ds2.username=root
spring.shardingsphere.datasource.ds2.password=123456
# 配置第4个数据源
spring.shardingsphere.datasource.ds3.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds3.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds3.url=jdbc:mysql://127.0.0.1:3306/order_3?characterEncoding=utf-8
spring.shardingsphere.datasource.ds3.username=root
spring.shardingsphere.datasource.ds3.password=123456
# 配置t_order表规则
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..3}.t_order_$->{0..31}
# 配置t_order表分库策略(inline-基于行表达式的分片算法)
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds${user_id % 2}
# 配置t_order表分表策略
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression = t_order_$->{order_id % 32}
#如其他表有分库分表需求,配置同上述t_order表
# ...
以上内容详细介绍并演示了针对已经分库分表的系统进行二次扩容时使用Sharding-Scaling Sharding-Proxy进行数据迁移的过程;而关于尚未进行过分库分表,但需要进行分库分表的系统来说,其过程与二次扩容并无差别,这里就不再赘述!
分库分表实践中需要注意的其他问题
在具体的分库分表实践中还需要注意表的主键问题,一般可以考虑分布式ID生成方案,例如UUID等,避免在扩容迁移数据时发生主键冲突。另外关于Sharding-Scaling Sharding-Proxy的使用,也需要注意一些异常情况,目前Sharding-Scaling的版本还是Alpha版本,所以使用过程中不排除会有一些问题,可以多看看源码,增进了解!
最后由于篇幅的原因,就没有具体演示历史数据的清理方法,大家如果在实践中有更好的方法,也欢迎同步给我!以上就是本篇文章想要表达的全部内容了,希望对大家有用!
—————END—————
参考资料:
https://shardingsphere.apache.org/document/current/en/quick-start/shardingsphere-scaling-quick-start/
https://blog.csdn.net/beichen8641/article/details/106924189