这是运维范畴的工作,但是后端开发人员也必须懂。
注:本文使用的是最新版 docker,docker 自带 compose 命令。非最新版玩家在运行时使用
docker-compose
命令就行。
配置
文件结构
为了方便和容易管理,我的目录结构是这样的,仅供参考:
master-slave/
├── docker-compose.yml
├── master
│ ├── data
│ ├── log
| │ └── error.log
│ └── my.cnf
├── slave1
│ ├── data
│ ├── log
| │ └── error.log
│ └── my.cnf
└── slave2
├── data
├── log
│ └── error.log
└── my.cnf
复制代码
根据目录结构可以看出这里采用一主双从的结构。data
目录用于存储 MySQL 数据,log/error.log
用于保存 MySQL 的异常输出,my.cnf
是 MySQL 配置文件。
Compose 文件
注意点:
- 必须定义网络,否则各容器无法通讯,不然从库怎么找主库?
- 在内部网络中,容器之间相互访问是通过容器内部的端口,不是主机端口。
- 数据卷
error.log
是给 MySQL 的log-error
配置项使用的,必须先创建好,否则会报:File Not Found
错误而无法启动容器。 - 数据卷
error.log
由于是在宿主机创建的,容器内部的 mysql 用户无权访问会报Permission Deny
错误而无法启动容器,因此需要配置entrypoint
在容器启动时执行命令进行授权。
version: '3'
# 建立一个网络,否则机器之间无法进行连接
networks:
mysql-master-slave-network:
driver: bridge
ipam:
config:
- subnet: 172.25.0.0/24
services:
mysql-master:
image: mysql
container_name: mysql-master
ports:
- 3307:3306
volumes:
- "./master/data:/var/lib/mysql"
- "./master/log/error.log:/var/log/mysql/error.log"
- "./master/my.cnf:/etc/mysql/my.cnf"
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: test
entrypoint: bash -c "chown -R mysql:mysql /var/log/mysql && exec /entrypoint.sh mysqld"
restart: unless-stopped
networks:
mysql-master-slave-network:
ipv4_address: 172.25.0.101
mysql-slave-1:
image: mysql
container_name: mysql-slave-1
ports:
- 3308:3306
volumes:
- "./slave1/data:/var/lib/mysql"
- "./slave1/log/error.log:/var/log/mysql/error.log"
- "./slave1/my.cnf:/etc/mysql/my.cnf"
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: test
entrypoint: bash -c "chown -R mysql:mysql /var/log/mysql && exec /entrypoint.sh mysqld"
restart: unless-stopped
networks:
mysql-master-slave-network:
ipv4_address: 172.25.0.102
mysql-slave-2:
image: mysql
container_name: mysql-slave-2
ports:
- 3309:3306
volumes:
- "./slave2/data:/var/lib/mysql"
- "./slave2/log/error.log:/var/log/mysql/error.log"
- "./slave2/my.cnf:/etc/mysql/my.cnf"
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: test
entrypoint: bash -c "chown -R mysql:mysql /var/log/mysql && exec /entrypoint.sh mysqld"
restart: unless-stopped
networks:
mysql-master-slave-network:
ipv4_address: 172.25.0.103
复制代码
my.cnf 配置
-
Master
[mysqld] pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql log-error = /var/log/mysql/error.log bind-address = 0.0.0.0 secure-file-priv = NULL max_connections = 16384 character-set-server = utf8mb4 collation-server = utf8mb4_general_ci init_connect ='SET NAMES utf8mb4' skip-name-resolve skip-host-cache server_id = 1 # ID,不要重复 log-bin = mysql-bin # 开启 bin log binlog-do-db = test # 开启 bin log 的数据库名,如果有多个,重复设置就行 log-slave-updates # 将从服务器从主服务器收到的更新记入到从服务器自己的二进制日志文件中 sync_binlog = 1 # 控制 bin log 写入频率 auto_increment_offset = 1 # 自增 offset,主主同步时用到 auto_increment_increment = 1 # 自增间隔,同样主主同步时使用 expire_logs_days = 7 # bin 留存天数 log_bin_trust_function_creators = 1 # 复制定义的函数 # Custom config should go here !includedir /etc/mysql/conf.d/ 复制代码
-
Slave1 & Slave2,唯一区别是
server_id
,2
和3
,和 Master 不一样就行。[mysqld] pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql log-error = /var/log/mysql/error.log bind-address = 0.0.0.0 secure-file-priv = NULL max_connections = 16384 character-set-server = utf8mb4 collation-server = utf8mb4_general_ci init_connect ='SET NAMES utf8mb4' skip-name-resolve skip-host-cache server_id = log-bin = mysql-bin log-slave-updates sync_binlog = 0 innodb_flush_log_at_trx_commit = 0 replicate-do-db = test slave-net-timeout = 60 log_bin_trust_function_creators = 1 # Custom config should go here !includedir /etc/mysql/conf.d/ 复制代码
配置结束即可启动。
主从设置
-
获取 Master 状态。
登录 Master,输入
show master status\G
,File
指当前的 bin-log 文件,Position
指当前的偏移 offset,下一条命令从这里开始。因此我们需要记住这两个信息,从库依靠这两个信息开始进行复制的。mysql> show master status\G *************************** 1. row *************************** File: mysql-bin.000003 Position: 156 Binlog_Do_DB: test Binlog_Ignore_DB: Executed_Gtid_Set: 1 row in set (0.00 sec) 复制代码
-
登录从库,清空原
slave
信息。mysql> reset slave; Query OK, 0 rows affected, 1 warning (0.00 sec) 复制代码
-
从库配置
slave
信息。注意,
master_port=3306
,因为容器内部网络访问的是容器的接口,而非宿主机的接口。读取 Master 的File
和Position
信息在这里配置。我在
my.cnf
中开启了远程连接,所以这里是可以直接连的。如果不是按上方bind-address
配置来的话,未开启请开启,否则从库没法登录主库的。想要看从库机器能否连接主库,可以在从库中用mysql
客户端连接主库试试。mysql> change master to master_host='172.25.0.101',master_user='root',master_password='123456',master_port=3306,master_log_file='mysql-bin.000003',master_log_pos=156; Query OK, 0 rows affected, 8 warnings (0.05 sec) 复制代码
-
启动 slave 连接。
mysql> start slave; Query OK, 0 rows affected, 1 warning (0.01 sec) 复制代码
-
查看 slave 状态,如果
Slave_IO_Runnig
和Slave_SQL_Running
进程都为Yes
,表示都启动成功,则可以进行复制测试了。Slave_IO
进程负责主机通信,用于连接主库,并拉取主库 bin-log 到 relay-log。
Slave_SQL
进程负责执行命令,用于执行 relay-log 中的操作。如果发现其中一个不为 Yes,可以去
error.log
中查看具体信息。mysql> show slave status\G *************************** 1. row *************************** Slave_IO_State: Waiting for source to send event Master_Host: 172.25.0.101 Master_User: root Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000003 Read_Master_Log_Pos: 156 Relay_Log_File: 34d0770589c6-relay-bin.000002 Relay_Log_Pos: 324 Relay_Master_Log_File: mysql-bin.000003 Slave_IO_Running: Yes Slave_SQL_Running: Yes Replicate_Do_DB: test Replicate_Ignore_DB: ... 1 row in set, 1 warning (0.01 sec) 复制代码
-
复制测试
在主机的
test
库中新建一个表,从库就会新建一个表。在表中增、删、改数据,从库就会增、删、改数据。
禁止从库更新
在从库上执行 set global read_only=1;
命令,可以限制普通用户的写操作,包括增、删、改、新建表、修改表、删除表。
mysql> set global read_only=1;
Query OK, 0 rows affected (0.00 sec)
复制代码
设置失去效果?
但是如果你照着上文的操作来,会发现这里依旧可以执行成功(WTF?):
mysql> drop table t1;
Query OK, 0 rows affected (0.07 sec)
复制代码
这是因为,这个操作仅仅限制了普通用户,无法限制超级用户,root
是可以为所欲为的。如果要限制 root
,就得这么玩:
mysql> flush tables with read lock;
Query OK, 0 rows affected (0.02 sec)
mysql> set global read_only=1;
Query OK, 0 rows affected (0.00 sec)
mysql> drop table t1;
ERROR 1223 (HY000): Can't execute the query because you have a conflicting read lock
复制代码
但是这样玩的话,从库就无法复制主库了,因为这样也锁死了 Slave_SQL
进程无法执行写操作。所以,超级权限用户是不能动的。使用 unlock tables;
可以解开 flush tables with read lock;
操作,此时从库就可以复制主库操作了。
正确的复制
正确的做法是,复制时所使用的用户是 Master 上新建的普通用户即可,这样 set global read_only=1;
就会生效。同时保证从库 root
这些超级权限账号只能本地登录,不允许外界登录和进行写操作即可。
那为什么上面不直接按正确的来?
如果你那么喜欢重复,就让你体验一下
要改用户,主库三个连招:
CREATE USER 'slave'@'%' IDENTIFIED WITH sha256_password BY 'slave123';
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%';
FLUSH privileges;
show master status\G
复制代码
从库:
stop slave;
reset slave;
change master to master_host='172.25.0.101',master_user='slave',master_password='slave123',master_port=3306,master_log_file='mysql-bin.******',master_log_pos=*****;
start master;
复制代码
坑很多,有的坑是自己的逻辑错误,有的坑是对 MySQL 的认识不足,踩到了肯定很烦,但是有调查才有发言权,经历过才有真功夫。