为什么个人站长也需要主从复制
很多草根站长的 MySQL 都是一台服务器上单库跑到底:数据盘坏了数据全丢,误删了表只能靠几天前的备份恢复,网站半夜流量大一点主库 CPU 就飙到 100%。主从复制解决的就是这几个问题:一是数据冗余,主库挂掉从库还有一份完整数据;二是读写分离,把查询压力分流到从库;三是在从库上做备份和报表查询,不干扰线上主库。这篇文章从零开始,用 MySQL 8.0 的 GTID 模式把主从复制完整搭一遍,并给出读写分离的落地思路。
一、复制原理先搞懂
主从复制的核心是 binlog(二进制日志)。主库把所有数据变更写入 binlog,从库通过一个 IO 线程把主库的 binlog 拉到本地存成 relay log(中继日志),再由一个 SQL 线程把 relay log 里的变更重放到自己的数据里。旧版本靠文件名加偏移量(file + position)定位同步位置,配置麻烦还容易出错;MySQL 5.6 之后引入的 GTID(全局事务标识符)则给每个事务分配一个全局唯一的 ID,从库只要记住自己执行到哪个 GTID,就能自动接着同步,配置简单得多,也是现在推荐的方式。
二、环境准备
演示环境为两台 Ubuntu 服务器:主库 10.0.0.1,从库 10.0.0.2,都装 MySQL 8.0。生产环境建议主从版本一致,大版本不要跨太多,否则可能出现 binlog 格式不兼容的问题。另外两台机器的系统时间最好保持一致,时间不同步不会导致复制失败,但日志里的时间戳会错乱,排查问题时分不清先后顺序,建议都开启系统自动校时。先确认版本:
mysql --version mysql -u root -p -e "SELECT VERSION();"
三、主库配置
编辑主库的 MySQL 配置文件 /etc/mysql/mysql.conf.d/mysqld.cnf,在 [mysqld] 段加上:
[mysqld]server-id = 1
log_bin = mysql-bin
binlog_format = ROW
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_expire_logs_seconds = 604800
server-id 在整个复制拓扑里必须唯一;log_bin 开启二进制日志,这是复制的数据源;binlog_format 用 ROW 行模式,比 STATEMENT 更安全,不容易出现函数、存储过程在主从执行结果不一致的问题;gtid_mode 和 enforce_gtid_consistency 一起开启 GTID;binlog_expire_logs_seconds 设为 7 天,避免 binlog 无限占磁盘。改完重启主库:
systemctl restart mysql
SHOW MASTER STATUS; # 重启后确认 binlog 正常
四、从库配置
从库同样编辑 mysqld.cnf,注意 server-id 不能和主库相同:
[mysqld]
server-id = 2
relay_log = relay-bin
gtid_mode = ON
enforce_gtid_consistency = ON
read_only = ON
read_only 让从库拒绝非超级用户的写操作,防止程序误连从库写入导致主从不一致。改完重启从库。
五、创建复制账号
在主库上创建一个专门用于复制的账号,不要直接用 root:
CREATE USER 'repl'@'%' IDENTIFIED BY '强密码请替换';
GRANT REPLICATION SLAVE ON . TO 'repl'@'%';
FLUSH PRIVILEGES;
REPLICATION SLAVE 权限只允许该账号拉取 binlog,权限面很小,即使泄露也无法读写数据。
六、已有数据怎么初始化
如果主库是全新空库,直接跳到下一步。如果主库已经有线上数据,需要先把数据完整导到从库,再启动复制。最稳妥的方式是用 mysqldump 带 GTID 参数导出:
mysqldump -u root -p --single-transaction --all-databases \
--master-data=2 --set-gtid-purged=ON > /tmp/all.sql
--single-transaction 用 InnoDB 的一致性快照导出,不锁表;--master-data=2 会在导出文件里注释掉当前 binlog 坐标;GTID 模式下更简单,把文件拷到从库执行导入:
scp /tmp/all.sql root@10.0.0.2:/tmp/
mysql -u root -p < /tmp/all.sql
数据量特别大(几百 G)时推荐用 Percona XtraBackup 做物理备份初始化,速度比逻辑导出快一个数量级,这里不展开。
七、从库启动复制
MySQL 8.0 的语法是 CHANGE REPLICATION SOURCE TO(5.7 及以下用 CHANGE MASTER TO),在从库执行:
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '10.0.0.1',
SOURCE_USER = 'repl',
SOURCE_PASSWORD = '强密码请替换',
SOURCE_AUTO_POSITION = 1;
START REPLICA;
SOURCE_AUTO_POSITION = 1 表示用 GTID 自动定位,不用再手工填 binlog 文件名和偏移量。启动后查看复制状态:
SHOW REPLICA STATUSG
重点看三个字段:Replica_IO_Running 和 Replica_SQL_Running 都必须是 Yes;Seconds_Behind_Source 表示从库落后主库的秒数,稳定在 0 说明实时同步。第一次看到 Connection 状态是正常的,稍等几秒再查。
八、验证同步是否生效
在主库建一张测试表并插入数据:
CREATE DATABASE testdb;
CREATE TABLE testdb.t1 (id INT PRIMARY KEY, name VARCHAR(32));
INSERT INTO testdb.t1 VALUES (1, 'hello');
然后去从库查询:
SELECT * FROM testdb.t1; -- 应该能看到同一行数据
能看到说明复制链路已经通了。以后写操作都走主库,读操作可以走从库。另外在主库上执行 SHOW PROCESSLIST,能看到一个 User 为 repl 的 Binlog Dump 线程,说明从库正在实时拉取 binlog,这也是判断复制是否在工作的快速方法。
九、常见故障排查
复制出问题先看 SHOW REPLICA STATUS 的 Last_IO_Error 和 Last_SQL_Error 字段,常见的几类:一是 IO 线程连不上主库,多半是账号密码错误、主库防火墙没放行 3306 端口,或者云安全组没开;二是报 1062 主键冲突,说明从库已有相同主键的数据,通常是从库被误写过,需要先手工修复数据再继续;三是报 Last_IO_Error: Fatal error 提示 server UUID 相同,常见于用虚拟机克隆或整盘复制出来的从库,两台机器 MySQL 的 auto.cnf 里 UUID 一样,删除从库的 /var/lib/mysql/auto.cnf 后重启 MySQL 即可;四是主从长期不一致,可以用 pt-table-checksum 工具定期校验,发现差异再用 pt-table-sync 修复。如果 SQL 线程因为某条语句报错停住,确认这条语句可以忽略的话,可以在从库执行 STOP REPLICA、SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1、START REPLICA 跳过一个事务,但这只是临时救火,跳完之后一定要查清楚错误的根源,否则同样的问题会反复出现。
十、主从延迟的常见原因与处理
Seconds_Behind_Source 长期不为 0 是复制运维最常遇到的问题。延迟的常见原因有四个:第一,大事务,比如一次 UPDATE 影响几十万行,主库执行完要整体写进 binlog,从库回放同样要花很长时间,这类操作尽量拆小或者放到业务低峰期;第二,DDL 加锁,ALTER TABLE 在部分场景会阻塞回放,从库上的长查询也可能拖住 SQL 线程;第三,从库硬件弱,从库的磁盘和 CPU 通常不如主库,回放速度跟不上写入速度;第四,历史版本的单线程回放限制,MySQL 5.6 之前 SQL 线程只有一个,5.7 引入并行复制,8.0 默认开启,可以在从库配置并行度:
[mysqld]
replica_parallel_workers = 4
replica_parallel_type = LOGICAL_CLOCK
配置后重启从库生效。另外要区分瞬时延迟和持续延迟:跑一次大查询导致延迟几十秒是正常的,恢复后会自动追上;如果延迟持续增长,就要检查是不是从库磁盘 IO 打满或者有大事务在排队。主从之间如果走公网复制,带宽和网络延迟也会直接影响同步速度,MySQL 8.0.20 之后可以在配置里加 binlog_transaction_compression = ON 开启 binlog 压缩,传输量和 relay log 占用都会明显下降,对跨机房、跨地域的主从特别有用。建议写个监控脚本每小时记录一次 Seconds_Behind_Source,超过阈值发告警。
十一、读写分离怎么落地
复制搭好后,读写分离有两种常见做法。小项目可以在应用层做:配置两个数据源,写操作连主库,读操作连从库,代码里按需选择,实现最简单,适合个人项目。流量大了之后用代理层更省心,比较流行的是 ProxySQL:它支持自动读写分离,SELECT 走从库、其他语句走主库,还带连接池和主从延迟检测。核心配置就三张表,注册服务器和路由规则:
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (10, '10.0.0.1', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (20, '10.0.0.2', 3306);
INSERT INTO mysql_query_rules (rule_id, match_pattern, destination_hostgroup)
VALUES (1, '^SELECT', 20);
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
写组 10 指向主库,读组 20 指向从库,规则把 SELECT 开头的查询全部路由到读组。需要注意,主从复制是异步的,刚写入的数据立刻去从库读可能读不到,登录态、订单这类强一致场景一定要强制走主库,这是读写分离最常见的坑。
十二、总结
主从复制是 MySQL 高可用和读写分离的地基,GTID 模式让配置变得简单可靠。核心步骤一句话:主库开 binlog 建复制账号,从库配好 server-id 导入数据,CHANGE REPLICATION SOURCE TO 指向主库,最后盯住 SHOW REPLICA STATUS 里的两个 Running 和延迟秒数。如果哪天主库彻底宕机,把从库的 read_only 关掉、STOP REPLICA 后 RESET REPLICA ALL,从库就变成了新主库,业务先恢复,再找时间重建复制拓扑。建议再用脚本每小时检查一次复制状态,发现 SQL 线程停了立刻告警,因为复制中断时间越长,追平的成本越高。