亚马逊AWS官方博客

百尺竿头更进一步 – Amazon Aurora的读写能力扩展之ShardingSphere-Proxy篇

1.前言

Amazon Aurora 是亚马逊云科技自研的一项关系数据库服务,它在提供和开源数据库MySQL、PostgreSQL的完好兼容性同时,也能够提供和商业数据库媲美的性能和可用性。性能方面,Aurora MySQL能够支持到与开源标准MySQL同等配置下五倍的吞吐量,Aurora PostgreSQL能够支持与开源标准PostgreSQL同等配置下三倍的吞吐量的提升。在扩展性的角度,Aurora在存储与计算、横向与纵向方面都进行了功能的增强和创新。Aurora支持多达128TB的存储容量,而且支持10GB为单位的存储层动态收缩。计算方面,Aurora提供多个读副本的可扩展性配置支持一个区域内多达15个读副本的扩展,提供多主的架构来支持同一个区域内4个写节点的扩展,提供Serverless无服务器化的架构实例级别的秒级纵向扩展,提供全球数据库来实现数据库的低延迟跨区域扩展。

随着用户数据量的增长,Aurora已经提供了很好的扩展性,那是否可以进一步处理更多的数据量、支持更多的并发访问呢?您可以考虑利用分库分表的方式,来支持底层多个Aurora集群的配置。基于此,包含这篇博客在内的系列博客会进行相应的介绍,旨在为您进行分库分表时选择使用代理或者JDBC提供参考。

本篇博客会聚焦如何使用ShardingSphere-Proxy,一个开源的分库分表中间件工具,来进行数据库集群的构建,会涵盖分库分表、读写分离、动态配置等方面。

2. ShardingSphere-Proxy介绍

Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成

作为中间件,ShardingSphere-Proxy的定位是透明化的数据库代理端。它采用Apache2.0协议,持续迭代版本,最新版本为5.1.0,目前支持MySQL和PostgreSQL版本。它对应用程序透明,兼容MySQL/PostgreSQL协议的客户端。MySQL命令行mysql,MySQL workbench等都可以直接访问ShardingSphere-Proxy。

ShardingSphere-Proxy下层可以连接不同的数据库,这些数据库可以是同构也可以是异构的。用户可以有两种方式指定底层数据库的分库分表或者读写分离规则:1)根据yaml配置文件静态指定2)利用ShardingSphere提供的增强性的DistSQL语言来指定。因为DistSQL支持动态创建规则不需要重启Proxy本身,它成为ShardingSphere-Proxy未来发展的重点。

作为数据库代理,是否能够提供连接池增强用户并发访问的连接处理是需要考量的一方面,ShardingSphere-Proxy在添加数据源并进行初始化时,会支持为每个数据库配置一个Hikari连接池。Hikari是业界广泛使用的连接池,对性能损耗较小,而且被SpringBoot采用为缺省连接池。ShardingSphere-Proxy的连接池可以支持用户配置最大连接数、最大空闲时间以及缓存相关的信息等。除Hikari连接池外,ShardingSphere-Proxy也支持其它连接池的配置。

和现有SQL的语法兼容性也是用户衡量数据库代理的关键因素,因为这涉及到是否更改应用代码。以MySQL为例,ShardingSphere支持大部分的MySQL语法,但也有少量不支持的语法,比如optimize表、资源组的管理、用户的创建和GRANT权限管理等。具体可以查阅ShardingSphere的最新文档

下面会分享我对ShardingSphereProxy连接Aurora的几个维度的实验测试:1)分库分表 2)动态扩展 3)读写分离 4)多表join 5)故障恢复。

3. 环境构建

3.1 Aurora集群搭建

首先根据Aurora集群创建指南创建三套Aurora MySQL集群,机型为db.r5.2xlarge,每套集群有一个写节点一个读节点。

3.2 ShardingSphere-Proxy搭建

在与Aurora相同的可用区下启动一台EC2节点,机型为r5.8xlarge. 然后在上面安装ShardingSphere-Proxy。

3.2.1 下载安装包

直接下载二进制安装包,进行解压。下载最新版本5.1.0,它对DistSQL支持较好。

wget https://dlcdn.apache.org/shardingsphere/5.1.0/apache-shardingsphere-5.1.0-shardingsphere-proxy-bin.tar.gz
tar -xvf apache-shardingsphere-5.1.0-shardingsphere-proxy-bin.tar.gz

SharingSphereProxy自带的库里包含对PostgreSQL的JDBC driver,但不包含MySQL的driver。因为创建的集群是MySQL,需要将MySQL的JDBC driver拷贝到lib目录。

wget https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.47/mysql-connector-java-5.1.47.jar
cp mysql-connector-java-5.1.47.jar apache-shardingsphere-5.1.0-shardingsphere-proxy-bin/lib/

3.2.2 配置Proxy的服务端

在ShardingSphere-Proxy的根目录下,有个配置文件目录为conf,里面有一个文件是server.yaml,用来配置ShardingSphere-Proxy自己作为代理对外提供服务的信息以及元信息存放等。下面是一个配置示例,里面配置了用户权限信息,特定属性信息,以及元信息以集群模式存放在zookeeper里。

rules:
  - !AUTHORITY
    users:  //访问Proxy的用户名和密码信息
      - root@%:root
      - sharding@:sharding
    provider:  //控制用户对schema的登陆权限
      type: ALL_PRIVILEGES_PERMITTED
  - !TRANSACTION  //事务类型配置,支持本地事务、XA两阶段事务、BASE柔性事务
    defaultType: XA
    providerType: Atomikos

