数据库主从复制:原理不够用了之后

报表查询把单库 CPU 顶到 90% 以上,读写挤在同一条链路上,慢查询一多,写事务也跟着抖。书上的主从复制原理我早看过,真正动手配 binlog、拉从库、做读写分离,是在这次高可用改造里才串起来的。

为什么需要主从复制

读性能问题

随着业务增长,单库扛不住读了。

解决方案:读写分离,写走主库,读走从库。

高可用问题

单库故障,整个服务不可用。

解决方案:主从复制,主库挂了切换到从库。

数据备份问题

全量备份太慢,影响业务。

解决方案:从库做备份,不影响主库。

MySQL 主从复制

复制原理

MySQL 主从复制基于 binlog:

graph TB A[主库] -->|binlog| B[从库IO线程] B -->|中继日志| C[从库SQL线程] C --> D[从库数据] style A fill:#FFD700 style B fill:#90EE90 style C fill:#90EE90 style D fill:#87CEEB
  1. 主库执行 SQL,写入 binlog
  2. 从库 IO 线程读取 binlog,写入中继日志
  3. 从库 SQL 线程执行中继日志,同步数据

配置主库

-- 主库配置
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-format = ROW
binlog-do-db = myapp

-- 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

-- 查看主库状态
SHOW MASTER STATUS;

配置从库

-- 从库配置
[mysqld]
server-id = 2
relay-log = mysql-relay-bin
read-only = 1

-- 连接主库
CHANGE MASTER TO
  MASTER_HOST='master-ip',
  MASTER_USER='repl',
  MASTER_PASSWORD='password',
  MASTER_LOG_FILE='mysql-bin.000001',
  MASTER_LOG_POS=154;

-- 启动复制
START SLAVE;

-- 查看复制状态
SHOW SLAVE STATUS;

PostgreSQL 主从复制

流复制配置

主库配置

-- postgresql.conf
wal_level = replica
max_wal_senders = 5
wal_keep_segments = 10

-- pg_hba.conf
host    replication     repl            192.168.1.0/24          md5

从库配置

# 使用 pg_basebackup 创建从库
pg_basebackup -h master-ip -U repl -D /var/lib/postgresql/data -P -R

# 修改 postgresql.conf
hot_standby = on

MongoDB 主从复制

副本集配置

// 初始化副本集
rs.initiate({
  _id: "myapp",
  members: [
    { _id: 0, host: "mongodb1:27017" },
    { _id: 1, host: "mongodb2:27017" },
    { _id: 2, host: "mongodb3:27017", arbiterOnly: true }
  ]
});

// 查看副本集状态
rs.status();

Redis 主从复制

配置主从

# 从库配置
redis.conf
slaveof master-ip 6379
masterauth password

Sentinel 高可用

# sentinel.conf
sentinel monitor mymaster master-ip 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000

读写分离实践

应用层路由

# Python 示例
import random

class DatabaseRouter:
    def __init__(self, master_config, slave_configs):
        self.master = create_connection(master_config)
        self.slaves = [create_connection(c) for c in slave_configs]
    
    def get_read_connection(self):
        # 随机选择一个从库
        return random.choice(self.slaves)
    
    def get_write_connection(self):
        return self.master
    
    def execute_read(self, query):
        conn = self.get_read_connection()
        return conn.execute(query)
    
    def execute_write(self, query):
        conn = self.get_write_connection()
        return conn.execute(query)

# 使用
router = DatabaseRouter(
    master_config={'host': 'master-ip', 'port': 3306},
    slave_configs=[
        {'host': 'slave1-ip', 'port': 3306},
        {'host': 'slave2-ip', 'port': 3306}
    ]
)

中间件路由

ProxySQL

-- 配置 ProxySQL
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (1, 'master-ip', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (2, 'slave1-ip', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (2, 'slave2-ip', 3306);

-- 配置读写分离规则
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply) VALUES (1, 1, '^SELECT', 2, 1);
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply) VALUES (2, 1, '^INSERT|^UPDATE|^DELETE', 1, 1);

LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;

主从切换

手动切换

# 1. 停止从库复制
STOP SLAVE;

# 2. 提升从库为主库
STOP SLAVE;
RESET MASTER;
# 确保所有数据已同步

# 3. 修改应用配置,连接新主库

# 4. 将原主库配置为从库
CHANGE MASTER TO MASTER_HOST='new-master-ip';
START SLAVE;

自动切换

MHA (Master High Availability)

# 安装 MHA
apt-get install mha4mysql-node mha4mysql-manager

# 配置 MHA
[server default]
user=mha
password=mha
manager_workdir=/var/log/masterha
manager_log=/var/log/masterha/manager.log

[server1]
hostname=master-ip
candidate_master=1

[server2]
hostname=slave1-ip
candidate_master=1

[server3]
hostname=slave2-ip
no_master=1

Orchestrator

{
  "Debug": false,
  "ListenAddress": ":3000",
  "MySQLTopologyUser": "orchestrator",
  "MySQLTopologyPassword": "orchestrator",
  "Backend": "sqlite",
  "SQLite3DataFile": "/var/lib/orchestrator/orchestrator.sqlite3"
}

踩过的坑

坑一:主从延迟

从库延迟过高,读到了旧数据。

原因

  • 主库写压力大
  • 从库配置差
  • 网络延迟

解决

  • 增加从库数量
  • 升级从库配置
  • 使用 GTID 避免数据不一致
  • 部分查询直接读主库
-- 查看主从延迟
SHOW SLAVE STATUS;
-- 看 Seconds_Behind_Master 字段

-- 启用 GTID
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON

坑二:复制中断

binlog 位置不对,复制失败。

原因

  • 从库执行了写操作
  • 主库 binlog 被清理
  • 网络问题

解决

  • 从库设为只读
  • 配置 binlog 保留时间
  • 配置复制重试
-- 配置从库只读
[mysqld]
read-only = 1
super-read-only = 1

-- 配置 binlog 保留
[mysqld]
expire_logs_days = 7

坯三:切换失败

主库挂了,切换失败。

原因

  • 从库数据不完整
  • 切换脚本有问题
  • 网络分区

解决

  • 定期演练切换
  • 使用成熟的高可用方案
  • 配置自动切换
# 定期演练切换
0 0 1 * * /usr/local/bin/ha-test.sh

写在最后

主从复制这东西,不只是技术问题,也是架构问题。

解决了

  • 读性能问题
  • 高可用问题
  • 数据备份问题

带来了

  • 复杂度增加
  • 主从延迟
  • 数据一致性挑战

实施之前先评估:

  • 读写比例
  • 可用性要求
  • 团队能力
  • 运维成本

不是所有场景都需要主从复制,有时候单库加缓存就够用。


这次数据库高可用改造花了一个月,从单库到主从复制,再到自动切换。改造完成后,读性能提升了 3 倍,可用性从 99.5% 提升到 99.9%。

版权声明: 本文首发于 指尖魔法屋-数据库主从复制:原理不够用了之后https://blog.thinkmoon.cn/post/62-database-master-slave-replication-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!