Docker基础知识 (14) - 使用 Docker 部署 MariaDB 集群 (二) | 基于 MyCat 的读写分离和负载均衡
读写分离适用的场景:读操作的频率远高于写操作的频率,写操作的耗时长于读操作的耗时,即长耗时低频率的写操作降低了整个数据库的高并发读写能力,同时读操作对数据实时性要求不高,允许一定时间的延时。
MySQL 读写分离基本原理是让 master 数据库处理写操作,slave 数据库处理读操作,master 将写操作的变更同步到各个 slave 节点。
MySQL 读写分离能提高系统性能的原因:
(1) 物理服务器增加,机器处理能力提升,拿硬件换性能;
(2) 主从只负责各自的读和写,极大程度缓解 X 锁和 S 锁争用;
(3) slave 可以配置 MyIASM 引擎,提升查询性能以及节约系统开销;
(4) master 直接写是并发的,slave 通过主库发送来的 binlog 恢复数据是异步;
(5) slave 可以单独设置一些参数来提升其读的性能;
(6) 增加冗余,提高可用性;
实现读写分离的方案:
1) 基于 MySQL Proxy 代理的方式
MySQL 的代理最常见的是 mysql-proxy、cobar、mycat、Atlas 等。这种方式对于应用来说,MySQL Proxy 是完全透明的,应用则只需要连接到 MySQL Proxy 的监听端口即可。
当然,这样 Proxy 机器可能会单点失效,但可以使用多个 Proxy 机器做为冗余,在应用服务器的连接池配置中配置到多个 Proxy 的连接参数即可。
(1) mysql-proxy 是一个轻量的中间代理,是官方提供的 MySQL 中间件产品可以实现负载平衡,读写分离,failover 等,依靠内部一个 lua 脚本实现读写语句的判断。项目地址:https://github.com/mysql/mysql-proxy ,该项目已经六七年没有维护了,官方也不建议应用于生成环境。
(2) cobar 是阿里提供的一个中间件,已经停止更新。项目地址:https://github.com/alibaba/cobar。
(3) mycat 的前身就是 cobar,活跃度比较高,完全使用 java 语言开发。 项目地址:https://github.com/MyCATApache/Mycat-Server。
(4) moeba(变形虫)是阿里工程师陈思儒基于 java 开发的一款数据库读写分离的项目(读写分离只是它的一个小功能),与 MySQL 官方的 MySQL Proxy 相比,作者强调的是 amoeba 配置的方便(基于XML的配置文件,用 SQLJEP 语法书写规则,比基于 lua 脚本的MySQL Proxy 简单)。更多详细介绍请参考:https://www.biaodianfu.com/amoeba.html , 下载地址:https://sourceforge.net/projects/amoeba/ 。
(5) Atlas 是奇虎 360 的一个开源中间代理,是在 MySQL 官方 mysql-proxy 0.8.2 的基础上进行了优化,增加一些新的功能特性。 项目地址: https://github.com/Qihoo360/Atlas 。
2) 基于应用内路由的方式
基于 Spring 的 AOP 实现: 用AOP 来拦截 Spring 项目的 dao 层方法,根据方法名称就可以判断要执行的 SQL 类型(即是 read 还是 write 类型),进而动态切换主从数据源。
参考项目:https://gitee.com/baomidou/dynamic-datasource-spring-boot-starter
3) 基于 mysql-connector-java 的 jdbc 驱动方式
使用 MySQL 驱动 Connector/J 的可以实现读写分离。即在 jdbc 的 url 中配置为如下的形示:
jdbc:mysql:replication://master,slave1,slave2,slave3/test
Java 程序通过在连接 MySQL 的 jdbc 中配置主库与从库等地址,jdbc 会自动将读请求发送给从库,将写请求发送给主库,此外,mysql 的 jdbc 驱动还能够实现多个从库的负载均衡。
关于 jdbc 文档地址:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html
关于读写分离文档地址:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-source-replica-replication-connection.html
4) 基于 sharding-jdbc 的方式
sharding-sphere 是强大的读写分离、分表分库中间件,sharding-jdbc 是 sharding-sphere 的核心模块,sharding-jdbc 可以与 Springboot 集成。官方网址:https://shardingsphere.apache.org/。
以上四种方案各有优缺点,基于 MySQL Proxy 代理的方式对于应用来说相对简单,但是在项目稳定性、事务支持性等方面还存在问题;而基于应用内路由的方式固然灵活度比较高,但是也增加了应用逻辑的复杂度;基于 mysql-connector-java 的 jdbc 驱动和 sharding-jdbc 的方式在使用上相对简单,但限制了需要使用 java 开发。
本文部署 MyCat + MariaDB 集群来演示如何实现读写分离和负载均衡。
1. 部署环境
IP 地址(本地测试环境):192.168.0.10
操作系统:Linux CentOS 7.9
Docker 版本:20.10.7
Docker Compose 版本:2.6.1
MyCat 版本:1.6.7.5
MariaDB 集群目录:/home/docker/mysql_cluster
MyCat 目录:/home/docker/mysql_cluster/mycat
MyCat GitHub:https://github.com/MyCATApache/Mycat-Server
2. 构建 mycat 镜像
1) 下载 Mycat
$ cd /home/docker/mysql_cluster/mycat
$ wget https://github.com/MyCATApache/Mycat-Server/releases/download/Mycat-server-1675-release/Mycat-server-1.6.7.5-release-20200422133810-linux.tar.gz
$ tar -vxzf Mycat-server-1.6.7.5-release-20200422133810-linux.tar.gz
$ cp -R mycat/conf/ ./
2) 创建 Dockerfile
$ cd /home/docker/mysql_cluster/mycat
$ vim Dockerfile
FROM openjdk:8 ADD mycat/ /usr/local/mycat/ VOLUME /usr/local/mycat/conf ENV MYCAT_HOME=/usr/local/mycat EXPOSE 8066 1984 CMD ["/usr/local/mycat/bin/mycat", "console"]
注:这里使用 openjdk:8 基础镜像来演示,建议实际应用中使用较新的 Java 版本,可以访问 https://hub.docker.com/_/openjdk/tags 查询。
3) 构建
$ cd /home/docker/mysql_cluster/mycat
$ docker build -t mycat-1.6.7:2022-09-28 .
Sending build context to Docker daemon 49.36MB Step 1/6 : FROM openjdk:8 8: Pulling from library/openjdk 001c52e26ad5: Pull complete d9d4b9b6e964: Pull complete 2068746827ec: Pull complete 9daef329d350: Pull complete d85151f15b66: Pull complete 52a8c426d30b: Pull complete 8754a66e0050: Pull complete Digest: sha256:86e863cc57215cfb181bd319736d0baf625fe8f150577f9eb58bd937f5452cb8 Status: Downloaded newer image for openjdk:8 ---> b273004037cc Step 2/6 : ADD mycat/ /usr/local/mycat/ ---> 1fcae8e968b0 Step 3/6 : VOLUME /usr/local/mycat/conf ---> Running in 6af0a0e6d779 Removing intermediate container 6af0a0e6d779 ---> 43cb021696d6 Step 4/6 : ENV MYCAT_HOME=/usr/local/mycat ---> Running in f78ac994990f Removing intermediate container f78ac994990f ---> 45509b7f7fb6 Step 5/6 : EXPOSE 8066 1984 ---> Running in f24b257ccee6 Removing intermediate container f24b257ccee6 ---> c878bd1130c0 Step 6/6 : CMD ["/usr/local/mycat/bin/mycat", "console"] ---> Running in e373990ba208 Removing intermediate container e373990ba208 ---> c15cdad98dd6 Successfully built c15cdad98dd6 Successfully tagged mycat-1.6.7:2022-09-28
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE mycat-1.6.7 2022-09-28 c4ee675557be 30 seconds ago 552MB openjdk 8 b273004037cc 8 weeks ago 526MB ...
3. 创建 docker-compose.yml
$ cd /home/docker/mysql_cluster/mycat
$ vim docker-compose.yml
version: "3" services: mariadb_master: image: mariadb:10.4 container_name: mariadb_master-10.4 ports: - "3306:3306" restart: always environment: - MARIADB_ROOT_PASSWORD=123456 volumes: - /home/docker/mysql_cluster/master/conf:/etc/mysql/conf.d - /home/docker/mysql_cluster/master/data:/var/lib/mysql - /home/docker/mysql_cluster/master/log:/var/log/mysql mariadb_node_1: image: mariadb:10.4 container_name: mariadb_node_1-10.4 ports: - "3307:3307" restart: always environment: - MARIADB_ROOT_PASSWORD=123456 volumes: - /home/docker/mysql_cluster/node_1/conf:/etc/mysql/conf.d - /home/docker/mysql_cluster/node_1/data:/var/lib/mysql - /home/docker/mysql_cluster/node_1/log:/var/log/mysql mariadb_node_2: image: mariadb:10.4 container_name: mariadb_node_2-10.4 ports: - "3308:3308" restart: always environment: - MARIADB_ROOT_PASSWORD=123456 volumes: - /home/docker/mysql_cluster/node_2/conf:/etc/mysql/conf.d - /home/docker/mysql_cluster/node_2/data:/var/lib/mysql - /home/docker/mysql_cluster/node_2/log:/var/log/mysql mycat: image: mycat-1.6.7:2022-09-28 container_name: mycat ports: - "8066:8066" - "1984:1984" restart: always volumes: - /home/docker/mysql_cluster/mycat/conf:/usr/local/mycat/conf - /home/docker/mysql_cluster/mycat/logs:/usr/local/mycat/logs
4. MariaDB 配置文件
1) 创建 master 的 my_mariadb.cnf 文件
在 /home/docker/mysql_cluster/master/conf 目录下,创建 my_mariadb.cnf 文件,内容如下:
[mysqld] server-id=1 port=3306 #basedir=/usr/local/mysql #tmpdir=/tmp datadir=/var/lib/mysql # 查询日志,默认在 /var/lib/mysql 目录下 #general_log=1 #general_log_file=mysql_general-1.log # 二进制日志,默认在 /var/lib/mysql 目录下 log_bin=mysql_binlog-1 # 慢查询日志,默认在 /var/log/mysql 目录下 #slow_query_log=1 #long_query_time=1 #slow_query_log_file=mysql_slow_query-1.log # 错误日志,指定到 /var/log/mysql 目录 log_error=/var/log/mysql/mysql_err-1.log
注:MariaDB (MySQL) 的默认配置文件是 /etc/mysql/my.cnf 文件。如果想要自定义配置,在 /etc/mysql/conf.d 目录中创建 *.cnf 文件。新建的文件可以任意起名,只要保证后缀名是 cnf 即可。新建的文件中的配置项可以覆盖 /etc/mysql/my.cnf 中的配置项。
2) 创建 node_1 的 my_mariadb.cnf 文件
在 /home/docker/mysql_cluster/node_1/conf 目录下,创建 my_mariadb.cnf 文件,内容如下:
[mysqld] server-id=2 port=3307 #basedir=/usr/local/mysql #tmpdir=/tmp datadir=/var/lib/mysql # 查询日志,默认在 /var/lib/mysql 目录下 #general_log=1 #general_log_file=mysql_general-2.log # 二进制日志,默认在 /var/lib/mysql 目录下 #log_bin=mysql_binlog-2 # 慢查询日志,默认在 /var/log/mysql 目录下 #slow_query_log=1 #long_query_time=1 #slow_query_log_file=mysql_slow_query-2.log # 错误日志,指定到 /var/log/mysql 目录 log_error=/var/log/mysql/mysql_err-2.log # relay-log relay-log=mysql-relaylog-2
3) 创建 node_2 的 my_mariadb.cnf 文件
在 /home/docker/mysql_cluster/node_2/conf 目录下,创建 my_mariadb.cnf 文件,内容如下:
[mysqld] server-id=3 port=3308 #basedir=/usr/local/mysql #tmpdir=/tmp datadir=/var/lib/mysql # 查询日志,默认在 /var/lib/mysql 目录下 #general_log=1 #general_log_file=mysql_general-3.log # 二进制日志,默认在 /var/lib/mysql 目录下 #log_bin=mysql_binlog-3 # 慢查询日志,默认在 /var/log/mysql 目录下 #slow_query_log=1 #long_query_time=1 #slow_query_log_file=mysql_slow_query-3.log # 错误日志,指定到 /var/log/mysql 目录 log_error=/var/log/mysql/mysql_err-3.log # relay-log relay-log=mysql-relaylog-3
5. 启动
$ cd /home/docker/mysql_cluster/mycat # 进入 docker-compose.yml 所在目录
$ docker-compose up -d # 在后台运行
[+] Running 5/5 ⠿ Network mycat_default Created 0.0s ⠿ Container mariadb_node_1-10.4 Started 1.0s ⠿ Container mycat Started 1.0s ⠿ Container mariadb_node_2-10.4 Started 0.7s ⠿ Container mariadb_master-10.4 Started 0.5s
注:本地 docker 没有所需要的镜像时,会自动下载所需的镜像。遇到无法自动下载的情况,可以使用 docker pull 命令下载所需的镜像,再运行 docker-compose up 命令。
$ docker images # 查看镜像
REPOSITORY TAG IMAGE ID CREATED SIZE mycat-1.6.7 2022-09-28 c4ee675557be 14 minutes ago 552MB mariadb 10.4 801fdf0164b2 3 weeks ago 404MB openjdk 8 b273004037cc 8 weeks ago 526MB
$ docker-compose ps # 类似 docker ps 的作用
NAME COMMAND SERVICE STATUS PORTS mariadb_master-10.4 ... mariadb_master running 0.0.0.0:3306->3306/tcp,... mariadb_node_1-10.4 mariadb_node_1 running 0.0.0.0:3307->3307/tcp,... mariadb_node_2-10.4 mariadb_node_2 running 0.0.0.0:3308->3308/tcp,... mycat mycat running 0.0.0.0:1984->1984/tcp,...
容器内的程序要在挂载的 log 目录下创建 log 文件,需要确保 root/root 以外的用户也有 log 目录的写权限,修改 log 目录的权限,命令如下:
$ chmod a+w /home/docker/mysql_cluster/master/log /home/docker/mysql_cluster/node_1/log /home/docker/mysql_cluster/node_2/log
修改后需要重启容器,命令如下:
$ docker restart mariadb_master-10.4 mariadb_node_1-10.4 mariadb_node_2-10.4
注:这里把 MariaDB 集群配置成主从复制(一主二从),如何配置主从复制,请参考 “Docker基础知识 (13) - 部署 MariaDB 集群 (一) | 主从复制” 的 “5. 配置主从复制” 部分。
6. 测试
1) 在 master 上创建创建数据库和表
$ docker exec -it mariadb_master-10.4 /bin/bash # 进入 master 容器
# 进入 MySQL 命令行控制台 root@986cfd3cdf63:/# mysql -u root -p Enter password: Welcome to the MariaDB monitor. Commands end with ; or \g. MariaDB [(none)]> CREATE DATABASE IF NOT EXISTS test_mycat; Query OK, 1 row affected (0.000 sec) MariaDB [(none)]> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | test_mycat | +--------------------+ 4 rows in set (0.001 sec) MariaDB [(none)]> use test_mycat; Database changed MariaDB [test_mycat]> CREATE TABLE `test_table` (`id` int(11) NOT NULL, `name` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8; Query OK, 0 rows affected (0.008 sec)
注:master 上创建的库和表会同步到 node_1 和 node_2 上。
2) 修改 mycat 配置文件
(1) 主要配置文件
server.xml 是 mycat 服务器参数调整和用户授权的配置文件;
schema.xml 是逻辑库定义和表以及分片定义的配置文件;
rule.xml 是分片规则的配置文件;
(2) 修改 schema.xml
$ cd /home/docker/mysql_cluster/mycat/conf
$ vim schema.xml
<?xml version="1.0"?> <!DOCTYPE mycat:schema SYSTEM "schema.dtd"> <mycat:schema xmlns:mycat="http://io.mycat/"> <schema name="test_mycat" checkSQLschema="false" sqlMaxLimit="100" dataNode="test_mycat" /> <dataNode name="test_mycat" dataHost="localhost1" database="test_mycat" /> <dataHost name="localhost1" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100"> <heartbeat>select 1</heartbeat> <!-- 一主二从,读写分离 --> <writeHost host="hostM1" url="192.168.0.10:3306" user="root" password="123456"> <readHost host="hostS1" url="192.168.0.10:3307" user="root" password="123456" /> <readHost host="hostS2" url="192.168.0.10:3308" user="root" password="123456" /> </writeHost> <!-- <writeHost host="hostM2" url="localhost:3316" user="root" password="123456"/> --> </dataHost> </mycat:schema>
(3) 修改 server.xml
<?xml version="1.0" encoding="UTF-8"?> <!-- - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --> <!DOCTYPE mycat:server SYSTEM "server.dtd"> <mycat:server xmlns:mycat="http://io.mycat/"> ... <user name="root" defaultAccount="true"> <property name="password">123456</property> <property name="schemas">test_mycat</property> <property name="defaultSchema">test_mycat</property> <!--No MyCAT Database selected 错误前会尝试使用该schema作为schema,不设置则为null,报错 --> <!-- 表级 DML 权限设置 --> <!-- <privileges check="false"> <schema name="TESTDB" dml="0110" > <table name="tb01" dml="0000"></table> <table name="tb02" dml="1111"></table> </schema> </privileges> --> </user> <user name="user"> <property name="password">user</property> <property name="schemas">test_mycat</property> <property name="readOnly">true</property> <property name="defaultSchema">test_mycat</property> </user> </mycat:server>
(4) 重新启动 mycat
$ docker restart mycat
mycat
3) 测试 mycat
Windows 测试终端的环境配置
OS:Windows 10 Home 21H2
MySQL Client:mysql Ver 15.1 Distrib 10.4.21-MariaDB, for Win64 (AMD64)
IP:192.168.0.2
Step 1:访问 mycat
C:>mysql -h 192.168.0.10 -P 8066 -u root -p Enter password: ****** Welcome to the MariaDB monitor. Commands end with ; or \g. ... MySQL [(none)]> use test_mycat; Database changed MySQL [(none)]> INSERT INTO test_table (id, name) VALUES ('1', 'mycat master'); Query OK, 1 row affected (0.022 sec) MySQL [test_mycat]> select * from test_table; +----+--------------+ | id | name | +----+--------------+ | 1 | mycat master | +----+--------------+ 1 row in set (0.003 sec)
Step 2:直接访问 node_1
C:>mysql -h 192.168.0.10 -P 3307 -u root -p Enter password: ****** Welcome to the MariaDB monitor. Commands end with ; or \g. ... MySQL [(none)]> use test_mycat; Database changed MariaDB [test_mycat]> select * from test_table; +----+--------------+ | id | name | +----+--------------+ | 1 | mycat master | +----+--------------+ 1 row in set (0.001 sec) MariaDB [test_mycat]> update test_table set name='mycat node_1' where id='1';
Step 3:直接访问 node_2
C:>mysql -h 192.168.0.10 -P 3308 -u root -p Enter password: ****** Welcome to the MariaDB monitor. Commands end with ; or \g. ... MySQL [(none)]> use test_mycat; Database changed MariaDB [test_mycat]> select * from test_table; +----+--------------+ | id | name | +----+--------------+ | 1 | mycat master | +----+--------------+ 1 row in set (0.001 sec) MariaDB [test_mycat]> update test_table set name='mycat node_2' where id='1';
Step 4:再访问 mycat
C:>mysql -h 192.168.0.10 -P 8066 -u root -p Enter password: ****** Welcome to the MariaDB monitor. Commands end with ; or \g. ... MySQL [(none)]> use test_mycat; Database changed MySQL [test_mycat]> select * from test_table; +----+--------------+ | id | name | +----+--------------+ | 1 | mycat node_1 | +----+--------------+ 1 row in set (0.002 sec) MySQL [test_mycat]> select * from test_table; +----+--------------+ | id | name | +----+--------------+ | 1 | mycat node_2 | +----+--------------+ 1 row in set (0.002 sec) MySQL [test_mycat]> select * from test_table; +----+--------------+ | id | name | +----+--------------+ | 1 | mycat node_1 | +----+--------------+ 1 row in set (0.001 sec) ...
注:很显然,从 mycat 的 8066 端口读不到 master 的数据,master 只负责写数据。
Step 5:直接访问 master
C:>mysql -h 192.168.0.10 -P 3306 -u root -p Enter password: ****** Welcome to the MariaDB monitor. Commands end with ; or \g. ... MySQL [(none)]> use test_mycat; Database changed MySQL [test_mycat]> select * from test_table; +----+--------------+ | id | name | +----+--------------+ | 1 | mycat master | +----+--------------+ 1 row in set (0.001 sec)
测试结果:
(1) 通过 mycat (端口:8066)写数据,都写到了 master 的 test_mycat 数据库;
(2) 通过 mycat 读数据,数据交替来自 node_1 和 node_2 的 test_mycat 数据库;
(3) 根据 (1)、(2),可以判断 mycat 实现了读写分离,并实现了读数据时负载均衡。