props:  //特定属性配置
  max-connections-size-per-query: 1
  proxy-hint-enabled: true //为强制路由使用,默认值为false

mode: //元信息存放的配置,shardingsphereProxy支持三种模式:内存、单机和集群
  type: Cluster
  repository:
    type: ZooKeeper //可以设置为zookeeper、etcd等
    props:
      namespace: shardingproxy
      server-lists: localhost:2181
      retryIntervalMilliseconds: 500
      timeToLiveSeconds: 60
      maxRetries: 3
      operationTimeoutMilliseconds: 500
  overwrite: false

3.3启动Proxy

直接在ShardingSphereProxy根目录下的bin对应着启动和停止脚本。运行时的日志在目录logs下。启动Proxy

bin/start.sh
bin/stop.sh

3.4 验证连接

如无特殊配置,ShardingSphereProxy默认使用3307端口。使用3.2.2中配置的用户名和密码登录proxy。在EC2上运行mysql命令行工具进行连接,连接成功。注意这里没有任何数据库,因为我们没有使用YAML配置文件预先配置数据源。

[ec2-user@ip-111-22-3-123 bin]$ mysql -h 127.0.0.1 -uroot --port 3307 -proot
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.7.22-ShardingSphere-Proxy 5.1.0 

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> show databases;
Empty set (0.01 sec)

4. 功能测试

4.1 DistSQL创建分片规则和数据分片测试

本节来验证ShardingSphere的基本的分库分表能力。ShardingSphereProxy支持两种方式创建分片规则和读写分离规则,YAML和DistSQL。DistSQL扩展了SQL语法,可以支持在线创建数据源、创建和更改建表规则,较为灵活,本文只介绍DistSQL的用例。

4.1.1.创建数据库

连接到ShardingSphere-Proxy,去创建数据库,作为逻辑的分布式数据库。

MySQL [(none)]> create database distsql_sharding_db;
Query OK, 0 rows affected (0.90 sec)

在各个Aurora集群上创建数据库,作为数据库源进行连接。其中,rshard1,rshard2,rshard3是我自己定义的连接Aurora数据库的alias。

alias rshard1=’mysql -h $dbname -u$username -p$password’
[ec2-user@ ip-111-22-3-123 bin]$ rshard1 -e "create database dist_ds";
[ec2-user@ ip-111-22-3-123 bin]$ rshard2 -e "create database dist_ds;"
[ec2-user@ ip-111-22-3-123 bin]$ rshard3 -e "create database dist_ds;"

4.1.2 创建数据源

在ShadingSphereProxy中运行下面DistSQL语句创建3个数据源,分别指向3个不同Aurora集群

MySQL [distsql_sharding_db]> add resource ds_0(url="jdbc:mysql://aurora-2-07-7-shard1.cluster-12345678.us-east-1.rds.amazonaws.com:3306/dist_ds?serverTimezone=UTC&useSSL=false",user=admin,password=12345678);
Query OK, 0 rows affected (0.03 sec)

MySQL [distsql_sharding_db]> add resource ds_1(url="jdbc:mysql://aurora-2-07-7-shard2.cluster-12345678.us-east-1.rds.amazonaws.com:3306/dist_ds?serverTimezone=UTC&useSSL=false",user=admin,password=12345678);
Query OK, 0 rows affected (0.06 sec)

MySQL [distsql_sharding_db]> add resource ds_2(url="jdbc:mysql://aurora-2-07-7-shard3.cluster-12345678.us-east-1.rds.amazonaws.com:3306/dist_ds?serverTimezone=UTC&useSSL=false",user=admin,password=12345678);
Query OK, 0 rows affected (0.05 sec)

4.1.3 创建分片规则

这里指明t_order表的分片规则,注意分片规则的表名和后续要创建的表表名一致。具体规则为:对底层的3个数据源(Aurora集群)按照order_id对表进行hash分片,分成6份。另外,对order_id采用值自动生成的策略,采用策略为snowflake算法。ShardingSphere支持两种分布式主键生成策略:UUID和雪花算法SNOWFLAKE。使用雪花算法生成的主键,二进制表示形式包含4部分,从高位到低位分表为:1bit符号位、41bit时间戳位、10bit工作进程位以及12bit序列号位。在ShardingSphereProxy中运行下面DistSQL语句建立分片规则:

MySQL [distsql_sharding_db]> CREATE SHARDING TABLE RULE t_order(
→ RESOURCES(ds_0,ds_1, ds_2),
→ SHARDING_COLUMN=order_id,
→ TYPE(NAME=hash_mod,PROPERTIES("sharding-count"=6)),
→ KEY_GENERATE_STRATEGY(COLUMN=order_id,TYPE(NAME=snowflake))
→ );
Query OK, 0 rows affected (0.02 sec)

4.1.4 建表

建表语句和普通MySQL建表语句一致。在ShardingSphereProxy中运行下面语句建表:

