配置ProxySQL实现透明读写分离

在业务应用不方便进行读写分离调用的时候,可以考虑使用ProxySQL实现全局的透明读写分离。本文描述如何配置一个单机非集群版本的ProxySQL的MySQL 代理。后端数据库为AWS Aurora MySQL数据库。

一、安装和常用命令

1、安装和部署

首先准备一个Amazon Linux 2系统,生产环境上规格可以选择 c5.large 等基础规格。测试环境可以使用 t3a.medium 等规格。

以root身份执行如下命令:

yum update -y
reboot

执行reboot的原因是确保内核等均为最新。继续执行以下命令。

yum install perl-DBD-mysql gnutls
wget https://s3.cn-north-1.amazonaws.com.cn/myworkshop-lxy/proxysql/proxysql2-2.0.12-1.1.el7.x86_64.rpm
rpm -ivh proxysql2-2.0.12-1.1.el7.x86_64.rpm
service proxysql start

安装完成。

2、测试管理端口连接

执行如下命令:

mysql -u admin -padmin -h 127.0.0.1 -P6032 --prompt='Admin> '

如果可以返回以下信息,表示安装完成。

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 5.5.30 (ProxySQL Admin Module)

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

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

Admin>

以上看到的版本5.5.30并不是后台真实的MySQL实例的版本,这只是ProxySQL本身模拟的版本,因此是正常的。数据库解析SQL主要是看后端的MySQL版本是5.6、5.7还是8.0,与ProxySQL的显示的没有直接关系。

3、查询当前配置

查询当前server、查询转发规则、查询当前用户:

SELECT * FROM mysql_servers;
SELECT * from mysql_query_rules;
SELECT * FROM mysql_users;

以上查询结果应该均为空,表示是一个全新装好的服务器。

二、配置ProxySQL

1、设置默认管理员账号和对外服务端口

ProxySQL默认有两个端口,管理端口在6032,服务端口在6033。

ProxySQL的管理员账户在6032端口,默认是用户名是admin,密码也是admin。系统设计上,不允许管理员用户与MySQL普通用户重名。这就带来了一个使用场景的问题,即应用程序和后端MySQL经常使用admin作为用户名,这样将导致冲突无法登录。由此,我们在这里另外创建一个后续要使用的管理员账号叫做proxyadmin。然后我们将对外服务端口从6033改为程序代码常用的3306。本步骤非常重要,配置错误将导致无法登录ProxySQL的管理端口。

执行如下命令以默认账号admin/admin连接到ProxySQL的6032管理端口。

mysql -u admin -padmin -h 127.0.0.1 -P6032 --prompt='Admin> '

执行如下命令:

SET admin-admin_credentials='proxyadmin:proxyadmin';
load admin variables to runtime;
save admin variables to disk;
SET mysql-interfaces='0.0.0.0:3306';
SAVE MYSQL VARIABLES TO DISK;
PROXYSQL RESTART;

执行完毕后,服务会中断,输入quit,退出到Linux Console,然后重新连接到6032的管理端口。重新连接时候,就可以使用新的proxyadmin用户名和密码登录了。命令如下:

mysql -u proxyadmin -pproxyadmin -h 127.0.0.1 -P6032 --prompt='Admin>

登录成功就表示配置成功。此时服务端口3306还不能登录,有待后续配置。

2、新增后端节点

假设ProxySQL的后台是AWS Aurora for MySQL数据库,有一个Writer节点(主节点)和一个Reader节点(只读节点),那么执行如下命令,新增两个节点,且分别配置两个节点的组ID为1和2。其中hostgroup_id=1是可写入的主节点。

INSERT INTO mysql_servers(hostgroup_id,hostname,port) VALUES (1,'proxy-test-cluster.cluster-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn',3306);
INSERT INTO mysql_servers(hostgroup_id,hostname,port) VALUES (2,'proxy-test-cluster.cluster-ro-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn',3306);
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;

配置成功后可运行如下命令检查配置正确:

SELECT * FROM mysql_servers;

正常的话应返回类似如下的信息:

Admin> SELECT * FROM mysql_servers;
+--------------+----------------------------------------------------------------------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+
| hostgroup_id | hostname                                                                   | port | gtid_port | status | weight | compression | max_connections | max_replication_lag | use_ssl | max_latency_ms | comment |
+--------------+----------------------------------------------------------------------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+
| 1            | proxy-test-cluster.cluster-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn    | 3306 | 0         | ONLINE | 1      | 0           | 1000            | 0                   | 0       | 0              |         |
| 2            | proxy-test-cluster.cluster-ro-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn | 3306 | 0         | ONLINE | 1      | 0           | 1000            | 0                   | 0       | 0              |         |
+--------------+----------------------------------------------------------------------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+
2 rows in set (0.00 sec)

