CentOS-高可用部署手册-MySQL双主RedisNginx

CentOS 高可用部署手册:MySQL 双主复制 + Keepalived、Redis、Nginx

1. 文档目标

这份文档的目标不是讲概念,而是让你按顺序执行,就能把下面三套组件搭起来:

  1. MySQL 双主复制(Master-Master Replication)+ Keepalived 单 VIP 漂移
  2. Redis 高可用
  3. 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 方案:

  1. 开源可落地方案:Redis 主从 + Sentinel
  2. 真双活方案:Redis Software / Enterprise Active-Active(如果你有安装包和授权)

2.3 Nginx:双活推荐"两台同时提供服务 + 双 VIP 互备"

Nginx 这里我给你的是可实际落地的"双活":

  • nginx01 持有 VIP-A
  • nginx02 持有 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.30192.168.10.33

如果你有 DNS:

  • api.example.com 可以同时解析到 192.168.10.30192.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 安装官方社区版

db01db02 执行:

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 登录文件

db01db02 创建:

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: Yes
  • Replica_SQL_Running: Yes
  • Last_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 检测脚本

db01db02 创建 /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 切成可写
  • 当前节点变成 BACKUPFAULT 时,把 MySQL 切成只读

db01db02 创建 /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 = OFF
  • super_read_only = OFF

db02 看:

sql 复制代码
SHOW VARIABLES LIKE 'read_only';
SHOW VARIABLES LIKE 'super_read_only';

预期:

  • read_only = ON
  • super_read_only = ON

6.6 业务连接方式

应用不要写死 db01db02 的物理 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.10
  • db02 获得 192.168.10.10
  • db02 自动切成可写

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 增强项:建议开启半同步复制

如果你希望降低切换时丢事务的风险,建议在两边开启半同步复制。

db01db02 执行:

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

所以我给你两套可执行内容:

  1. 开源方案:Redis 主从 + Sentinel
  2. 真双活方案:Redis Software / Enterprise Active-Active

如果你目前没有企业版安装包和授权,先按开源方案落地。

7.2 开源 Redis 高可用:主从 + Sentinel

这不是"真双活双写",但它是开源 Redis 官方推荐的非 Cluster 高可用方案。

7.2.1 安装 Redis

redis01redis02redis03 执行:

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:slaverole:replica

7.2.4 redis03 作为第三个 Sentinel 节点

redis03 可以不跑数据实例,只跑 Sentinel。

7.2.5 创建 Sentinel 配置

redis01redis02redis03 创建目录:

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

redis02bind 改成 192.168.10.22

redis03bind 改成 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 秒,再在 redis02redis03 上看:

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 基本步骤

  1. 准备 Redis Software 安装包和授权
  2. 在每个站点安装 Redis Software
  3. 分别建立两个集群
  4. 在集群间创建 Active-Active 数据库
  5. 让业务在每个地域连本地数据库端点
  6. 验证两地互相写入后数据收敛

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

分别完成两个独立集群的初始化:

  1. 设置集群名称
  2. 设置管理员账号
  3. 设置数据目录与持久化目录
  4. 确认管理端口可访问

7.3.4 创建 Active-Active 数据库

在任意一个集群的管理界面中:

  1. 进入 Databases
  2. 选择 Add database
  3. 选择 Active-Active
  4. 设置数据库名称,例如 aa-cache
  5. 设置每个参与集群的内存配额
  6. 选择参与的两个集群
  7. 创建数据库

创建完成后,每个集群都会得到自己的本地数据库端点和端口。

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.30
  • nginx02 持有 192.168.10.33
  • 两台机器都运行 Nginx
  • 两个 VIP 都对外提供服务
  • nginx01VIP-A 的主
  • nginx02VIP-B 的主
  • 任一节点挂掉,另一节点接管两个 VIP

这是真正常见、可落地、能同时承载流量的 Nginx 双活方案。

8.2 安装 Nginx 与 Keepalived

nginx01nginx02 执行:

bash 复制代码
dnf install -y nginx keepalived
systemctl enable nginx

8.3 配置 Nginx 反向代理

8.3.1 创建站点配置

nginx01nginx02/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 健康检查脚本

nginx01nginx02/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

nginx01nginx02 执行:

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 漂到 nginx02
  • 192.168.10.33 仍在 nginx02

此时 nginx02 同时接管两个 VIP。

8.8.3 恢复 nginx01

bash 复制代码
systemctl start nginx
systemctl restart keepalived

如果你想恢复后自动抢回,可以通过优先级和是否启用 nopreempt 再做细调;这里先按稳定优先处理。


9. 最终验收清单

你最后要确认以下结果全部成立。

9.1 MySQL

  • db01db02 都能 SHOW REPLICA STATUS\G
  • 正常情况下只有持有 VIP 的节点可写
  • 业务通过 192.168.10.10:3306 能持续写入
  • 停掉当前主节点 MySQL 后,VIP 能漂移到另一台
  • 漂移后另一台自动切成可写

9.2 Redis

  • redis01redis02 主从关系正常
  • 三个 Sentinel 都正常
  • 停掉当前 master 后,Sentinel 能选出新 master
  • 应用客户端通过 Sentinel 连接,不写死单节点

9.3 Nginx

  • 192.168.10.30192.168.10.33 都能提供服务
  • 两台 Nginx 都在正常转发流量
  • 任意一台 Nginx 宕掉后,另一台可接管两个 VIP

10. 推荐上线顺序

按下面顺序最稳:

  1. 先完成所有节点基础环境
  2. 先搭 MySQL,并把 VIP 漂移验证通过
  3. 再搭 Redis 主从 + Sentinel
  4. 最后搭 Nginx 双活
  5. 最后再让应用改为连 MySQL VIP、Redis Sentinel、Nginx VIP

11. 如果你下一步还要我继续做

这份文档已经可以直接照着搭。

如果你愿意,我下一步可以继续帮你补下面任意一种内容:

  1. 按你们真实 IP、网卡名、库名、域名,改成一份"你现场可直接复制"的定制版
  2. 再补一份"故障排查手册",专门写 SHOW REPLICA STATUS、Keepalived、Sentinel、Nginx 的异常处理
  3. 如果你有 Redis 企业版安装包,我单独补一份 Redis Active-Active 真双活专项实施手册

12. 官方参考资料

以下是本文编写时参考的官方资料,你后续排障时也可以直接对照:

相关推荐
vortex52 小时前
Shell 位置参数传递:从入门到“怀疑人生“
linux·bash·shell
阿图灵2 小时前
Linux常用基本命令(VI/VIM 编辑器)
linux·运维·服务器
承渊政道2 小时前
【MySQL数据库学习】(MySQL访问、连接池原理与简易网站数据流动)
数据库·学习·mysql·mysql访问·连接池原理
闪电悠米2 小时前
力扣hot100-438.找到字符串中所有字母异位词-固定长度滑动窗口详解
linux·服务器·数据结构·算法·leetcode·滑动窗口·力扣hot100
wefg14 小时前
【MySQL】索引(索引底层原理/创建/查看/删除主键、普通、联合、前缀、全文索引)
数据库·mysql
周杰伦的稻香12 小时前
MySQL8.0+中引入的SET_USER_ID权限迭代SUPER权限指定 DEFINER
数据库·mysql
风曦Kisaki13 小时前
#Linux数据库管理Day06:主从同步与MaxScale读写分离
linux·运维·数据库
小楼昨夜又东风12613 小时前
使用python快速拉包
linux
韩楚风14 小时前
【参天引擎】Cantian 服务端框架全景解析:进程架构、模块组成与交互关系
数据库·mysql·架构·cantian