MySQL [distsql_sharding_db]> CREATE TABLE `t_order` ( `order_id` bigint NOT NULL, `user_id` int NOT NULL, `status` varchar(45) DEFAULT NULL, PRIMARY KEY (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    -> ;
Query OK, 0 rows affected (0.22 sec)

在ShardingSphereProxy上查看表的状态。

MySQL [distsql_sharding_db]> show tables;
+-------------------------------+------------+
| Tables_in_distsql_sharding_db | Table_type |
+-------------------------------+------------+
| t_order                       | BASE TABLE |
+-------------------------------+------------+
1 row in set (0.00 sec)

分别连接到3个Aurora集群上查看表是否自动创建。可以看到每个底层数据库集群上都创建了两张表,一共是6张表。而且表名是以“t_oder_”数字排序的

[ec2-user@ ip-111-22-3-123 bin]$ rshard1 -Ddist_ds -e "show tables;"
+-------------------+
| Tables_in_dist_ds |
+-------------------+
| t_order_0         |
| t_order_3         |
+-------------------+
[ec2-user@ ip-111-22-3-123 bin ]$ rshard2 -Ddist_ds -e "show tables;"
+-------------------+
| Tables_in_dist_ds |
+-------------------+
| t_order_1         |
| t_order_4         |
+-------------------+
[ec2-user@ ip-111-22-3-123 bin]$ rshard3 -Ddist_ds -e "show tables;"
+-------------------+
| Tables_in_dist_ds |
+-------------------+
| t_order_2         |
| t_order_5         |
+-------------------+

4.1.5 插入和查找数据

在ShardingSphere-Proxy中插入并查找数据,数据可以正常插入和查找成功。在ShardingSphere-Proxy中运行:

MySQL [distsql_sharding_db]> insert into t_order(user_id, status) values (1, 'ok');
insert into t_order(user_id, status) values (2, 'abc');
Query OK, 1 row affected (0.01 sec)

MySQL [distsql_sharding_db]> insert into t_order(user_id, status) values (2, 'abc');
insert into t_order(user_id, status) values (3, 'abc');
Query OK, 1 row affected (0.00 sec)

MySQL [distsql_sharding_db]> insert into t_order(user_id, status) values (3, 'abc');
insert into t_order(user_id, status) values (4, 'abc');
Query OK, 1 row affected (0.01 sec)

MySQL [distsql_sharding_db]> insert into t_order(user_id, status) values (4, 'abc');
insert into t_order(user_id, status) values (5, 'abc');
Query OK, 1 row affected (0.00 sec)

MySQL [distsql_sharding_db]> insert into t_order(user_id, status) values (5, 'abc');
insert into t_order(user_id, status) values (6, 'abc');
Query OK, 1 row affected (0.01 sec)

MySQL [distsql_sharding_db]> insert into t_order(user_id, status) values (6, 'abc');
insert into t_order(user_id, status) values (7, 'abc');
Query OK, 1 row affected (0.00 sec)

MySQL [distsql_sharding_db]> insert into t_order(user_id, status) values (7, 'abc');
insert into t_order(user_id, status) values (8, 'abc');
Query OK, 1 row affected (0.01 sec)

MySQL [distsql_sharding_db]> insert into t_order(user_id, status) values (8, 'abc');
Query OK, 1 row affected (0.00 sec)

MySQL [distsql_sharding_db]> insert into t_order(user_id, status) values (9, 'abc');
Query OK, 1 row affected (0.00 sec)

MySQL [distsql_sharding_db]> select * from t_order;
+--------------------+---------+--------+
| order_id           | user_id | status |
+--------------------+---------+--------+
| 708700161915748353 |       2 | abc    |
| 708700161995440128 |       5 | abc    |
| 708700169725542400 |       9 | abc    |
| 708700161877999616 |       1 | ok     |
| 708700161936719872 |       3 | abc    |
| 708700162041577472 |       7 | abc    |
| 708700161970274305 |       4 | abc    |
| 708700162016411649 |       6 | abc    |
| 708700162058354689 |       8 | abc    |
+--------------------+---------+--------+
9 rows in set (0.01 sec)

去各个Aurora集群中查找子表插入的数据,可以看到在Proxy插入的9条记录被打散到底层的6张表中。因为order_id为snowflake算法生成而数据量比较小,这里的数据并不均匀。

[ec2-user@ip-111-22-3-123 bin]$ rshard1 -Ddist_ds -e "select * from t_order_0;"
[ec2-user@ip-111-22-3-123 bin]$ rshard1 -Ddist_ds -e "select * from t_order_3;"
+--------------------+---------+--------+
| order_id           | user_id | status |
+--------------------+---------+--------+
| 708700161915748353 |       2 | abc    |
+--------------------+---------+--------+
[ec2-user@ip-111-22-3-123 bin]$ rshard2 -Ddist_ds -e "select * from t_order_1;"
[ec2-user@ip-111-22-3-123 bin]$ rshard2 -Ddist_ds -e "select * from t_order_4;"
+--------------------+---------+--------+
| order_id           | user_id | status |
+--------------------+---------+--------+
| 708700161995440128 |       5 | abc    |
| 708700169725542400 |       9 | abc    |
+--------------------+---------+--------+
[ec2-user@111-22-3-123 bin]$ rshard3 -Ddist_ds -e "select * from t_order_2;"
+--------------------+---------+--------+
| order_id           | user_id | status |
+--------------------+---------+--------+
| 708700161877999616 |       1 | ok     |
| 708700161936719872 |       3 | abc    |
| 708700162041577472 |       7 | abc    |
+--------------------+---------+--------+
[ec2-user@ip-111-22-3-123 bin]$ rshard3 -Ddist_ds -e "select * from t_order_5;"
+--------------------+---------+--------+
| order_id           | user_id | status |
+--------------------+---------+--------+
| 708700161970274305 |       4 | abc    |
| 708700162016411649 |       6 | abc    |
| 708700162058354689 |       8 | abc    |
+--------------------+---------+--------+</code></pre></div>

上述实验验证了ShardingSphere-Proxy具有创建逻辑库、连接数据源、创建分片规则、创建逻辑表时会自动在底层数据库上创建子表、能够执行查询的分发以及聚合能力。

4.2. 动态伸缩验证(在线扩展分片)

本节来验证ShardingSphere-Proxy是否具有动态更改表的分片规则的能力。

ShardingSphere-Proxy提供在线更改分片规则的能力,但是如果子表已经按照之前的规则创建成功,则不会有新的子表随着分片数目的增多被创建出来,也不会有原来的子表随着分片数目的减少而被删除。所以需要手动在底层分片数据库上创建表名和迁移数据。

将4.1节里的表的分片数从6调高到9,修改分片规则本身能够成功,但是后续查找会出错,因为没有新的子表创建出来。在ShardingSphere-Proxy上运行下面DistSQL:

MySQL [distsql_sharding_db]> alter SHARDING TABLE RULE t_order(
    -> RESOURCES(ds_0,ds_1, ds_2),
    -> SHARDING_COLUMN=order_id,
    -> TYPE(NAME=hash_mod,PROPERTIES("sharding-count"=9)),
    -> KEY_GENERATE_STRATEGY(COLUMN=order_id,TYPE(NAME=snowflake))
    -> );
Query OK, 0 rows affected (0.01 sec)

MySQL [distsql_sharding_db]> select * from t_order;
ERROR 1146 (42S02): Table 'dist_ds.t_order_6' doesn't exist

如果此时在子集群上分别创建好对应的子表,再在ShardingSphere-Proxy上查找就不会再出错。连接到3个Aurora集群,手动创建子表

[ec2-user@ip-111-22-3-123 bin]$ rshard1 -Ddist_ds -e "create table t_order_6(order_id bigint not null, user_id int not null, status varchar(45) default null, primary key(order_id)) engine=innodb default charset=utf8mb4; "
[ec2-user@ ip-111-22-3-123 bin]$ rshard2 -Ddist_ds -e "create table t_order_7(order_id bigint not null, user_id int not null, status varchar(45) default null, primary key(order_id)) engine=innodb default charset=utf8mb4; "
[ec2-user@ip-111-22-3-123 bin]$ rshard3 -Ddist_ds -e "create table t_order_8(order_id bigint not null, user_id int not null, status varchar(45) default null, primary key(order_id)) engine=innodb default charset=utf8mb4; "

Proxy查找整个逻辑表不再报错。在ShardingSphere-Proxy上运行下面SQL:

MySQL [distsql_sharding_db]> select * from t_order;
+--------------------+---------+--------+
| order_id           | user_id | status |
+--------------------+---------+--------+
| 708700161915748353 |       2 | abc    |
| 708700161995440128 |       5 | abc    |
| 708700169725542400 |       9 | abc    |
| 708700161877999616 |       1 | ok     |
| 708700161936719872 |       3 | abc    |
| 708700162041577472 |       7 | abc    |
| 708700161970274305 |       4 | abc    |
| 708700162016411649 |       6 | abc    |
| 708700162058354689 |       8 | abc    |
+--------------------+---------+--------+
9 rows in set (0.01 sec)

如果有新的数据插入,会按照新的分片规则进行到子表的映射。在ShardingSphere-Proxy上查看SQL语句的查询计划:

MySQL [distsql_sharding_db]> preview insert into t_order values(7, 100, 'new');
+------------------+---------------------------------------------+
| data_source_name | sql                                         |
+------------------+---------------------------------------------+
| ds_1             | insert into t_order_7 values(7, 100, 'new') |
+------------------+---------------------------------------------+
1 row in set (0.00 sec)

MySQL [distsql_sharding_db]> insert into t_order values(7, 100, 'new');
Query OK, 1 row affected (0.00 sec)

登录到Aurora子集群上查看子表,可以看到数据已经成功插入。

[ec2-user@ip-111-22-3-123 bin]$ rshard2 -Ddist_ds -e "select * from t_order_7;"
+----------+---------+--------+
| order_id | user_id | status |
+----------+---------+--------+
|        7 |     100 | new    |
+----------+---------+--------+

再来看下在线减少分片的情况。如果将分片数目调小,比如调到3,表里的已有数据不会被迁移,查找整张表时只能拿到部分数据。在ShardingSphere-Proxy上运行下面DistSQL和SQL语句:

MySQL [distsql_sharding_db]> alter SHARDING TABLE RULE t_order(
    ->     RESOURCES(ds_0,ds_1, ds_2),
    ->     SHARDING_COLUMN=order_id,
    ->     TYPE(NAME=hash_mod,PROPERTIES("sharding-count"=3)),
    ->     KEY_GENERATE_STRATEGY(COLUMN=order_id,TYPE(NAME=snowflake))
    ->     );
Query OK, 0 rows affected (0.02 sec)
MySQL [distsql_sharding_db]> select * from t_order;
+--------------------+---------+--------+
| order_id           | user_id | status |
+--------------------+---------+--------+
| 708700161877999616 |       1 | ok     |
| 708700161936719872 |       3 | abc    |
| 708700162041577472 |       7 | abc    |
+--------------------+---------+--------+
3 rows in set (0.00 sec)

经过上面验证,我们的结论是ShardingSphereProxy的分片规则是可以在线更改的,但子表的创建和数据的重新分布需要手动去完成。

4.3 绑定表和广播表的测试

本节来验证ShardingSphere-Proxy对于多表join的支持。尽管OLTP的数据库中的操作通常较为简单,但也有可能会涉及到多表join的情况。ShardingSphereProxy针对多表join的优化有支持绑定表和广播表。如果两张表是绑定表而且join时采用的是shard key,可以进行两张表的join。广播表通过把小表复制到各个节点,可以实现大表和小表的快速join。

4.3.1 绑定表

ShardingSphereProxy的绑定表可以通过DistSQL里的CREATE SHARDING BINDING TABLE RULES 来绑定两张表。这里以4.1节中提到的t_order表和新创建的一张表t_order_item为例进行展开。

连接到ShardingSphere-Proxy上运行下面DistSQL和SQL语句。

MySQL [distsql_sharding_db]> CREATE SHARDING TABLE RULE t_order_item(
    ->  RESOURCES(ds_0,ds_1, ds_2),
    ->  SHARDING_COLUMN=order_id,
    -> TYPE(NAME=hash_mod,PROPERTIES("sharding-count"=6)));
Query OK, 0 rows affected (0.04 sec)

MySQL [distsql_sharding_db]> CREATE TABLE `t_order_item` ( `order_id` bigint NOT NULL, `item_id` int NOT NULL, `name` varchar(45) DEFAULT NULL, PRIMARY KEY (`item_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;
Query OK, 0 rows affected (0.08 sec)

创建了binding rule以后,查看join计划,我们看到join下推到对应子表和子表的join上。在ShardingSphere-Proxy上运行:

MySQL [distsql_sharding_db]>  CREATE SHARDING BINDING TABLE RULES (t_order,t_order_item);
Query OK, 0 rows affected (0.04 sec)

MySQL [distsql_sharding_db]> preview select * from t_order, t_order_item where t_order.order_id=t_order_item.order_id;
+------------------+------------------------------------------------------------------------------------------+
| data_source_name | sql                                                                                      |
+------------------+------------------------------------------------------------------------------------------+
| ds_0             | select * from t_order_0, t_order_item_0 where t_order_0.order_id=t_order_item_0.order_id |
| ds_0             | select * from t_order_3, t_order_item_3 where t_order_3.order_id=t_order_item_3.order_id |
| ds_1             | select * from t_order_1, t_order_item_1 where t_order_1.order_id=t_order_item_1.order_id |
| ds_1             | select * from t_order_4, t_order_item_4 where t_order_4.order_id=t_order_item_4.order_id |
| ds_2             | select * from t_order_2, t_order_item_2 where t_order_2.order_id=t_order_item_2.order_id |
| ds_2             | select * from t_order_5, t_order_item_5 where t_order_5.order_id=t_order_item_5.order_id |
+------------------+------------------------------------------------------------------------------------------+
6 rows in set (0.01 sec)

4.3.2 广播表

广播表是指每张表在每个库里都有一个完整的备份,可以通过CREATE SHARDING BROADCAST TABLE RULES来指定。

MySQL [distsql_sharding_db]> CREATE SHARDING BROADCAST TABLE RULES (t_user);
Query OK, 0 rows affected (0.03 sec)

MySQL [distsql_sharding_db]> create table t_user (user_id int, name varchar(100));
Query OK, 0 rows affected (0.04 sec)

登录到各个shard Aurora集群查看创建的表。可以看到与分片表的子表名末尾有数字序号不同的是 ,广播表对应的每个库上的名字是相同的,就是逻辑表名本身。

[ec2-user@ip-111-22-3-123 bin]$ rshard1 -D dist_ds -e "show tables like '%user%';"
+----------------------------+
| Tables_in_dist_ds (%user%) |
+----------------------------+
| t_user                     |
+----------------------------+
[ec2-user@ip-111-22-3-123 bin]$ rshard2 -D dist_ds -e "show tables like '%user%';"
+----------------------------+
| Tables_in_dist_ds (%user%) |
+----------------------------+
| t_user                     |
+----------------------------+
[ec2-user@ip-111-22-3-123 bin]$ rshard3 -D dist_ds -e "show tables like '%user%';"
+----------------------------+
| Tables_in_dist_ds (%user%) |
+----------------------------+
| t_user                     |
+----------------------------+

在ShardingSphereProxy中运行广播表和其它表的join,采用的是本地join的方式。

MySQL [distsql_sharding_db]> preview select * from t_order, t_user where t_order.user_id=t_user.user_id;
+------------------+------------------------------------------------------------------------+
| data_source_name | sql                                                                    |
+------------------+------------------------------------------------------------------------+
| ds_0             | select * from t_order_0, t_user where t_order_0.user_id=t_user.user_id |
| ds_0             | select * from t_order_3, t_user where t_order_3.user_id=t_user.user_id |
| ds_1             | select * from t_order_1, t_user where t_order_1.user_id=t_user.user_id |
| ds_1             | select * from t_order_4, t_user where t_order_4.user_id=t_user.user_id |
| ds_2             | select * from t_order_2, t_user where t_order_2.user_id=t_user.user_id |
| ds_2             | select * from t_order_5, t_user where t_order_5.user_id=t_user.user_id |
+------------------+--------

上面实验验证了ShardingSphere-Proxy是可以支持两张绑定表的join,以及广播表和分片表的join的。对于非绑定的两张分片表的join,ShardingSphere-Proxy有一个Federation的功能是在支持的,但还不是很成熟,建议后续持续关注。

4.4读写分离功能验证

本节来验证ShardingSphere-Proxy对于读写分离的支持。随着业务增长,写和读的负载分别在不同的数据库节点上能够有效提供整个数据库集群的处理能力。Aurora通过读/写的endpoint可以满足用户写和强一致性读的需求,只读的endpoint可以满足用户非强一致性读的需求。Aurora的读写延迟在毫秒级别,比MySQL基于binlog的逻辑复制要低得多,所以有很多负载是可以直接打到只读endpoint的。

ShardingSphereProxy提供的读写分离的特性可以进一步可以封装Aurora的读/写端点和只读端点。用户可以直接连接到Proxy的端点,即可进行自动的读写分离。ShardingSphereProxy对特殊情况的处理逻辑是:1)同一线程且同一数据库连接内,如果有写入操作,则后续的读操作均从主库读取 2)可以通过Hint的机制强制把读请求发到写节点(主库)。下面会以Aurora3个集群中的第一个集群来验证ShardingSphere-Proxy读写分离的能力。

4.4.1 查看Aurora集群读/写端点和只读端点

Aurora集群有两个端点,写的端点和读的端点。

4.4.2 在Aurora集群中创建数据库

连接到Aurora集群中运行:

[ec2-user@ip-111-22-3-123 ~]$ rdbw -e "create database wr_ds;"

4.4.3 数据源配置

在ShardingSphere-Proxy上创建数据源,写的数据源指向Aurora的读写endpoint,读的数据源指向Aurora的只读endpoint。注意:对域名的情况,ShardingSphereProxy只支持通过url的方式创建数据源,尚未支持通过HOST、Port的方式。连接到ShardingSphere-Proxy上创建逻辑数据库distsql_rwsplit_db并在改数据库中添加数据源:

MySQL [(none)]> create database distsql_rwsplit_db;
Query OK, 0 rows affected (0.02 sec)
MySQL [(none)]> use distsql_rwsplit_db;
Database changed
MySQL [distsql_rwsplit_db]> add resource write_ds(url="jdbc:mysql://aurora-2-07-7-shard1.cluster-12345678.us-east-1.rds.amazonaws.com:3306/wr_ds?serverTimezone=UTC&useSSL=false",user=admin,password=12345678), read_ds(url="jdbc:mysql://aurora-2-07-7-shard1.cluster-ro-12345678.us-east-1.rds.amazonaws.com:3306/wr_ds?serverTimezone=UTC&useSSL=false",user=admin,password=12345678);
Query OK, 0 rows affected (0.08 sec)

4.4.4 读写分离规则配置

创建读写分离规则,写请求发到写的数据源,读请求发到读的数据源。与分库分表规则要求RULE后面必须是表名不同的是,这里的RULE后面跟的是数据源的名字,适用于在这个数据库里创建的所有的表。在ShardingSphere-Proxy上运行下面DistSQL语句:

MySQL [distsql_ rwsplit_db]> CREATE READWRITE_SPLITTING RULE wr_ds (
    -> WRITE_RESOURCE=write_ds,
    -> READ_RESOURCES(read_ds),
    -> TYPE(NAME=random)
    -> );
Query OK, 0 rows affected (0.36 sec)

4.4.5 建表

创建一张普通表,建表语句和MySQL建表语句一致。在ShardingSphere-Proxy上运行下面SQL语句:

MySQL [distsql_ rwsplit_db]> create table wr_table (a int, b int, c varchar(20));
Query OK, 0 rows affected (0.17 sec)

4.4.6 检查读写分离是否实现

在ShardingSphere-Proxy上运行下面语句查看查询计划,查看语句是发送到底层哪个数据源。可以看到:写请求发送到写节点,读请求会发送到读写点。

MySQL [distsql_rwsplit_db]> preview insert into wr_table values(1,1,'ab');
+------------------+---------------------------------------+
| data_source_name | sql                                   |
+------------------+---------------------------------------+
| write_ds         | insert into wr_table values(1,1,'ab') |
+------------------+---------------------------------------+
1 row in set (0.10 sec)
MySQL [distsql_rwsplit_db]> preview select * from wr_table;
+------------------+------------------------+
| data_source_name | sql                    |
+------------------+------------------------+
| read_ds          | select * from wr_table |
+------------------+------------------------+
1 row in set (0.02 sec)

运行一个脚本来多次操作,再去Aurora集群指标监控中去验证。该脚本是一个循环,运行1000次,每次会插入一条记录,并查找表的记录总条数。

[ec2-user@ip-111-22-3-123 shardingproxy]$ cat testReadWrite.sh 

#!/bin/bash
n=1
while [ $n -le 1000 ]
do
    mysql -h 127.0.0.1 -uroot --port 3307 -proot -Ddistsql_rwsplit_db -e "insert into wr_table values($n,$n,'ok');"
    mysql -h 127.0.0.1 -uroot --port 3307 -proot -Ddistsql_rwsplit_db -e "select count(*) from wr_table;"
    let n++
done  

查看Aurora集群的写节点和读节点的读写延迟,可以看到写延迟只在写节点上发生,读延迟只在读节点上发生。说明读写分离规则生效。

尽管Aurora的写和读节点之间的复制延迟很低在毫秒级别,但某些应用还是会有强一致性的需求,即要求写后立刻可以读。这时候,可以采用强制将读请求发送到写节点的方式。ShardingSphereProxy通过hint的方式来支持。首先需要在前面提到的conf/server.yaml里添加一个属性proxy-hint-enabled: true。然后在连接中显式设置 readwrite_splitting hint source 值为write来开启强制路由到写节点通过设置值为auto或者clear hint可以采用默认的规则。readwrite_splitting hint source可以在session级别生效。

在ShardingSphere-Proxy上依次运行下面语句。可以看到默认的读请求是发送到读节点,将readwrite_splitting hint source设置为write以后,会发送到写节点,再设成auto,可以发回至读写点。

MySQL [distsql_rwsplit_db]> preview select count(*) from wr_table;
+------------------+-------------------------------+
| data_source_name | sql                           |
+------------------+-------------------------------+
| read_ds          | select count(*) from wr_table |
+------------------+-------------------------------+
1 row in set (0.01 sec)

MySQL [distsql_rwsplit_db]> set readwrite_splitting hint source = write;
Query OK, 0 rows affected (0.00 sec)

MySQL [distsql_rwsplit_db]> preview select count(*) from wr_table;
+------------------+-------------------------------+
| data_source_name | sql                           |
+------------------+-------------------------------+
| write_ds         | select count(*) from wr_table |
+------------------+-------------------------------+
1 row in set (0.01 sec)
MySQL [distsql_rwsplit_db]> set readwrite_splitting hint source = auto;
Query OK, 0 rows affected (0.00 sec)

MySQL [distsql_rwsplit_db]> preview select count(*) from wr_table;
+------------------+-------------------------------+
| data_source_name | sql                           |
+------------------+-------------------------------+
| read_ds          | select count(*) from wr_table |
+------------------+-------------------------------+
1 row in set (0.00 sec)

另外不使用YAML文件更改的方式是直接在DistSQL里先后设置两个变量proxy_hint_enabled和readwrite_splitting hint source。

MySQL [distsql_rwsplit_db]> set variable proxy_hint_enabled=true;
Query OK, 0 rows affected (0.01 sec)
MySQL [distsql_rwsplit_db]> set readwrite_splitting hint source = write;
Query OK, 0 rows affected (0.01 sec)
MySQL [distsql_rwsplit_db]> preview select * from wr_table;
+------------------+------------------------+
| data_source_name | sql                    |
+------------------+------------------------+
| write_ds         | select * from wr_table |
+------------------+------------------------+
1 row in set (0.00 sec)

以上实验验证了ShardingSphere-Proxy有良好的读写分离的能力。它验证了底下连接单个Aurora集群进行读写分离的场景。如果既需要分库分表又需要读写分离,ShardingSphere-Proxy也是支持的。比如先分到3个Aurora集群,然后每个集群需要提供读写分离的能力,我们可以直接将读写分离规则后面定义的数据源名称(4.4.4里的wr_ds)放在分库分表规则对每张表指定的数据源里(4.1.3里的ds_0,ds_1,ds_2)。

4.5. 故障恢复验证

本节来验证ShardingSphere-Proxy对于Aurora集群故障切换的感知能力。在Aurora集群发生主备切换时,如果Proxy能够动态检测到主备切换并连接到新的主数据库是比较理想的。本节实验仍然是验证第一个Aurora集群。

测试脚本如下,它会持续连接到写节点并发送update请求,每次请求间隔1秒钟。

[ec2-user@ip-111-22-3-123 shardingproxy]$ cat testFailover.sh 
#!/bin/bash
while true
do
    mysql -h 127.0.0.1 -uroot --port 3307 -proot -Ddistsql_rwsplit_db -e "update wr_table set c='failover' where a = 1;"
    now=$(date +"%T")
    echo "update done: $now"
    sleep 1
done    

运行脚本,然后在Aurora集群的写节点上点击Action->Failover。会启动Aurora写节点和读节点的自动切换。在切换过程中,整个集群的读/写endpoint和只读endpoint维持不变,只是底层映射的节点发生变化。

通过观测Aurora的Event(事件),可以看到整个故障切换在30秒左右完成。

遗憾的是,应用程序直接连接ShardingSphereProxy也就是前面的运行脚本不能自动监测到底层的IP变化。运行脚本一直抛错:

ERROR 1290 (HY000) at line 1: The MySQL server is running with the --read-only option so it cannot execute this statement
update done: 15:04:04
ERROR 1290 (HY000) at line 1: The MySQL server is running with the --read-only option so it cannot execute this statement
update done: 15:04:05
ERROR 1290 (HY000) at line 1: The MySQL server is running with the --read-only option so it cannot execute this statement
update done: 15:04:06

直接在MySQL命令行连接到Proxy也是会有一样的错误。

MySQL [distsql_rwsplit_db]> update wr_table set c="failover" where a =2;
ERROR 1290 (HY000): The MySQL server is running with the --read-only option so it cannot execute this statement

分析原因在于Aurora发生故障切换的时候,读写endpoint和IP的映射会发生变化,而ShardingSphere的连接池在连接Aurora的时候,没有更新到新的IP上。我们可以采用下面的workaround可以使ShardingSphereProxy指向新的写节点,即重新创建数据源。尽管数据源本身定义没有发生变化,但是通过重建数据源alter resource的操作, ShardingSphereProxy会重新拿取一遍endpoint到IP的映射,所以能够成功运行。

MySQL [distsql_rwsplit_db]> alter resource write_ds(url="jdbc:mysql://aurora-2-07-7-shard1.cluster-12345678.us-east-1.rds.amazonaws.com:3306/wr_ds?serverTimezone=UTC&useSSL=false",user=admin,password=12345678), read_ds(url="jdbc:mysql://aurora-2-07-7-shard1.cluster-ro-12345678.us-east-1.rds.amazonaws.com:3306/wr_ds?serverTimezone=UTC&useSSL=false",user=admin,password=12345678);
Query OK, 0 rows affected (0.05 sec)

MySQL [distsql_rwsplit_db]> update wr_table set c="failover" where a =2;
Query OK, 1 row affected (0.01 sec)

每次Aurora故障切换时,我们可以检测故障切换的event,或者是在应用收到read-only报错时显式调用上面语句。为了降低对应用的影响,我们可以采用Lambda的方式将failover重置数据源的操作自动化。因为Aurora的failover的事件是可以被监测到的,我们可以写一个Lambda函数,在监测到failover成功以后,显示调用更改resource的操作。

总体的思路是: RDS通过Event Subscription将Event的通知信息传递给SNS topic,再由SNS topic传递给Lambda方法,然后在Lambda方法里显式连接ShardingProxy调用alter resource 的DistSQL语句。

具体步骤如下:

4.5.1 创建SNS

按照SNS创建指南创建SNS。打开SNS的dashboard,点击创建SNS topic,选择Standard标准类型。其它选择默认或者根据需要调整。

4.5.2对要进行的Aurora集群创建Event Subscription

在RDS的Event Subscriptions上,点击“Create Event Subscription”,在弹出的选项卡中选择Target为上一步骤创建的SNS,Source type选择为Cluster,cluster里面选中我们需要关注的Aurora的集群,事件选择Failover事件。

4.5.3 创建Lamdba方法

因为Lambda要调用VPC里的EC2上部署的ShardingProxy,应该给它绑定一个专门的Role,这个Role有权限在VPC里执行Lambda方法: AWSLambdaVPCAccessExecutionRole

按照IAM Role创建文档创建Role和Policy,使failoverlambda的role有AWSLambdaVPCAccessExecutionRole的权限。

接下来按照Lambda文档创建Lambda方法

创建好Lambda方法以后,点击Trigger,指明为SNS,并指明在4.5.1里创建的SNS topic。

4.5.4 编写Lambda方法

import os
import json
import pymysql

# connect to ShardingProxy to reset the data source    
def resetDataSource():
    db = pymysql.connect(host='111.22.3.123', user='root', password='root', port=3307, database='distsql_rwsplit_db')
    
    cur = db.cursor()
    SQL = "alter resource write_ds(url=\"jdbc:mysql://aurora-2-07-7-shard1.cluster-12345678.us-east-1.rds.amazonaws.com:3306/wr_ds?serverTimezone=UTC&useSSL=false\",user=admin,password=12345678), read_ds(url=\"jdbc:mysql://aurora-2-07-7-shard1.cluster-ro-12345678.us-east-1.rds.amazonaws.com:3306/wr_ds?serverTimezone=UTC&useSSL=false\",user=admin,password=12345678);"
    print (SQL)
    cur.execute(SQL)
    result = cur.fetchall()
    for x in result:
        print(x)
    db.close()

def lambda_handler(event, context):
    wholeMessage = event['Records'][0]['Sns']['Message']
    print ("whole message" + wholeMessage)
    
    wholeMessageInJson = json.loads(wholeMessage)
    eventMessage = wholeMessageInJson['Event Message']
    print ("event message: " + eventMessage)
    
    isFailover = eventMessage.startswith('Completed failover to DB instance')
    if isFailover == True:
        print ("Failover completed! " + eventMessage)
        resetDataSource()

    return {
        'statusCode': 200,
        'body': Lambda Invocation Successful!'
    }

Lambda方法是Python语言书写的,在访问ShardingSphereProxy时以MySQL方式访问,所以需要引入pymysql的lib库。具体方法为:

1)在Linux上安装pymysql,以Amazon-Linux虚拟机为例,会默认安装到目录./home/ec2-user/.local/lib/python3.7/site-packages/pymysql下

2)将pymysql目录拷贝到临时目录/tmp

3)写Lambda方法,存储到lambda_function.py文件中

4)打包 zip -r lambda.zip pymysql lambda_function.py

5) 在控制台通过S3 或者本地上传。

4.5.5 设置 ShardingSphereProxy所在EC2的security group

因为Lambda要在VPC里访问ShardingSphereProxy,而ShardingSphereProxy以3307端口运行,应该配置相应secuity group,打开3307端口给同一个VPC内部的访问。依据安全组配置文档配置成的security group如下:

4.5.6 验证failover­

重复本小节开始的操作,运行testFailover.sh,然后手动在RDS console页failover Aurora节点,会发现testFailover.sh持续稳定输出, 不会再出现read-only的错误。

update done: 13:44:44
…
update done: 13:44:47
update done: 13:45:00
update done: 13:45:01
…
update done: 13:45:17

去cloudwatch里查看Lambda function的日志,会发现Lambda被成功调用。

以上实验验证了ShardingSphere-Proxy对于Aurora集群故障切换的感知能力。尽管ShardingSphere-Proxy自己没有提供良好的匹配性,通过监测Aurora集群事件触发Lamdba方法来显式重置ShardingSphere-Proxy数据源的方式,我们可以实现ShardingSphere-Proxy与Aurora结合的故障切换能力。

5. 结语

本篇文章通过数据库中间件ShardingSphere-Proxy拓展了Aurora的分库分表能力和读写分离的能力。

ShardingSphere-Proxy内置连接池,对MySQL语法支持较强,在分库分表和读写分离上表现出色。它对多表join上,可以支持分片规则相同的表的join,以及小表和大表的join,基本能满足OLTP场景的需求。在动态分片上,ShardingSphere-Proxy提供在线更改分片规则的能力,但需要用户在底层Aurora集群手动操作子表创建及数据迁移,需要一定工作量。故障切换维度,ShardingSphere-Proxy与Aurora的融合不是很好,但是可以通过本文提供的Aurora故障切换Event调用Lambda方法来显式重置数据源的方式,实现ShardingSphere-Proxy对Aurora集群故障切换对感知。

总体而言,ShardingSphere-Proxy这个中间件产品还是能与Aurora集群进行一个良好匹配,进一步提升Aurora集群的读写能力的。它有良好的文档,也是比较受关注的开源产品,建议读者在考虑Aurora分库分表实现时,评估下这个产品。后续我们会继续推出对其他中间件以及JDBC方面的拓展和研究系列博客。

本篇作者

马丽丽

亚马逊云科技数据库解决方案架构师,十余年数据库行业经验,先后涉猎NoSQL 数据库Hadoop/Hive、企业级数据库DB2、分布式数仓Greenplum/Apache HAWQ以及亚马逊云原生数据库的开发和研究。