Admin>

3、新增流量规则

首先定义默认节点,如果规则没有显式声明,则所有流量送往主节点。接下来定义规则,将所有查询语句送往只读节点,其中Select for update特殊对待还送往主节点。执行如下命令:

UPDATE mysql_users SET default_hostgroup=1;
INSERT INTO mysql_query_rules (rule_id,active,match_digest,destination_hostgroup,apply)
VALUES
(1,1,'^SELECT.*FOR UPDATE$',1,1),
(2,1,'^SELECT',2,1);
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;

配置成功后可运行如下命令检查配置正确:

SELECT * from mysql_query_rules;

正常的话应返回类似如下的信息:

Admin> SELECT * from mysql_query_rules;
+---------+--------+----------+------------+--------+-------------+------------+------------+--------+----------------------+---------------+----------------------+--------------+---------+-----------------+-----------------------+-----------+--------------------+---------------+-----------+---------+---------+-------+-------------------+----------------+------------------+-----------+--------+-------------+-----------+---------------------+-----+-------+---------+
| rule_id | active | username | schemaname | flagIN | client_addr | proxy_addr | proxy_port | digest | match_digest         | match_pattern | negate_match_pattern | re_modifiers | flagOUT | replace_pattern | destination_hostgroup | cache_ttl | cache_empty_result | cache_timeout | reconnect | timeout | retries | delay | next_query_flagIN | mirror_flagOUT | mirror_hostgroup | error_msg | OK_msg | sticky_conn | multiplex | gtid_from_hostgroup | log | apply | comment |
+---------+--------+----------+------------+--------+-------------+------------+------------+--------+----------------------+---------------+----------------------+--------------+---------+-----------------+-----------------------+-----------+--------------------+---------------+-----------+---------+---------+-------+-------------------+----------------+------------------+-----------+--------+-------------+-----------+---------------------+-----+-------+---------+
| 1       | 1      | NULL     | NULL       | 0      | NULL        | NULL       | NULL       | NULL   | ^SELECT.*FOR UPDATE$ | NULL          | 0                    | CASELESS     | NULL    | NULL            | 1                     | NULL      | NULL               | NULL          | NULL      | NULL    | NULL    | NULL  | NULL              | NULL           | NULL             | NULL      | NULL   | NULL        | NULL      | NULL                | NULL | 1     | NULL    |
| 2       | 1      | NULL     | NULL       | 0      | NULL        | NULL       | NULL       | NULL   | ^SELECT              | NULL          | 0                    | CASELESS     | NULL    | NULL            | 2                     | NULL      | NULL               | NULL          | NULL      | NULL    | NULL    | NULL  | NULL              | NULL           | NULL             | NULL      | NULL   | NULL        | NULL      | NULL                | NULL | 1     | NULL    |
+---------+--------+----------+------------+--------+-------------+------------+------------+--------+----------------------+---------------+----------------------+--------------+---------+-----------------+-----------------------+-----------+--------------------+---------------+-----------+---------+---------+-------+-------------------+----------------+------------------+-----------+--------+-------------+-----------+---------------------+-----+-------+---------+
2 rows in set (0.00 sec)

Admin>

4、设置用户

前文已经将服务端口从ProxySQL安装好之后默认的6033修改为3306了。但此时还不能使用,需要配置账号。目前版本的ProxySQL不支持前后端分离账号,ProxySQL所有的认证体系都是同一套。

这意味着:

  • ProxySQL登录后端Aurora MySQL的身份验证
  • 用户应用程序登录ProxySQL 3306端口发起访问的身份验证

必须是相同的用户名和密码。这也是当前ProxySQL的一个小小的局限。由此,我们继续配置。

执行如下命令:

INSERT INTO mysql_users(username,password,default_hostgroup) VALUES ('admin','wp123456',1);
LOAD MYSQL USERS TO RUNTIME;
SAVE MYSQL USERS TO DISK;

这里就添加了一个用户名为admin和密码是wp123456的用户。配置完毕后,执行如下命令,验证是否正常:

SELECT * FROM mysql_users;

显示结果类似如下则表示正常:

