CentOS 高可用部署手册:MySQL 双主复制 + Keepalived、Redis、Nginx
1. 文档目标
这份文档的目标不是讲概念,而是让你按顺序执行,就能把下面三套组件搭起来:
MySQL 双主复制(Master-Master Replication)+ Keepalived 单 VIP 漂移Redis 高可用Nginx 双活
本文特别按你的要求,把 MySQL 写成:
- 两台 MySQL 互为主从,双向复制
- 对业务只暴露一个
写 VIP VIP 始终漂移到当前健康的可写节点- 业务写操作永远连
VIP,不要直连某个物理 MySQL IP
这套设计的核心思想是:
MySQL 底层是双主复制业务层面坚持单写Keepalived 决定谁持有 VIP持有 VIP 的节点自动切成可写没有 VIP 的节点自动切成只读
这样做比"真双写"稳得多,能避免大量主键冲突、行冲突和数据分叉问题。
2. 先看重要结论
2.1 MySQL:推荐"双主复制 + 单写 VIP",不推荐真正双写
虽然 MySQL 可以做双主复制,但生产上更稳的方式是:
- 两边都复制
- 业务只向 VIP 写
- VIP 挂在哪台机子上,哪台才允许写
- 另一台始终
read_only + super_read_only
这样既满足你说的"数据双向同步",又能保证"写操作始终指向当前健康节点"。
2.2 Redis:开源 Redis 不能安全实现真正意义上的双活双写
这点一定先说清楚:
- 开源 Redis 官方高可用方案是
Replication + Sentinel - 开源 Redis Cluster 解决的是分片和部分高可用,不是"同一份数据多主双写"
- 真正的
Redis Active-Active是 Redis 官方企业版 / 软件版基于CRDT的能力
所以本文会给你两套 Redis 方案:
开源可落地方案:Redis 主从 + Sentinel真双活方案:Redis Software / Enterprise Active-Active(如果你有安装包和授权)
2.3 Nginx:双活推荐"两台同时提供服务 + 双 VIP 互备"
Nginx 这里我给你的是可实际落地的"双活":
nginx01持有VIP-Anginx02持有VIP-B- 两台都在对外提供服务
- 任意一台挂掉,另一台接管两个 VIP
3. 版本与环境说明
3.1 操作系统建议
本文以 CentOS Stream 9 / Rocky Linux 9 / AlmaLinux 9 为例。
原因很简单:
CentOS Linux 7已于 2024-06-30 结束维护CentOS Linux 8已于 2021-12-31 结束维护
如果你现在是新部署,不建议再上 CentOS Linux 7/8。
如果你现场就是 CentOS 7:
- 大多数配置思想完全一样
- 只是包管理器把
dnf换成yum - 个别包名、repo 地址可能不同
3.2 网络前提
Keepalived 的 VIP 漂移一般要求:
- 两台机器在同一二层网络 / 同一 VLAN
- 交换机、虚拟化平台允许 VIP 漂移
- 没有云厂商的反 ARP / 反欺骗限制
如果你在公有云上:
- 很多云环境不支持传统 VRRP 漂移
- 这时不要强行上 Keepalived VIP
- 应改用云厂商的
SLB / EIP / ENI / 私网IP漂移 / 健康检查
本文默认你是在:
- 物理机房
- VMware / KVM
- 同网段虚拟机
4. 整体拓扑规划
4.1 主机规划
你可以直接照这个规划做,后面所有示例命令也按这个写。
| 角色 | 主机名 | IP | 说明 |
|---|---|---|---|
| MySQL 节点1 | db01 |
192.168.10.11 |
初始主写节点 |
| MySQL 节点2 | db02 |
192.168.10.12 |
初始备写节点 |
| MySQL 写 VIP | mysql-vip |
192.168.10.10 |
业务写连接地址 |
| Redis 主节点 | redis01 |
192.168.10.21 |
开源方案下的初始主 |
| Redis 从节点 | redis02 |
192.168.10.22 |
开源方案下的初始从 |
| Redis Sentinel 3号 | redis03 |
192.168.10.23 |
只跑 Sentinel 也可以 |
| Nginx 节点1 | nginx01 |
192.168.10.31 |
持有 VIP-A |
| Nginx 节点2 | nginx02 |
192.168.10.32 |
持有 VIP-B |
| Nginx VIP-A | nginx-vip-a |
192.168.10.30 |
初始在 nginx01 |
| Nginx VIP-B | nginx-vip-b |
192.168.10.33 |
初始在 nginx02 |
| 应用节点1 | app01 |
192.168.10.101 |
示例上游 |
| 应用节点2 | app02 |
192.168.10.102 |
示例上游 |
4.2 业务访问方式
业务访问建议如下:
- MySQL 写:连
192.168.10.10:3306 - Redis:
- 如果用 Sentinel:客户端连 3 个 Sentinel,自动发现当前 master
- 如果用 Redis Active-Active:客户端连各地本地实例地址
- Nginx:对外提供
192.168.10.30和192.168.10.33
如果你有 DNS:
api.example.com可以同时解析到192.168.10.30和192.168.10.33
5. 所有节点的统一前置操作
下面这些操作,除非特别说明,否则所有相关节点都执行。
5.1 设置主机名
db01
bash
hostnamectl set-hostname db01
db02
bash
hostnamectl set-hostname db02
redis01
bash
hostnamectl set-hostname redis01
redis02
bash
hostnamectl set-hostname redis02
redis03
bash
hostnamectl set-hostname redis03
nginx01
bash
hostnamectl set-hostname nginx01
nginx02
bash
hostnamectl set-hostname nginx02
5.2 配置 /etc/hosts
所有节点写入:
bash
cat >> /etc/hosts <<'EOF'
192.168.10.11 db01
192.168.10.12 db02
192.168.10.21 redis01
192.168.10.22 redis02
192.168.10.23 redis03
192.168.10.31 nginx01
192.168.10.32 nginx02
192.168.10.101 app01
192.168.10.102 app02
EOF
5.3 安装基础工具
bash
dnf install -y vim wget curl net-tools lsof bash-completion tar unzip rsync chrony policycoreutils-python-utils
5.4 启动时钟同步
bash
systemctl enable --now chronyd
chronyc tracking
高可用环境里时间同步很重要,尤其是:
- MySQL 切换排障
- Redis Sentinel 判定
- 日志对齐
5.5 防火墙
先开启基础端口。
MySQL 节点
bash
firewall-cmd --permanent --add-service=mysql
firewall-cmd --permanent --add-rich-rule='rule protocol value="vrrp" accept'
firewall-cmd --reload
Redis 节点
bash
firewall-cmd --permanent --add-port=6379/tcp
firewall-cmd --permanent --add-port=26379/tcp
firewall-cmd --reload
Nginx 节点
bash
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-rich-rule='rule protocol value="vrrp" accept'
firewall-cmd --reload
如果你后面部署 Redis Software / Enterprise Active-Active,还要根据实际数据库端口额外放通管理端口与复制端口。
5.6 SELinux
如果你现在主要目标是先把环境搭起来,建议调试期先切到 permissive。
bash
setenforce 0
sed -i 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config
说明:
- 生产上长期最优做法是按服务写策略或打标签
- 但如果你现在先要"快速搭起来并验证切换",
permissive会省很多时间
5.7 内核参数
所有 Redis 节点执行
bash
cat > /etc/sysctl.d/99-redis.conf <<'EOF'
vm.overcommit_memory = 1
net.core.somaxconn = 1024
EOF
sysctl --system
关闭透明大页
bash
cat > /etc/systemd/system/disable-thp.service <<'EOF'
[Unit]
Description=Disable Transparent Huge Pages
After=network.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled'
ExecStart=/bin/bash -c 'echo never > /sys/kernel/mm/transparent_hugepage/defrag'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now disable-thp
6. MySQL:双主复制 + Keepalived 写 VIP
6.1 架构说明
这一节按下面的模式部署:
db01 <-> db02双向复制- 对业务只暴露一个写 VIP:
192.168.10.10 - 初始
db01持有 VIP - 初始
db01可写 - 初始
db02只读 - 当
db01故障时,VIP 漂移到db02 db02自动从只读切成可写db01恢复后保持只读,等待人工切回
这是一种:
- 底层双主
- 业务单写
- 通过 VIP 固定写入口
的生产可用方案。
6.2 MySQL 安装
6.2.1 安装官方社区版
在 db01 和 db02 执行:
bash
dnf install -y https://dev.mysql.com/get/mysql84-community-release-el9-1.noarch.rpm
dnf module disable -y mysql
dnf install -y mysql-community-server keepalived
如果上面的 MySQL repo rpm 因为官方小版本变化下载失败,就去 MySQL 官方 YUM 仓库页面换成当前版本的 release rpm,后续步骤不变。
6.2.2 启动 MySQL
bash
systemctl enable --now mysqld
systemctl status mysqld --no-pager
6.2.3 取出临时 root 密码
bash
grep 'temporary password' /var/log/mysqld.log
6.2.4 首次登录并修改 root 密码
bash
mysql --connect-expired-password -uroot -p
登录后执行:
sql
ALTER USER 'root'@'localhost' IDENTIFIED BY 'Root@123456!';
6.2.5 创建本地 root 登录文件
在 db01 和 db02 创建:
bash
cat > /root/.my.cnf <<'EOF'
[client]
user=root
password=Root@123456!
EOF
chmod 600 /root/.my.cnf
后面脚本会直接用这个文件连接本地 MySQL。
6.3 MySQL 配置文件
先确认网卡名:
bash
ip addr
假设网卡名是 ens192,下面按这个写。如果你的实际网卡不是 ens192,全部替换掉。
6.3.1 db01 的 /etc/my.cnf
ini
[mysqld]
bind-address = 0.0.0.0
port = 3306
server-id = 11
report_host = db01
log_bin = mysql-bin
binlog_format = ROW
binlog_row_image = FULL
log_replica_updates = ON
relay_log = relay-bin
relay_log_recovery = ON
gtid_mode = ON
enforce_gtid_consistency = ON
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
binlog_expire_logs_seconds = 604800
skip_name_resolve = ON
auto_increment_increment = 2
auto_increment_offset = 1
read_only = ON
super_read_only = ON
6.3.2 db02 的 /etc/my.cnf
ini
[mysqld]
bind-address = 0.0.0.0
port = 3306
server-id = 12
report_host = db02
log_bin = mysql-bin
binlog_format = ROW
binlog_row_image = FULL
log_replica_updates = ON
relay_log = relay-bin
relay_log_recovery = ON
gtid_mode = ON
enforce_gtid_consistency = ON
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
binlog_expire_logs_seconds = 604800
skip_name_resolve = ON
auto_increment_increment = 2
auto_increment_offset = 2
read_only = ON
super_read_only = ON
6.3.3 重启 MySQL
bash
systemctl restart mysqld
6.4 初始化双主复制
6.4.1 在 db01 临时切成可写
因为我们配置文件默认是只读,所以先在 db01 临时打开写权限用于初始化。
bash
mysql -e "SET GLOBAL super_read_only=OFF;"
mysql -e "SET GLOBAL read_only=OFF;"
6.4.2 在 db01 创建复制账号
sql
CREATE USER 'repl'@'192.168.10.%' IDENTIFIED BY 'Repl@123456!';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'192.168.10.%';
FLUSH PRIVILEGES;
6.4.3 先配置 db02 从 db01 复制
在 db02 执行:
sql
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='192.168.10.11',
SOURCE_PORT=3306,
SOURCE_USER='repl',
SOURCE_PASSWORD='Repl@123456!',
SOURCE_AUTO_POSITION=1,
GET_SOURCE_PUBLIC_KEY=1;
START REPLICA;
检查:
sql
SHOW REPLICA STATUS\G
你至少要看到下面几个关键字段正常:
Replica_IO_Running: YesReplica_SQL_Running: YesLast_IO_Error:为空Last_SQL_Error:为空
6.4.4 等 db02 同步到复制账号后,再配置 db01 从 db02 复制
先在 db02 确认复制账号已经同步过去:
sql
SELECT user, host FROM mysql.user WHERE user='repl';
确认有结果后,在 db01 执行:
sql
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='192.168.10.12',
SOURCE_PORT=3306,
SOURCE_USER='repl',
SOURCE_PASSWORD='Repl@123456!',
SOURCE_AUTO_POSITION=1,
GET_SOURCE_PUBLIC_KEY=1;
START REPLICA;
检查:
sql
SHOW REPLICA STATUS\G
6.4.5 验证双向复制
在 db01 执行:
sql
CREATE DATABASE ha_demo;
USE ha_demo;
CREATE TABLE t1 (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO t1(name) VALUES('from_db01_1');
在 db02 验证:
sql
SELECT * FROM ha_demo.t1;
现在不要在 db02 直接对业务表写入。虽然底层已经是双主复制,但业务写入口必须统一走 VIP。
6.5 Keepalived 配置 MySQL 写 VIP
6.5.1 Keepalived 检测脚本
在 db01 和 db02 创建 /etc/keepalived/check_mysql.sh:
bash
cat > /etc/keepalived/check_mysql.sh <<'EOF'
#!/bin/bash
/usr/bin/mysqladmin --defaults-file=/root/.my.cnf ping >/dev/null 2>&1 || exit 1
/usr/bin/mysql --defaults-file=/root/.my.cnf -Nse "SELECT 1" >/dev/null 2>&1 || exit 1
exit 0
EOF
chmod 755 /etc/keepalived/check_mysql.sh
6.5.2 Keepalived 切换角色脚本
这个脚本负责:
- 当前节点成为
MASTER时,把 MySQL 切成可写 - 当前节点变成
BACKUP或FAULT时,把 MySQL 切成只读
在 db01 和 db02 创建 /etc/keepalived/mysql_role.sh:
bash
cat > /etc/keepalived/mysql_role.sh <<'EOF'
#!/bin/bash
ROLE="$1"
MYSQL="/usr/bin/mysql --defaults-file=/root/.my.cnf -Nse"
logger -t keepalived-mysql "switch mysql role to ${ROLE}"
case "${ROLE}" in
master)
${MYSQL} "SET GLOBAL super_read_only=OFF;"
${MYSQL} "SET GLOBAL read_only=OFF;"
;;
backup|fault|stop)
${MYSQL} "SET GLOBAL read_only=ON;"
${MYSQL} "SET GLOBAL super_read_only=ON;"
;;
*)
logger -t keepalived-mysql "unknown role ${ROLE}"
exit 1
;;
esac
exit 0
EOF
chmod 755 /etc/keepalived/mysql_role.sh
6.5.3 db01 的 /etc/keepalived/keepalived.conf
conf
global_defs {
router_id MYSQL_HA_DB01
enable_script_security
script_user root
}
vrrp_script chk_mysql {
script "/etc/keepalived/check_mysql.sh"
interval 2
timeout 2
fall 2
rise 2
weight -60
}
vrrp_instance VI_MYSQL {
state MASTER
interface ens192
virtual_router_id 51
priority 150
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass MyVip001
}
unicast_src_ip 192.168.10.11
unicast_peer {
192.168.10.12
}
virtual_ipaddress {
192.168.10.10/24 dev ens192 label ens192:vip
}
track_script {
chk_mysql
}
notify_master "/etc/keepalived/mysql_role.sh master"
notify_backup "/etc/keepalived/mysql_role.sh backup"
notify_fault "/etc/keepalived/mysql_role.sh fault"
notify_stop "/etc/keepalived/mysql_role.sh stop"
}
6.5.4 db02 的 /etc/keepalived/keepalived.conf
conf
global_defs {
router_id MYSQL_HA_DB02
enable_script_security
script_user root
}
vrrp_script chk_mysql {
script "/etc/keepalived/check_mysql.sh"
interval 2
timeout 2
fall 2
rise 2
weight -60
}
vrrp_instance VI_MYSQL {
state BACKUP
interface ens192
virtual_router_id 51
priority 100
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass MyVip001
}
unicast_src_ip 192.168.10.12
unicast_peer {
192.168.10.11
}
virtual_ipaddress {
192.168.10.10/24 dev ens192 label ens192:vip
}
track_script {
chk_mysql
}
notify_master "/etc/keepalived/mysql_role.sh master"
notify_backup "/etc/keepalived/mysql_role.sh backup"
notify_fault "/etc/keepalived/mysql_role.sh fault"
notify_stop "/etc/keepalived/mysql_role.sh stop"
}
6.5.5 调整 Keepalived 启动顺序
bash
mkdir -p /etc/systemd/system/keepalived.service.d
cat > /etc/systemd/system/keepalived.service.d/override.conf <<'EOF'
[Unit]
After=network-online.target mysqld.service
Wants=network-online.target
EOF
systemctl daemon-reload
6.5.6 启动 Keepalived
先在 db01 启动:
bash
systemctl enable --now keepalived
systemctl status keepalived --no-pager
ip addr show ens192
确认 192.168.10.10 已经在 db01 上。
再在 db02 启动:
bash
systemctl enable --now keepalived
systemctl status keepalived --no-pager
6.5.7 验证主备节点读写状态
在 db01 看:
sql
SHOW VARIABLES LIKE 'read_only';
SHOW VARIABLES LIKE 'super_read_only';
预期:
read_only = OFFsuper_read_only = OFF
在 db02 看:
sql
SHOW VARIABLES LIKE 'read_only';
SHOW VARIABLES LIKE 'super_read_only';
预期:
read_only = ONsuper_read_only = ON
6.6 业务连接方式
应用不要写死 db01 或 db02 的物理 IP。
统一连:
text
192.168.10.10:3306
例如 Java JDBC:
properties
spring.datasource.url=jdbc:mysql://192.168.10.10:3306/ha_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
spring.datasource.username=app_user
spring.datasource.password=App@123456!
6.7 MySQL 切换测试
6.7.1 正常写入测试
通过 VIP 连 MySQL:
bash
mysql -h 192.168.10.10 -uroot -p
执行:
sql
INSERT INTO ha_demo.t1(name) VALUES('write_via_vip_1');
SELECT * FROM ha_demo.t1;
在 db02 验证同步:
sql
SELECT * FROM ha_demo.t1;
6.7.2 模拟 db01 的 MySQL 进程故障
在 db01:
bash
systemctl stop mysqld
观察:
bash
ip addr show ens192
预期:
db01丢失192.168.10.10db02获得192.168.10.10db02自动切成可写
在 db02 上确认:
sql
SHOW VARIABLES LIKE 'read_only';
SHOW VARIABLES LIKE 'super_read_only';
应该都为 OFF。
然后再次通过 VIP 写入:
bash
mysql -h 192.168.10.10 -uroot -p -e "INSERT INTO ha_demo.t1(name) VALUES('write_after_failover');"
6.7.3 恢复 db01
在 db01:
bash
systemctl start mysqld
systemctl restart keepalived
由于配置了 nopreempt,恢复后的 db01 不会自动抢回 VIP。
这通常是更稳的做法,避免服务刚恢复就抖动回切。
6.7.4 手工回切到 db01
先确认 db01 复制正常:
sql
SHOW REPLICA STATUS\G
然后在 db02:
bash
systemctl stop keepalived
VIP 会回到 db01。
确认后再在 db02 重新启动:
bash
systemctl start keepalived
6.8 MySQL 增强项:建议开启半同步复制
如果你希望降低切换时丢事务的风险,建议在两边开启半同步复制。
在 db01 和 db02 执行:
sql
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
SET PERSIST rpl_semi_sync_source_enabled = ON;
SET PERSIST rpl_semi_sync_replica_enabled = ON;
SET PERSIST rpl_semi_sync_source_timeout = 1000;
SET PERSIST rpl_semi_sync_source_wait_for_replica_count = 1;
查看状态:
sql
SHOW VARIABLES LIKE 'rpl_semi_sync%';
SHOW STATUS LIKE 'Rpl_semi_sync%';
说明:
- 半同步不是强一致
- 但能显著缩小主库故障时未复制事务的窗口
6.9 MySQL 常见坑
坑1:不要让应用绕过 VIP 直连某台 MySQL
否则会出现:
- 应用以为自己写的是主库
- 实际却写到了当前备库
- 数据流向和切换逻辑被破坏
坑2:双主复制不等于建议双写
不要因为底层是双主,就让一部分服务写 db01,另一部分服务写 db02。
这样很容易出现:
- 自增主键冲突
- 同一行被两边同时改
- 复制冲突
- 双方数据分叉
坑3:Keepalived 两节点方案仍有脑裂风险
任何双节点 VIP 方案都不是绝对无脑裂。
如果你要更强的自动化和仲裁能力,建议后续引入:
- MySQL Orchestrator
- MHA
- ProxySQL
- 第三方仲裁 / STONITH / 云平台故障隔离
7. Redis:两套方案都给你
7.1 先明确能力边界
如果你要的是"同一份数据,两边都能同时写,而且自动合并冲突":
- 开源 Redis 不适合直接这样做
- 官方真正的 Active-Active 依赖企业版 / 软件版的
CRDT
所以我给你两套可执行内容:
开源方案:Redis 主从 + Sentinel真双活方案:Redis Software / Enterprise Active-Active
如果你目前没有企业版安装包和授权,先按开源方案落地。
7.2 开源 Redis 高可用:主从 + Sentinel
这不是"真双活双写",但它是开源 Redis 官方推荐的非 Cluster 高可用方案。
7.2.1 安装 Redis
在 redis01、redis02、redis03 执行:
bash
dnf install -y redis
如果你的仓库里没有 redis 包,就换用你们内部仓库或按官方文档安装当前版本。
7.2.2 redis01 配置为主节点
备份原配置:
bash
cp /etc/redis/redis.conf /etc/redis/redis.conf.bak
写入 /etc/redis/redis.conf:
conf
bind 127.0.0.1 192.168.10.21
protected-mode yes
port 6379
timeout 0
tcp-keepalive 300
daemonize no
supervised systemd
dir /var/lib/redis
loglevel notice
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec
requirepass Redis@123456!
masterauth Redis@123456!
启动:
bash
systemctl enable --now redis
systemctl status redis --no-pager
7.2.3 redis02 配置为从节点
bash
cp /etc/redis/redis.conf /etc/redis/redis.conf.bak
写入 /etc/redis/redis.conf:
conf
bind 127.0.0.1 192.168.10.22
protected-mode yes
port 6379
timeout 0
tcp-keepalive 300
daemonize no
supervised systemd
dir /var/lib/redis
loglevel notice
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec
replicaof 192.168.10.21 6379
requirepass Redis@123456!
masterauth Redis@123456!
启动:
bash
systemctl enable --now redis
systemctl status redis --no-pager
验证:
bash
redis-cli -a 'Redis@123456!' INFO replication
在 redis02 上应看到 role:slave 或 role:replica。
7.2.4 redis03 作为第三个 Sentinel 节点
redis03 可以不跑数据实例,只跑 Sentinel。
7.2.5 创建 Sentinel 配置
在 redis01、redis02、redis03 创建目录:
bash
mkdir -p /etc/redis-sentinel /var/lib/redis-sentinel
chown -R redis:redis /var/lib/redis-sentinel
在不同机器上分别写自己的 bind 地址。
下面先给 redis01 示例:
bash
cat > /etc/redis-sentinel/sentinel.conf <<'EOF'
port 26379
bind 127.0.0.1 192.168.10.21
daemonize no
protected-mode yes
dir /var/lib/redis-sentinel
logfile ""
sentinel monitor mymaster 192.168.10.21 6379 2
sentinel auth-pass mymaster Redis@123456!
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
EOF
redis02 把 bind 改成 192.168.10.22。
redis03 把 bind 改成 192.168.10.23。
7.2.6 创建 Sentinel systemd 服务
在三台 Redis 节点执行:
bash
cat > /etc/systemd/system/redis-sentinel.service <<'EOF'
[Unit]
Description=Redis Sentinel
After=network.target
[Service]
User=redis
Group=redis
ExecStart=/usr/bin/redis-sentinel /etc/redis-sentinel/sentinel.conf --supervised systemd
ExecStop=/bin/kill -s TERM $MAINPID
Restart=always
LimitNOFILE=10032
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now redis-sentinel
systemctl status redis-sentinel --no-pager
7.2.7 验证 Sentinel
在任意一台执行:
bash
redis-cli -p 26379 SENTINEL masters
redis-cli -p 26379 SENTINEL replicas mymaster
redis-cli -p 26379 SENTINEL sentinels mymaster
7.2.8 Sentinel 故障切换测试
在 redis01 停掉 Redis:
bash
systemctl stop redis
等待 10 到 30 秒,再在 redis02 或 redis03 上看:
bash
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
应该返回 redis02 的地址。
7.2.9 客户端连接方式
业务不要只连 192.168.10.21:6379。
应该使用支持 Sentinel 的客户端。
例如 Spring Boot:
properties
spring.data.redis.password=Redis@123456!
spring.data.redis.sentinel.master=mymaster
spring.data.redis.sentinel.nodes=192.168.10.21:26379,192.168.10.22:26379,192.168.10.23:26379
7.3 Redis 真双活:Redis Software / Enterprise Active-Active
如果你明确要"多地同时写入同一份数据",那就不要拿开源 Redis 硬拼。
应该直接使用 Redis 官方 Active-Active。
7.3.1 架构要求
生产上建议:
- 至少两个站点 / 两个集群
- 每个集群至少 3 个节点
- 每个站点都有本地读写入口
- 底层通过 CRDT 复制冲突处理
如果你只是实验环境,可以先做:
- 集群A:1台
- 集群B:1台
但那只是功能验证,不是高可用生产形态。
7.3.2 基本步骤
- 准备 Redis Software 安装包和授权
- 在每个站点安装 Redis Software
- 分别建立两个集群
- 在集群间创建 Active-Active 数据库
- 让业务在每个地域连本地数据库端点
- 验证两地互相写入后数据收敛
7.3.3 最小实验环境安装示例
下面给的是"最小验证环境"的做法,适合实验或 PoC。
假设:
- 集群A 节点:
192.168.10.41 - 集群B 节点:
192.168.10.42
先把 Redis Software 安装包上传到两台机器,例如放到 /opt/redis-enterprise/。
在两台机器分别执行:
bash
cd /opt/redis-enterprise
tar -vxf <downloaded-redis-software-package>.tar
sudo ./install.sh -y
安装完成后,浏览器访问:
text
https://192.168.10.41:8443
https://192.168.10.42:8443
分别完成两个独立集群的初始化:
- 设置集群名称
- 设置管理员账号
- 设置数据目录与持久化目录
- 确认管理端口可访问
7.3.4 创建 Active-Active 数据库
在任意一个集群的管理界面中:
- 进入
Databases - 选择
Add database - 选择
Active-Active - 设置数据库名称,例如
aa-cache - 设置每个参与集群的内存配额
- 选择参与的两个集群
- 创建数据库
创建完成后,每个集群都会得到自己的本地数据库端点和端口。
7.3.5 Active-Active 验证
假设:
- 集群A 数据库端点:
192.168.10.41:12000 - 集群B 数据库端点:
192.168.10.42:12000
在集群A写入:
bash
redis-cli -h 192.168.10.41 -p 12000 SET aa:key "from-site-a"
在集群B读取:
bash
redis-cli -h 192.168.10.42 -p 12000 GET aa:key
再反向测试:
bash
redis-cli -h 192.168.10.42 -p 12000 SET aa:key2 "from-site-b"
redis-cli -h 192.168.10.41 -p 12000 GET aa:key2
如果你要做真正生产形态,请把"单节点集群"升级为"每站点至少 3 节点集群"。
7.3.6 你需要知道的事实
- Active-Active 是 Redis 官方企业能力
- 底层不是开源 Redis 主从复制
- 它依赖 CRDT 机制来处理多地写入
如果你手上已经有 Redis Software 安装包,我建议下一步我单独再给你一份"Redis Software Active-Active 专项落地文档",因为这部分会涉及:
- 集群初始化
- 管理端口
- 授权导入
- Active-Active 数据库创建
- 站点互联
这和开源 Redis 不是同一套命令。
8. Nginx:双活 + 双 VIP 互备
8.1 架构说明
这里的 Nginx 双活我按下面方式做:
nginx01持有192.168.10.30nginx02持有192.168.10.33- 两台机器都运行 Nginx
- 两个 VIP 都对外提供服务
nginx01是VIP-A的主nginx02是VIP-B的主- 任一节点挂掉,另一节点接管两个 VIP
这是真正常见、可落地、能同时承载流量的 Nginx 双活方案。
8.2 安装 Nginx 与 Keepalived
在 nginx01 和 nginx02 执行:
bash
dnf install -y nginx keepalived
systemctl enable nginx
8.3 配置 Nginx 反向代理
8.3.1 创建站点配置
在 nginx01 和 nginx02 写 /etc/nginx/conf.d/service_case.conf:
nginx
upstream app_backend {
least_conn;
server 192.168.10.101:8080 max_fails=3 fail_timeout=10s;
server 192.168.10.102:8080 max_fails=3 fail_timeout=10s;
keepalive 64;
}
server {
listen 80;
server_name _;
location /nginx_status {
stub_status;
allow 127.0.0.1;
allow 192.168.10.0/24;
deny all;
}
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 3s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
}
}
8.3.2 校验并启动
bash
nginx -t
systemctl enable --now nginx
systemctl status nginx --no-pager
8.4 Nginx 健康检查脚本
在 nginx01 和 nginx02 写 /etc/keepalived/check_nginx.sh:
bash
cat > /etc/keepalived/check_nginx.sh <<'EOF'
#!/bin/bash
/usr/bin/systemctl is-active --quiet nginx || exit 1
/usr/bin/curl -fsS http://127.0.0.1/nginx_status >/dev/null 2>&1 || exit 1
exit 0
EOF
chmod 755 /etc/keepalived/check_nginx.sh
8.5 nginx01 的 Keepalived 配置
nginx01 同时承担:
VIP-A主VIP-B备
写 /etc/keepalived/keepalived.conf:
conf
global_defs {
router_id NGINX01
enable_script_security
script_user root
}
vrrp_script chk_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
timeout 2
fall 2
rise 2
weight -60
}
vrrp_instance VI_WEB_A {
state MASTER
interface ens192
virtual_router_id 61
priority 150
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass WebVip01
}
unicast_src_ip 192.168.10.31
unicast_peer {
192.168.10.32
}
virtual_ipaddress {
192.168.10.30/24 dev ens192 label ens192:vip30
}
track_script {
chk_nginx
}
}
vrrp_instance VI_WEB_B {
state BACKUP
interface ens192
virtual_router_id 62
priority 100
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass WebVip02
}
unicast_src_ip 192.168.10.31
unicast_peer {
192.168.10.32
}
virtual_ipaddress {
192.168.10.33/24 dev ens192 label ens192:vip33
}
track_script {
chk_nginx
}
}
8.6 nginx02 的 Keepalived 配置
nginx02 同时承担:
VIP-A备VIP-B主
写 /etc/keepalived/keepalived.conf:
conf
global_defs {
router_id NGINX02
enable_script_security
script_user root
}
vrrp_script chk_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
timeout 2
fall 2
rise 2
weight -60
}
vrrp_instance VI_WEB_A {
state BACKUP
interface ens192
virtual_router_id 61
priority 100
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass WebVip01
}
unicast_src_ip 192.168.10.32
unicast_peer {
192.168.10.31
}
virtual_ipaddress {
192.168.10.30/24 dev ens192 label ens192:vip30
}
track_script {
chk_nginx
}
}
vrrp_instance VI_WEB_B {
state MASTER
interface ens192
virtual_router_id 62
priority 150
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass WebVip02
}
unicast_src_ip 192.168.10.32
unicast_peer {
192.168.10.31
}
virtual_ipaddress {
192.168.10.33/24 dev ens192 label ens192:vip33
}
track_script {
chk_nginx
}
}
8.7 启动 Keepalived
在 nginx01、nginx02 执行:
bash
systemctl enable --now keepalived
systemctl status keepalived --no-pager
检查 VIP:
在 nginx01:
bash
ip addr show ens192
预期有 192.168.10.30。
在 nginx02:
bash
ip addr show ens192
预期有 192.168.10.33。
8.8 Nginx 双活验证
8.8.1 同时访问两个 VIP
bash
curl -I http://192.168.10.30/
curl -I http://192.168.10.33/
都应该正常返回。
8.8.2 模拟 nginx01 故障
在 nginx01:
bash
systemctl stop nginx
然后在 nginx02 查看:
bash
ip addr show ens192
预期:
192.168.10.30漂到nginx02192.168.10.33仍在nginx02
此时 nginx02 同时接管两个 VIP。
8.8.3 恢复 nginx01
bash
systemctl start nginx
systemctl restart keepalived
如果你想恢复后自动抢回,可以通过优先级和是否启用 nopreempt 再做细调;这里先按稳定优先处理。
9. 最终验收清单
你最后要确认以下结果全部成立。
9.1 MySQL
db01与db02都能SHOW REPLICA STATUS\G- 正常情况下只有持有 VIP 的节点可写
- 业务通过
192.168.10.10:3306能持续写入 - 停掉当前主节点 MySQL 后,VIP 能漂移到另一台
- 漂移后另一台自动切成可写
9.2 Redis
redis01、redis02主从关系正常- 三个 Sentinel 都正常
- 停掉当前 master 后,Sentinel 能选出新 master
- 应用客户端通过 Sentinel 连接,不写死单节点
9.3 Nginx
192.168.10.30与192.168.10.33都能提供服务- 两台 Nginx 都在正常转发流量
- 任意一台 Nginx 宕掉后,另一台可接管两个 VIP
10. 推荐上线顺序
按下面顺序最稳:
- 先完成所有节点基础环境
- 先搭 MySQL,并把 VIP 漂移验证通过
- 再搭 Redis 主从 + Sentinel
- 最后搭 Nginx 双活
- 最后再让应用改为连 MySQL VIP、Redis Sentinel、Nginx VIP
11. 如果你下一步还要我继续做
这份文档已经可以直接照着搭。
如果你愿意,我下一步可以继续帮你补下面任意一种内容:
- 按你们真实 IP、网卡名、库名、域名,改成一份"你现场可直接复制"的定制版
- 再补一份"故障排查手册",专门写
SHOW REPLICA STATUS、Keepalived、Sentinel、Nginx 的异常处理 - 如果你有 Redis 企业版安装包,我单独补一份
Redis Active-Active 真双活专项实施手册
12. 官方参考资料
以下是本文编写时参考的官方资料,你后续排障时也可以直接对照:
- MySQL Replication 文档:
- MySQL GTID 文档:
- MySQL 半同步复制文档:
- MySQL YUM 仓库安装文档:
- Keepalived 配置文档:
- Redis Sentinel 官方文档:
- Redis Sentinel 客户端规范:
- Redis Active-Active 官方文档:
- Nginx upstream 模块文档:
- Nginx 反向代理文档:
- CentOS 官方 EOL 说明: