尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Docker Compose的MySQL一主二从复制配置实战与避坑指南

基于Docker Compose的MySQL一主二从复制配置实战与避坑指南 简介面向需要快速搭建MySQL主从复制环境的Docker用户这里提供了一套基于Docker Compose的一主二从配置方案解决开发与测试环境中数据库读写分离、数据冗余和负载均衡的部署难题。包内以docker-compose.yml为中心配套primary、replica1、replica2三个实例的initdb与conf.d目录包含.cnf配置文件、.sh初始化脚本和.sql建表脚本共8个文件压缩包小巧仅5KB目录层级直观适合直接套用或按需修改。配置逻辑完整覆盖主库server-id与log-bin开启、从库read-only限制、主从复制账号及权限设置并通过depends_on控制容器启动顺序同时给出健康检查与等待MySQL就绪的注意点可避免常见的从库连接失败问题。已有39人学习下载适合具备一定Docker基础、希望规避手工配置多容器复杂度的运维和开发人员参考能显著缩短一主二从环境的搭建与排查时间。 干过几年数据库维护的都知道单库跑业务就像走钢丝读请求一多主库CPU直接拉满慢查询能把整个业务拖垮。这阵子我基于docker-compose搭了一套MySQL一主二从用来做读写分离和容灾演练整个过程踩了一些坑这篇文章把配置从头到尾讲清楚。内容包含完整的docker-compose文件、主从配置文件、复制账号初始化、复制状态验证以及我在实际部署中遇到的高频问题。适合已经会MySQL基础操作、想快速复现一套主从复制环境的开发者也适合准备把数据库高可用方案落地的运维同学参考。1. 主从复制方案整体设计1.1 为什么选MySQL原生binlog复制MySQL主从复制最经典的实现方式就是基于binlog的异步复制。主库把变更写入二进制日志从库上的I/O线程拉取并写到本地relay logSQL线程再回放relay log。整个链路很简单数据最终一致性能开销也不大。有人可能会问为什么不用双主、组复制或者半同步我的判断很直接一主二从的目标是解决读压力、隔离备份不需要自动故障转移。原生异步复制足够达到目的而且排错路径清晰。用组复制虽然能提供高可用但架构复杂度明显上升测试环境没必要一上来就堆这么多东西。我在配置里明确开启了GTID模式。GTID就是每个事务的全局唯一标识相比传统基于binlog文件名偏移量的定位方式GTID让从库连接主库时的位置判断变得自动且可靠。尤其是在容器重启、从库重搭的场合GTID能省掉一大半手工对齐位置的心智负担。1.2 为什么用docker-compose而不是直接装多个MySQL实例以前我在服务器上搭主从习惯直接装好几个MySQL实例或者用mysqld_multi管理。结果有两个痛点一是配置分散每个实例的my.cnf容易改乱二是环境不可复现换台机器就得重新搞一遍。docker-compose的好处是“基础设施即代码”。一个docker-compose.yml把三个MySQL服务的镜像、端口、数据卷、网络、健康检查全部定义清楚一条命令拉起整套环境删了重建也无所谓数据放在宿主机目录不会轻易丢。当然我这里要说明白生产环境如果跑的是高并发业务我不建议用docker跑数据库毕竟容器网络和磁盘IO多多少少会有损耗。但这次搭建的目的是测试、学习和做读写分离演练docker-compose就是性价比最高的选择。真到生产环境思路一致只是部署形态需要换成物理机或云数据库。1.3 拓扑与端口规划我规划的拓扑很简单一个主库master两个从库slave1、slave2。三个容器跑在同一个自定义bridge网络内相互之间用服务名通信这样从库在容器内访问主库时永远走3306端口不需要关心宿主机端口映射。角色容器名宿主机端口容器内端口server-id主库mysql-master330633061从库1mysql-slave1330733062从库2mysql-slave2330833063数据卷我这样规划主库数据放在./data/master从库1放在./data/slave1从库2放在./data/slave2。外部挂载的好处是容器升级、重搭不会丢数据。这里有一个容易忽略的细节三个容器的server-id必须不同否则从库连接主库后会出现复制ID冲突表现就是Slave_SQL_Running异常。2. 环境准备与目录规划2.1 依赖确认在动手之前先把环境确认好省得后面各种灵异问题。需要安装的东西是Docker和Docker Compose插件。现在主流的docker compose命令是docker compose老项目里还能看到docker-compose这种写法底层逻辑一样。检查命令如下docker --version docker compose version如果没有Docker不同发行版安装方式不太一样这里不展开。我的建议是直接用官方源安装最新稳定版别用系统自带的旧版本。Compose版本太旧会导致部分语法解析失败比如healthcheck字段、restart: unless-stopped这类配置在旧版本上不识别。2.2 镜像选择镜像我用的是mysql:8.0。选8.0的核心原因是MySQL 8.0已经是当前主流版本无论是安全特性还是性能都比5.7强很多。需要注意一点8.0默认的认证插件是caching_sha2_password这对我们后面创建复制账号有影响我会在第五节专门讲。如果不熟悉8.0想先拿5.7练习逻辑也完全一致只是部分参数默认值不同。文中的命令和配置在5.7上基本也能跑只是复制账号的认证插件问题在5.7上不那么突出。2.3 目录结构项目目录我建成了这样mysql-cluster/ ├── docker-compose.yml ├── master/ │ ├── conf/my.cnf │ └── data/ ├── slave1/ │ ├── conf/my.cnf │ └── data/ └── slave2/ ├── conf/my.cnf └── data/data目录不用手动创建Docker挂载时会自动生成。conf/my.cnf必须手动创建因为这是MySQL服务启动时加载配置的关键文件。我习惯把所有现网用过的配置文件保留在Git里这样每次重建环境都有一份可追溯的基线。2.4 主从配置文件主库master/conf/my.cnf的内容[mysqld] server-id1 log-binmysql-bin binlog-formatROW gtid_modeON enforce_gtid_consistencyON从库slave1/conf/my.cnf和slave2/conf/my.cnf基本一样只是server-id不同[mysqld] server-id2 log-binmysql-bin binlog-formatROW gtid_modeON enforce_gtid_consistencyON read_onlyON super_read_onlyON从库上我也开了binlog而且加了read_only和super_read_only。这里解释一下为什么从库开binlog是为了以后把它提升为主库时还有完整的日志链路read_only防止业务写入从库但仅仅read_only对root不生效所以再加super_read_only这样即使是超级用户也不能写除非显式把super_read_only先关掉。3. docker-compose配置与容器启动3.1 docker-compose.yml完整内容直接给出我最终使用的完整配置。为方便阅读我把服务拆成三块主库和两个从库都加上了healthcheck这样后续编排可以基于健康状态做依赖等待。services: master: image: mysql:8.0 container_name: mysql-master restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: root123 ports: - 3306:3306 volumes: - ./master/conf/my.cnf:/etc/mysql/conf.d/my.cnf - ./master/data:/var/lib/mysql networks: - mysql-net healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1] interval: 5s retries: 10 slave1: image: mysql:8.0 container_name: mysql-slave1 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: root123 ports: - 3307:3306 volumes: - ./slave1/conf/my.cnf:/etc/mysql/conf.d/my.cnf - ./slave1/data:/var/lib/mysql networks: - mysql-net healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1] interval: 5s retries: 10 slave2: image: mysql:8.0 container_name: mysql-slave2 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: root123 ports: - 3308:3306 volumes: - ./slave2/conf/my.cnf:/etc/mysql/conf.d/my.cnf - ./slave2/data:/var/lib/mysql networks: - mysql-net healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1] interval: 5s retries: 10 networks: mysql-net: driver: bridge注意这里把自定义配置文件挂载到/etc/mysql/conf.d/my.cnfMySQL启动时会自动加载该目录下的所有.cnf文件从而覆盖默认配置。还有一种方式是直接通过command传参比如--server-id1效果一样但我更推荐保留my.cnf因为可读性更好且后续改配置不需要动compose文件。3.2 启动并检查容器在mysql-cluster目录下执行docker compose up -d docker compose ps正常情况下三个容器都是Up状态。如果某个容器启动失败用日志定位docker compose logs master常见启动失败原因是my.cnf文件权限或语法问题比如mysql容器对挂载配置文件的属主要求比较严格如果宿主机文件权限不对MySQL可能直接拒绝启动。我遇到过文件是root:root且0644容器内mysql用户也可以读但部分发行版默认snap安装的Docker会有SELinux限制导致挂载目录无法访问这时候需要调整SELinux context或者用-v的z后缀例如在volume配置里写./master/conf/my.cnf:/etc/mysql/conf.d/my.cnf:z。3.3 在主库创建复制账号容器启动完成后进入主库容器docker exec -it mysql-master bash mysql -uroot -proot123然后创建复制专用账号。这一步很关键如果使用MySQL 8.0强烈建议用mysql_native_password来创建避免从库I/O线程连不上CREATE USER repl% IDENTIFIED WITH mysql_native_password BY repl123; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO repl%; FLUSH PRIVILEGES;REPLICATION SLAVE权限是必须的REPLICATION CLIENT是为了让这个账号能执行SHOW MASTER STATUS之类的命令方便排查问题。在生产环境建议把账号网段限制得更严比如repl192.168.1.%而不是%。3.4 从库执行CHANGE MASTER分别在两个从库容器里执行配置主库的命令docker exec -it mysql-slave1 bash mysql -uroot -proot123STOP SLAVE; CHANGE MASTER TO MASTER_HOSTmaster, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDrepl123, MASTER_AUTO_POSITION1; START SLAVE;slave2也执行同样命令。这里专门强调一个最容易踩的坑MASTER_HOST必须写docker-compose网络里的服务名master不是localhost也不是宿主机IP。因为这是在容器内部连接主库容器走的是docker内网。有人说我宿主机IP也能通那是因为端口映射到了宿主机但从库容器访问宿主机IP需要走网关反而增加了不确定因素服务名是最稳的。4. 复制状态验证4.1 查看关键状态字段在从库执行SHOW SLAVE STATUS\G重点关注这些字段字段名期望值说明Slave_IO_RunningYesI/O线程是否在拉取日志Slave_SQL_RunningYesSQL线程是否在回放日志Seconds_Behind_Master0从库落后主库的秒数Last_IO_Errno0I/O线程最近错误号Last_SQL_Errno0SQL线程最近错误号Retrieved_Gtid_Set非空从库已拉取的GTID集合只要Snake_IO_Running和Slave_SQL_Running都是Yes基本就说明复制建立成功了。Seconds_Behind_Master是0表示没有延迟瞬时写入量大时这个值会跳动只要稳定回落就不用紧张。4.2 数据同步测试验证复制是否真的生效最直接的办法就是在主库建表写数据。主库执行USE app_db; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ); INSERT INTO user(name) VALUES (zhangsan), (lisi);到从库1查询USE app_db; SELECT * FROM user;如果能查到两条记录说明同步正常。我再强调一个细节从库的数据是异步复制过来的刚执行完主库写入后立刻查询理论上可能有毫秒级延迟但通过GTID方式连接一般不会出现查询不到的问题。4.3 从库只读验证为了确认从库真的不可写我习惯直接做一次写入测试USE app_db; INSERT INTO user(name) VALUES (wangwu);预期会报错The MySQL server is running with the --read-only option这就说明read_only和super_read_only都生效了。有人在这里会发现root可以写那是因为super_read_only没开或者当前用户具备SUPER权限。所以配置文件中我特意把super_read_onlyON加上了。5. 实际部署中的常见问题5.1 I/O线程起不来Last_IO_Errno 2061这个问题在我早期搭8.0主从时频繁出现。报错日志里通常会有Authentication plugin caching_sha2_password cannot be loaded之类的话。原因就是MySQL 8.0默认使用caching_sha2_password认证而从库连接时用的客户端库可能不支持。解决办法是我前面写的创建复制账号时显式指定IDENTIFIED WITH mysql_native_password。虽然MySQL官方已经逐步推荐caching_sha2_password但兼容性和复制的顺畅度才是这里的关键开发测试环境没有必要上最严格的安全插件。5.2 容器重启后复制中断docker-compose环境经常因为重启或者系统更新导致容器重建。有些情况下重启后Slave_SQL_Running会变成No或者从库一直卡在某个GTID不再前进。根因往往是relay log上下文或者server-id冲突。我的排查顺序是先看Last_SQL_Errno如果数据量不大最直接的办法是重建从库复制关系STOP SLAVE; RESET SLAVE ALL; CHANGE MASTER TO MASTER_HOSTmaster, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDrepl123, MASTER_AUTO_POSITION1; START SLAVE;不要怕重建GTID模式下从库会自动从主库最近的GTID位置开始拉取不需要重新初始化数据。前提是从库数据目录没有脏数据如果从库数据已经和主库不一致那就需要重新灌一次基础数据。5.3 端口映射导致的连接失败这是一个看起来很低级但很容易犯的错。很多人在从库执行CHANGE MASTER时MASTER_PORT写成宿主机映射出来的3307或3308甚至写成3306但把MASTER_HOST写成宿主机IP结果I/O线程一直报Last_IO_Errno 2003无法连接主库。原因很简单从库容器和主库容器在同一个docker网络里它们之间通信使用容器IP和服务名端口永远是3306。宿主机映射的3307只是给宿主机或外部客户端使用的容器内部不能拿这个端口去找主库。5.4 SQL线程报1062或1032错误这类错表示主从数据已经不一致了可能是从库被手动写过数据或者binlog回放时发生主键冲突。错误处理有个原则不要一上来就执行SET GLOBAL SQL_SLAVE_SKIP_COUNTER1这样会把问题掩盖掉。我的做法是先判断不一致的范围如果只有个别事务出错可以精准定位后手工补偿如果错误很多说明从库数据可信度已经很低直接重建从库更省事。这里重建从库的代价远低于在脏数据上继续修补。6. 生产环境下的进一步建议6.1 数据卷与网络规划用docker-compose搭主从数据卷必须放在宿主机别贪图方便写匿名卷否则容器一删数据就没了。生产环境如果要跑MySQL我建议不要用默认的docker0或者自定义bridge网络跑数据库集群因为NAT转发对数据库这种长连接密集型应用不够友好。可以考虑network_mode: host但代价是端口管理变复杂。磁盘方面如果条件允许把数据目录放到独立的SSD盘上避免和系统盘抢IO。6.2 复制状态监控主从复制配好只是起点长期运行一定要监控两个关键线程。最简单的做法是写个脚本每隔30秒在从库执行SHOW SLAVE STATUS检查Slave_IO_Running和Slave_SQL_Running是否为Yes不是就告警。更专业一点用Prometheus加mysqld_exporter把Seconds_Behind_Master采集起来配合Grafana画趋势图。这是一个典型的建设建议但我在实践中发现很多人连第一层的脚本告警都没做直到主从断了很久才发现这种事故特别被动。6.3 从库作为备份节点一主二从搭建好以后我习惯把备份任务放在从库上执行避免备份影响主库性能。常用的备份命令是mysqldump -uroot -proot123 --single-transaction --source-data2 app_db backup.sql这里要点一下MySQL 8.0.26之前参数叫--master-data之后改成了--source-data。2表示在备份文件的注释里记录binlog位置或GTID信息方便后面用这份备份去搭建新的从库。如果要做更快速的物理备份可以使用percona-xtrabackup效果更好但配置复杂度也更高。6.4 读写分离的下一步延伸一主二从搭完如果业务想要真正减轻主库读压力还需要在这套架构前面接入读写分离中间件。目前比较主流的选择是ProxySQL和ShardingSphere-Proxy。ProxySQL配置简单对MySQL协议兼容性好可以把读流量按权重分发到两个从库写流量强制走主库。我打算过阵子把ProxySQL加到这套docker-compose环境里再把读写分离的效果测完发出来。到时候会发现一主二从本身只是基础设施上层路由才是完整落地的最后一公里。最后再分享一个我自己的习惯每次搭完这类环境我都会在项目目录里放一份README把关键账号、端口、复制命令、踩过的坑全写进去。等过三个月自己回来看会发现这份README比任何博客都实用。这套一主二从环境我现在仍然在测试环境中使用后续接上ProxySQL后我会再补一篇读写分离的实战文章。希望这篇配置过程能帮你少走一些弯路。本文还有配套的精品资源点击获取
返回列表