Admin> SELECT * FROM mysql_users;
+----------+----------+--------+---------+-------------------+----------------+---------------+------------------------+--------------+---------+----------+-----------------+---------+
| username | password | active | use_ssl | default_hostgroup | default_schema | schema_locked | transaction_persistent | fast_forward | backend | frontend | max_connections | comment |
+----------+----------+--------+---------+-------------------+----------------+---------------+------------------------+--------------+---------+----------+-----------------+---------+
| admin    | wp123456 | 1      | 0       | 1                 | NULL           | 0             | 1                      | 0            | 1       | 1        | 10000           |         |
+----------+----------+--------+---------+-------------------+----------------+---------------+------------------------+--------------+---------+----------+-----------------+---------+
1 row in set (0.00 sec)

Admin>

另外请注意,ProxySQL是支持使用hash后的密码而不是明文密码的,相关配置请参考ProxySQL官网的文档。

5、健康检查

本文使用AWS Aurora MySQL作为后单数据库,Aurora本身是以一个主节点一个只读节点的方式作为高可用的,且Aurora的Endpoint名称是自动做failover切换的。因此,无需使用ProxySQL自带的健康检查和切换功能。此部分配置可以跳过。

6、验证登录

现在退出ProxySQL的管理员console,使用普通的MySQL客户端连接到ProxySQL所在节点的IP的3306端口,测试下访问。命令如下。

mysql -h proxysql的服务器ip -uadmin -pwp123456

返回结果如下则表示成功。

[ec2-user@ip-172-31-11-6 ~]$ mysql -h 172.31.31.146 -uadmin -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 40
Server version: 5.5.30 (ProxySQL)

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;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)

MySQL [(none)]>

则表示连接成功且可以正常执行查询。

三、测试

1、安装Sysbench

启动一个新的EC2应用服务器,独立于ProxySQL的EC2,作为专门的应用客户端施加压力。系统启动完成后,以root身份执行如下命令:

amazon-linux-extras install epel
yum update -y
reboot

执行reboot的原因是确保内核等均为最新。

yum install sysbench mysql -y

安装Sysbench完成。

执行性能测试前请注意,AWS上不同规格的RDS承载能力是不一样的。最大连接数可以通过登录Aurora MySQL后使用如下命令查询。

show variables where Variable_name like "%conn%";

例如本次实验使用的规格较小,返回信息如下,其中最大连接数为90。

MySQL [(none)]> show variables where Variable_name like "%conn%";
+-----------------------------------------------+-----------------+
| Variable_name                                 | Value           |
+-----------------------------------------------+-----------------+
| aurora_max_connections_limit                  | 16000           |
| character_set_connection                      | utf8            |
| collation_connection                          | utf8_general_ci |
| connect_timeout                               | 10              |
| disconnect_on_expired_password                | ON              |
| init_connect                                  |                 |
| max_connect_errors                            | 100             |
| max_connections                               | 90              |
| max_user_connections                          | 0               |
| performance_schema_session_connect_attrs_size | 0               |
+-----------------------------------------------+-----------------+
10 rows in set (0.00 sec)

MySQL [(none)]>

测试中请注意,这个参数可以调大,但是默认数值根据AWS的RDS机型规格不一样,默认值有所不同。请注意后续读写压力测试时候的最大连接数都不要超过这个界限。ProxySQL内置的默认最大连接数是2048,但是Proxy后端的Aurora MySQL最大连接只有90,因此在不修改90的情况下,在后续的测试中只使用50个并发来做压力测试。

2、准备测试数据

在压力测试节点上,运行如下命令建立测试数据,其中的host地址需要替换为ProxySQL所在的内网IP地址。EC2的内网IP地址是不会改变的,因此可以不使用域名,直接使用IP地址。

sysbench --db-driver=mysql --mysql-host=172.31.31.146 --mysql-port=3306 --mysql-user=admin --mysql-password=wp123456 --mysql-db=wordpress --table_size=10000 --tables=20 --threads=2 oltp_read_write prepare

这里需要注意的是,执行插入测试数据时候,会写入大量的数据,此时数据是写入到主节点的,并通过Aurora的复制技术传输到只读节点。由此,主节点和辅助节点都有一定的CPU占用,这是正常的。数据导入完成后,等待一段时间,让CPU和负载逐渐平缓,并记录测试开始时间,稍后将从RDS控制面板上查询数据库性能。

2、写入数据测试

在压力测试节点上,运行如下命令:

sysbench --db-driver=mysql --mysql-host=172.31.31.146 --mysql-port=3306 --mysql-user=admin --mysql-password=changeme --mysql-db=wordpress --table_size=10000 --tables=20 --events=0 --time=300 --threads=50 --report-interval=10 oltp_write_only run

运行测试300秒结束后,通过AWS RDS的控制台上的监控数据可以看出,写入节点产生了50个并发连接,且在300秒时间内对CPU造成了较大的压力,写入任务顺利的完成。如下截图。

而此时作为对比,只读节点上并未接受到数据库连接,而且CPU也仅有很小的波动用于处理复制流量,因此可以确认写入流量成功的分离不会对只读节点进行操作。如下截图。

测试完成。

3、读取数据测试

由于ProxySQL还会对节点保持长连接,因此我们需要先重启服务重制连接,以获得一个干净的环境,便于更简单的通过监控数据来判定流量是否分离成功,然后再进行下一个测试。登录到ProxySQL的服务器,执行如下命令:

mysql -u proxyadmin -pproxyadmin -h 127.0.0.1 -P6032 --prompt='Admin>
PROXYSQL RESTART;

由此将通过重启服务重置所有连接。重置完毕后,在等待一段时间(5-10分钟),等到AWS RDS控制台上的监控数据已经恢复到较低水平后,再发起下一个测试。由此,将可以更简单的快速识别流量是否是读写分类正常施压造成的。

在压力测试节点上,运行如下命令:

sysbench --db-driver=mysql --mysql-host=172.31.31.146 --mysql-port=3306 --mysql-user=admin --mysql-password=changeme --mysql-db=wordpress --table_size=10000 --tables=20 --events=0 --time=300 --threads=50 --report-interval=10 oltp_read_only run

测试过程中,只读节点的压力如下截图。

在测试过程中,通过AWS控制台,查看RDS的Aurora主实例和只读实例的监控数据,发现两个实例都发生了数据库连接,连接数均达到了50个连接被建立。如下截图。

对比这两张图各自体现的负载却不相同,主实例负载很低,只读实例负载很高。从现象上判断,大部分读取压力被发送到了只读节点,依然有少量请求被发送到了负责写入的主节点。如何排查并确认这一点呢?

登录到ProxySQL的6032端口,执行如下命令:

select * from stats_mysql_processlist limit 10;

以上这条命令,将显示当前正在通过ProxySQL查询的进程的情况,并且只显示10行。注意,这个是显示当前正在跑的SQL语句的瞬时值,每次运行这条语句时候,每一秒输出的结果都不一样,且只显示10行。如果sysbench已经执行完毕结束了,那么将不会查询到。

Admin> select * from stats_mysql_processlist limit 10;
+----------+-----------+-------+-----------+--------------+----------+-----------+---------------+------------+----------------------------------------------------------------------------+----------+---------+---------+-----------------------------------------------------------+--------------+---------------+
| ThreadID | SessionID | user  | db        | cli_host     | cli_port | hostgroup | l_srv_host    | l_srv_port | srv_host                                                                   | srv_port | command | time_ms | info                                                      | status_flags | extended_info |
+----------+-----------+-------+-----------+--------------+----------+-----------+---------------+------------+----------------------------------------------------------------------------+----------+---------+---------+-----------------------------------------------------------+--------------+---------------+
| 0        | 52        | admin | wordpress | 172.31.29.78 | 60924    | 2         | 172.31.31.146 | 43224      | proxy-test-cluster.cluster-ro-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn | 3306     | Prepare | 1       | SELECT c FROM sbtest20 WHERE id=?                         | 0            | NULL          |
| 3        | 53        | admin | wordpress | 172.31.29.78 | 60928    | 2         | 172.31.31.146 | 43214      | proxy-test-cluster.cluster-ro-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn | 3306     | Execute | 2       | SELECT c FROM sbtest16 WHERE id=?                         | 0            | NULL          |
| 1        | 54        | admin | wordpress | 172.31.29.78 | 60926    | 2         | 172.31.31.146 | 43222      | proxy-test-cluster.cluster-ro-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn | 3306     | Execute | 0       | SELECT c FROM sbtest13 WHERE id=?                         | 0            | NULL          |
| 1        | 55        | admin | wordpress | 172.31.29.78 | 60930    | 2         | 172.31.31.146 | 43244      | proxy-test-cluster.cluster-ro-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn | 3306     | Execute | 1       | SELECT c FROM sbtest2 WHERE id=?                          | 0            | NULL          |
| 1        | 56        | admin | wordpress | 172.31.29.78 | 60932    | 2         | 172.31.31.146 | 43384      | proxy-test-cluster.cluster-ro-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn | 3306     | Execute | 3       | SELECT c FROM sbtest6 WHERE id BETWEEN ? AND ? ORDER BY c | 0            | NULL          |
| 2        | 57        | admin | wordpress | 172.31.29.78 | 60934    | 2         | 172.31.31.146 | 43390      | proxy-test-cluster.cluster-ro-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn | 3306     | Execute | 0       | SELECT c FROM sbtest20 WHERE id=?                         | 0            | NULL          |
| 1        | 58        | admin | wordpress | 172.31.29.78 | 60936    | 2         | 172.31.31.146 | 43240      | proxy-test-cluster.cluster-ro-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn | 3306     | Execute | 3       | SELECT c FROM sbtest5 WHERE id=?                          | 0            | NULL          |
| 0        | 59        | admin | wordpress | 172.31.29.78 | 60938    | 2         | NULL          | NULL       | NULL                                                                       | NULL     | Sleep   | 0       | NULL                                                      | NULL         | NULL          |
| 3        | 60        | admin | wordpress | 172.31.29.78 | 60940    | 1         | 172.31.31.146 | 49642      | proxy-test-cluster.cluster-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn    | 3306     | Execute | 0       | COMMIT                                                    | 0            | NULL          |
| 2        | 61        | admin | wordpress | 172.31.29.78 | 60942    | 2         | NULL          | NULL       | NULL                                                                       | NULL     | Sleep   | 0       | NULL                                                      | NULL         | NULL          |
+----------+-----------+-------+-----------+--------------+----------+-----------+---------------+------------+----------------------------------------------------------------------------+----------+---------+---------+-----------------------------------------------------------+--------------+---------------+
10 rows in set (0.01 sec)

Admin>

保持Sysbench一直处于read_only测试的情况下,反复多次执行上一条语句查询当前正在执行的SQL,且限制输出10行。反复执行几次,就可以看到其中的输出内容包括如下一条:

| 3        | 60        | admin | wordpress | 172.31.29.78 | 60940    | 1         | 172.31.31.146 | 49642      | proxy-test-cluster.cluster-cfvonviibzua.rds.cn-north-1.amazonaws.com.cn    | 3306     | Execute | 0       | COMMIT                                                    | 0            | NULL          |

这条命令的info字段显示正在执行的命令是commit命令,就是为什么部分流量被发往主节点的原因。因为Sysbench是一个完整的测试工具,其中使用了在只读测试中,也包含了commit语句,因此根据前文Proxy配置的规则,commit语句将被发往主节点进行解析。所以,才造成了在监控数据上看,主节点也拥有了50个并发连接,但是大部分压力都是在只读节点的原因。

由于Readonly测试不会写入数据,因此在跑Sysbench脚本时候,执行commit命令其实是没有意义的。因此可以禁用掉事务,进行完全的只读查询。

4、不带事务的只读测试

由于ProxySQL还会对节点保持长连接,因此我们需要先重启服务重置连接,以获得一个干净的环境,便于更简单的通过监控数据来判定流量是否分离成功。然后再进行下一个测试。登录到ProxySQL的服务器,执行如下命令:

mysql -u proxyadmin -pproxyadmin -h 127.0.0.1 -P6032 --prompt='Admin>
PROXYSQL RESTART;

由此将通过重启服务重置所有连接。重置完毕后,在等待一段时间(5-10分钟),等到AWS RDS控制台上的监控数据已经恢复到较低水平后,再发起下一个测试。由此,将可以更简单的快速识别流量是否是读写分类正常施压造成的。

这里对上一个只读测试脚本,增加–skip_trx=on的参数。到压力服务器上,执行如下命令:

sysbench --db-driver=mysql --mysql-host=172.31.31.146 --mysql-port=3306 --mysql-user=admin --mysql-password=changeme --mysql-db=wordpress --table_size=10000 --tables=20 --events=0 --time=300 --threads=50 --report-interval=10 --skip_trx=on oltp_read_only run

测试后,再次通过RDS控制台检查主实例和只读实例的负载,可以发现负载被完全路由到了只读实例,主实例不会承担负载,也不会有建立连接。只读实例的负载如下截图。

如下截图是主实例完全是空闲状态的曲线。主实例负载如下截图。

不带事务的只读测试可以反映出ProxySQL的用法,即需要打开Auto-Commit,让每一条SQL都成为一个独立的日志。如果关闭了Auto-Commit,那么一个事务里边可能包含5条语句,不同的语句可能被ProxySQL解析和送往不同的数据库节点,这将导致严重问题。而打开了Auto-Commit后,每一条Insert和Update都是一条独立事务,都会被解析到主节点完成写入,也就保持了事务完整性。

至此测试完成。

四、参考文件

整体配置说明

读写分离规则配置