
前言
在生产环境中,为了避免数据库访问入口出现单点故障,常采用 Keepalived + 双 Nginx + MySQL 双向复制 的架构。
整套架构实际上分为三层:
-
Keepalived: 负责 Nginx 代理入口的高可用,控制 VIP 归属。
-
Nginx: 负责 MySQL 的 TCP 连接转发,指定首选和备用数据库。
-
MySQL: 负责数据存储和双向复制。
本文将结合实际部署经验,详细介绍组件之间的工作机制、三个核心配置文件、故障切换过程,以及生产环境可能存在的风险。
安全说明: 所有服务器 IP 均使用 192.0.2.0/24 文档专用地址段,认证信息使用示例值,不包含真实生产环境 IP 和密码。
一、整体架构设计
1.1 服务器规划
| 角色 | 示例 IP | 软件 / 功能 |
|---|---|---|
| 代理节点 01 | 192.0.2.101 | Nginx + Keepalived |
| 代理节点 02 | 192.0.2.102 | Nginx + Keepalived |
| 统一入口 VIP | 192.0.2.100 | Keepalived 管理 |
| 数据库节点 01 | 192.0.2.103 | MySQL,首选后端 |
| 数据库节点 02 | 192.0.2.104 | MySQL,备用后端 |
本篇通用部署示例统一使用:
-
应用访问 Nginx:
13306 -
Nginx 访问 MySQL:
3306
注意端口差异: 原始环境的 Nginx 配置把两个数据库后端也写成了 13306,说明实际 TCP 目标端口并非默认的 3306。这可能是 MySQL 自定义监听端口,也可能是另一层数据库代理或端口映射,不能仅凭 Nginx 配置确定。本文为了方便新环境部署,使用后端 3306 的标准示例。实际部署必须以数据库真实监听端口为准。
1.2 整体架构拓扑
以下以 VIP 当前位于 Nginx 节点 02 为例:
+----------------------+
| Application / APP |
+----------------------+
|
v
VIP: 192.0.2.100:13306
|
v
Keepalived / VRRP
|
+--------------+--------------+
| |
| |
+-------v--------+ +--------v-------+
| Nginx Node-01 | | Nginx Node-02 |
| 192.0.2.101 | | 192.0.2.102 |
| BACKUP | | MASTER |
| No VIP | | VIP Owner |
+----------------+ +--------+-------+
|
v
Nginx Stream :13306
|
+-------------+-------------+
| |
v v
+----------------+ +----------------+
| MySQL 01 | | MySQL 02 |
| 192.0.2.103 | | 192.0.2.104 |
| Port: 3306 | | Port: 3306 |
| PRIMARY TARGET | | BACKUP TARGET |
+----------------+ +----------------+
^ ^
| |
+------ Replication --------+
拓扑解释:
-
应用统一连接
192.0.2.100:13306。 -
Keepalived 决定 VIP 当前归属哪个 Nginx 节点。
-
持有 VIP 的 Nginx 接收数据库 TCP 连接。
-
Nginx 正常情况下优先连接 MySQL 01。
-
首选数据库出现符合条件的 TCP 建连故障时,Nginx 可以尝试 MySQL 02。
-
两台 MySQL 通过双向复制同步数据。
图中的 MASTER/BACKUP 指的是 Keepalived 的 VRRP 角色;PRIMARY TARGET/BACKUP TARGET 指的是 Nginx 的代理路由角色,并不自动等同于 MySQL 的数据库主从身份。
二、为什么需要 Keepalived?
2.1 单台 Nginx 的问题
假设只有一台 Nginx:
Application
|
v
Nginx 192.0.2.101
|
v
MySQL Group
|
+------+------+
| |
MySQL 01 MySQL 02
这种架构可以实现 MySQL TCP 连接的首选和备用转发。
但如果 Nginx 所在服务器故障,即使两台 MySQL 都正常,应用仍可能无法连接数据库。
因此,单台 Nginx 存在代理入口单点故障。
2.2 双 Nginx + Keepalived 的改进
部署两台 Nginx,并通过 Keepalived 管理 VIP。
正常情况下只有一个节点持有 VIP,另一个节点等待接管。
当持有 VIP 的服务器停止正常参与 VRRP 时,备用节点可以通过选举接管 VIP。
2.3 VIP 故障漂移示意图
正常状态:
APP
|
v
VIP 192.0.2.100
|
v
Nginx-02 (MASTER)
|
v
MySQL-01 (Primary Target)
Nginx-01 = BACKUP / No VIP
代理节点 02 故障后:
APP
|
v
VIP 192.0.2.100
|
v
Nginx-01 (NEW MASTER)
|
v
MySQL-01 (Primary Target)
Nginx-02 = DOWN
可以看到:
-
VIP 地址没有变化。
-
应用无需修改数据库连接地址。
-
处理新连接的 Nginx 节点发生变化。
-
MySQL 的首选后端不因 VIP 漂移而自动改变。
已有 TCP 连接可能中断,需要由应用连接池重新建立连接。
三、三个最核心的配置文件
代理层核心配置只有三个:
| 文件 | 作用 |
|---|---|
/etc/keepalived/keepalived.conf |
控制 VIP 归属与 VRRP 选举 |
/app/nginx1.20/conf/nginx.conf |
Nginx 主配置,加载 Stream 模块配置 |
/app/nginx1.20/conf/tcp.d/mysql-tcp-13306.conf |
数据库 TCP 转发与主备后端选择 |
两个 Nginx 节点均需要这三个配置文件。
其中 Keepalived 的节点地址与优先级不同,Nginx 的代理配置通常保持一致。
四、Keepalived 配置详解
4.1 节点 01 配置
文件:
/etc/keepalived/keepalived.conf
global_defs {
router_id proxy-node-01
}
vrrp_instance VI_1 {
state BACKUP
interface ens192
virtual_router_id 42
priority 100
nopreempt
unicast_src_ip 192.0.2.101
unicast_peer {
192.0.2.102
}
advert_int 1
authentication {
auth_type PASS
auth_pass DEMOPASS
}
virtual_ipaddress {
192.0.2.100/24
}
}
4.2 节点 02 配置
global_defs {
router_id proxy-node-02
}
vrrp_instance VI_1 {
state BACKUP
interface ens192
virtual_router_id 42
priority 99
nopreempt
unicast_src_ip 192.0.2.102
unicast_peer {
192.0.2.101
}
advert_int 1
authentication {
auth_type PASS
auth_pass DEMOPASS
}
virtual_ipaddress {
192.0.2.100/24
}
}
DEMOPASS 是演示值,部署时需替换为符合环境要求的认证配置。VRRP PASS 认证不是强加密认证,仍需做好网络隔离与访问控制。
4.3 核心参数解释
| 参数 | 说明 |
|---|---|
state BACKUP |
节点启动时进入 BACKUP 初始状态 |
interface ens192 |
VIP 所绑定的网卡 |
virtual_router_id 42 |
VRRP 实例标识,两节点保持一致 |
priority 100/99 |
节点优先级 |
nopreempt |
禁止优先级高的节点主动抢占正常 MASTER |
unicast_src_ip |
本机 VRRP 通信源 IP |
unicast_peer |
对端 Keepalived 地址 |
advert_int 1 |
VRRP 通告间隔约为 1 秒 |
virtual_ipaddress |
配置需要漂移的 VIP |
router_id 是实例标识,并不是 VIP 选举使用的 virtual_router_id。
4.4 Keepalived 如何判断 VIP 何时漂移?
Keepalived 主要依靠 VRRP 通告判断当前 MASTER 是否仍正常参与选举。
正常情况下:
Nginx-02 / MASTER
|
| VRRP advertisement
| approximately every 1s
v
Nginx-01 / BACKUP
如果 BACKUP 持续收不到有效的 MASTER 通告,满足超时条件后,就会尝试接管 VIP。
以 advert_int=1、BACKUP 优先级 100 为例,标准 VRRP 主节点失联超时理论值约为:
3+\\frac{256-100}{256}\\approx3.61\\text{秒}
实际业务恢复时间还取决于网络邻居表更新、应用重连等因素。
4.5 为什么 priority=100 的节点可能没有 VIP?
因为启用了:
nopreempt
这意味着当优先级为 99 的节点已经正常成为 MASTER 时,优先级为 100 的节点不会仅因为优先级更高就主动抢占它。
但 nopreempt 并不意味着永远不会进行故障接管。
4.6 Keepalived 是否监控 Nginx?
这里必须重点说明:
本文参考的原始架构中,没有配置 Nginx 健康检查脚本。
因此:
-
Nginx 所在服务器宕机:通常可以通过 VRRP 超时触发接管。
-
Keepalived 停止正常参与 VRRP:备用节点通常可以接管。
-
只有 Nginx 进程停止:Keepalived 不会仅凭现有配置直接感知该故障。
后续若希望 Nginx 进程故障也触发 VIP 接管,需要另外设计 vrrp_script、track_script 等健康检查联动机制,并在测试环境验证。
五、Nginx 主配置文件
5.1 配置路径
/app/nginx1.20/conf/nginx.conf
两台服务器通常使用一致的 Nginx 主配置。
以下是精简后的通用参考配置:
worker_processes auto;
worker_rlimit_nofile 20960;
error_log logs/error.log crit;
events {
worker_connections 4096;
multi_accept on;
accept_mutex on;
accept_mutex_delay 500ms;
}
http {
server_tokens off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
access_log off;
include mime.types;
default_type application/octet-stream;
keepalive_timeout 300;
client_max_body_size 10240M;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 6;
gzip_types text/plain text/css
text/javascript application/json
application/javascript application/xml;
include /app/nginx1.20/conf/http.d/*.conf;
}
stream {
log_format proxy
'$remote_addr [$time_local] '
'$protocol $status '
'$bytes_sent $bytes_received '
'$session_time "$upstream_addr" '
'"$upstream_bytes_sent" '
'"$upstream_bytes_received" '
'"$upstream_connect_time"';
access_log
/app/nginx1.20/logs/tcp-access.log proxy;
include /app/nginx1.20/conf/tcp.d/*.conf;
}
client_max_body_size 等参数沿用了原架构设计思路,实际部署应根据业务需求和安全基线重新评估,不必机械照搬。
5.2 HTTP 和 Stream 的区别
http 主要处理:
-
Web 应用。
-
HTTP/HTTPS 反向代理。
-
基于 HTTP 协议的负载均衡。
stream 主要处理:
-
TCP 代理。
-
UDP 代理。
-
MySQL 等非 HTTP 协议连接。
MySQL 使用 TCP 协议,因此需要使用 stream 进行透明 TCP 转发。
5.3 最关键的是 include
stream {
include /app/nginx1.20/conf/tcp.d/*.conf;
}
这表示 Nginx 将 tcp.d 目录下符合条件的 .conf 文件作为 Stream 配置加载。
MySQL 首选后端、备用后端、代理监听端口等,都在这个目录下配置。
六、Nginx MySQL 主备代理配置
6.1 核心配置
文件:
/app/nginx1.20/conf/tcp.d/mysql-tcp-13306.conf
upstream mysql_backend {
server 192.0.2.103:3306
weight=5
max_fails=2
fail_timeout=2s;
server 192.0.2.104:3306
weight=5
backup;
}
server {
listen 13306 so_keepalive=on;
proxy_connect_timeout 8s;
proxy_timeout 300s;
proxy_pass mysql_backend;
}
这是标准 MySQL 后端 3306 的示例。
6.2 端口转发关系
APP
|
| TCP :13306
v
Nginx
|
| proxy_pass mysql_backend
|
+---- MySQL-01 192.0.2.103:3306
|
+---- MySQL-02 192.0.2.104:3306
(backup only)
这里有两个不同概念:
-
listen 13306:Nginx 对应用开放的端口。 -
server IP:3306:Nginx 连接后端 MySQL 时使用的端口。
前端端口和后端端口不要求相同。
如果数据库实际监听 13306,则需要把 upstream 中的 3306 替换为真实后端端口。
6.3 Nginx 怎么知道哪个 MySQL 是首选?
关键就在:
server 192.0.2.103:3306 ...;
server 192.0.2.104:3306 backup;
backup 是备用后端标记。
正常情况下,MySQL 01 作为普通后端参与调度,MySQL 02 不参与正常新连接分配。
因此即使两个节点都配置 weight=5,也不会因为权重相同而在正常情况下各分到 50% 连接。
6.4 Nginx 如何判断 MySQL 01 不可用?
关键参数:
max_fails=2
fail_timeout=2s
proxy_connect_timeout 8s
| 参数 | 功能 |
|---|---|
max_fails=2 |
达到指定失败次数时临时标记后端不可用 |
fail_timeout=2s |
失败统计窗口及临时不可用时长 |
proxy_connect_timeout=8s |
TCP 后端建连超时 |
backup |
将第二个数据库设为备用 |
当前使用的是被动故障检测。
Nginx 不会主动登录 MySQL,也不会定期执行 SQL 判断复制状态。
它根据真实业务连接过程中发生的 TCP 建连错误和超时记录失败。
6.5 首选 MySQL 故障切换流程
正常状态:
APP
|
v
Nginx
|
v
MySQL-01 :3306
|
+-- TCP CONNECTED
首选后端 TCP 建连失败:
APP
|
v
Nginx
|
+----X----> MySQL-01 :3306
| CONNECT FAILED
|
+---------> MySQL-02 :3306
BACKUP ATTEMPT
特别注意:由于 Stream 的 proxy_next_upstream 默认启用,某次连接 MySQL 01 失败时,就可以尝试 MySQL 02。
不一定需要先累计两次失败。
max_fails=2 的作用是临时不可用判定,而不是每次请求切换所必需的失败次数。
没有配置 upstream zone 时,Nginx 多个 Worker 的失败状态也可能不是统一共享的。
6.6 重要限制
Nginx 判断的是 TCP 建连能力。
它不能仅凭当前配置判断:
-
MySQL 是否允许写入。
-
SQL 是否执行正常。
-
数据库复制是否中断。
-
MySQL 主从角色是否变化。
-
两台数据库的数据是否已经同步。
因此,该配置实现的是 TCP 连接主备代理,而不是数据库层面的自动主库选举。
七、MySQL 双向复制与单写模式
7.1 双向复制架构
+------------------+ +------------------+
| MySQL-01 | | MySQL-02 |
| 192.0.2.103:3306 |<---->| 192.0.2.104:3306 |
+------------------+ +------------------+
Replication in both directions
双向复制说明两个数据库之间具有相互复制关系,但不意味着已经实现自动主库仲裁。
7.2 为什么需要单写?
两台 MySQL 即使都允许写入,也可能因为同时修改相同数据而产生复制冲突。
因此,在采用单写设计的业务系统中,需要确保正常业务写入集中到被认可的写入节点。
Nginx 可以实现正常情况下的单目标代理:
Normal:
APP -> Nginx -> MySQL-01
TCP failover:
APP -> Nginx -> MySQL-02
但这只是代理路由规则。
7.3 为什么不能单靠 Nginx 保证严格单写?
考虑以下故障恢复场景:
-
MySQL 01 故障,Nginx 开始将部分新连接发往 MySQL 02。
-
MySQL 02 上建立了业务连接。
-
MySQL 01 恢复 TCP 连接能力。
-
Nginx 再次把部分新连接优先发送到 MySQL 01。
-
MySQL 02 上的旧连接可能仍未结束。
此时如果两个数据库都允许写入,就可能同时存在向不同数据库写入的业务会话。
因此还需要数据库层面的写入约束、故障隔离、数据一致性检查和切换管理。
八、生产环境故障场景
| 故障场景 | 处理组件 | 结果 |
|---|---|---|
| 当前持有 VIP 的服务器宕机 | Keepalived | 备用节点通常尝试接管 VIP |
| Keepalived 无法继续发送有效 VRRP 通告 | Keepalived | 可能触发主备切换 |
| Nginx 进程单独停止 | 当前架构未配置直接检测 | VIP 不一定漂移 |
| MySQL 01 TCP 建连失败 | Nginx | 可以尝试 MySQL 02 |
| MySQL 01 SQL 逻辑执行失败 | 数据库 / 应用 | Nginx 不会仅凭 SQL 错误切换 |
| MySQL 01 恢复 TCP 服务 | Nginx | 新连接可能重新优先访问 MySQL 01 |
| MySQL 双向复制中断 | 数据库监控与运维机制 | Nginx 不负责复制恢复 |
九、生产环境常用只读检查命令
9.1 查看 VIP 当前在哪个节点
分别在两台 Nginx 上执行:
ip -br addr
确认 VIP 只绑定在预期节点。
9.2 查看 Keepalived 状态
systemctl status keepalived --no-pager
9.3 查看 VRRP 切换日志
journalctl -u keepalived --no-pager \
| grep -Ei 'MASTER|BACKUP|FAULT|Entering'
9.4 查看 Nginx 进程
ps -ef | grep '[n]ginx'
9.5 查看代理端口
ss -lntp | grep 13306
9.6 查看 Nginx TCP 代理日志
tail -n 50 /app/nginx1.20/logs/tcp-access.log
日志中的 $upstream_addr 可以帮助判断已记录的 TCP 会话究竟连接到哪个后端。
9.7 查看数据库实际监听端口
在数据库服务器上:
ss -lntp | grep -E ':3306|:13306'
也可以通过已经建立的 MySQL 管理连接执行:
SHOW VARIABLES LIKE 'port';
必须以实际监听端口为准,不可仅凭 MySQL 默认端口推断现网端口。
十、从零部署时的检查顺序
推荐按以下顺序搭建:
-
准备两台代理服务器,确认网络和软件版本。
-
安装支持 Stream 模块的 Nginx。
-
部署两台 Nginx 的主配置和数据库 TCP 代理配置。
-
确认后端 MySQL 实际监听端口,以及数据库复制和写入角色机制。
-
在新环境校验 Nginx 配置。
-
部署两台 Keepalived,设置 VRRP 单播对端、优先级和 VIP。
-
校验 VRRP 通信与 VIP 归属。
-
通过测试客户端验证数据库连接流向。
-
在测试环境演练入口故障和数据库连接失败。
-
确认应用重连、故障恢复、数据库单写和复制一致性机制。
在新部署环境中,可以使用:
/app/nginx1.20/sbin/nginx -t \
-c /app/nginx1.20/conf/nginx.conf
校验 Nginx 配置。
但在已有生产环境中,应遵循变更流程,不应随意通过停止服务模拟故障。
十一、常见问题
Q1:为什么已经有 Nginx,还需要 Keepalived?
因为普通 Nginx 本身不管理 VRRP VIP 漂移。Keepalived 为代理入口提供节点级故障接管能力。
Q2:两台 Nginx 是否必须组成双活集群?
不需要。本文采用主备模式,正常情况下只有一个节点处理通过 VIP 进入的连接。
Q3:两台 Nginx 配置为什么一样?
这样无论 VIP 在哪个节点,后端 MySQL 调度规则都保持一致。
Q4:没有 VIP 的那台 Nginx 是否停止工作?
不是。它仍然运行,只是不处理通过这个 VIP 进入的新连接;通过真实 IP 直接访问的业务仍可能由它处理。
Q5:MySQL 默认是 3306,为什么 Nginx 使用 13306?
因为 Nginx 对客户端提供的代理端口可以独立设置。应用访问 Nginx 的 13306,Nginx 可以转发到 MySQL 的 3306。若后端也配置为 13306,则以该后端实际服务端口为准。
Q6:Nginx 如何知道哪个数据库是主库?
它并不会自动识别 MySQL 主库,而是按照 upstream 中的普通后端和 backup 标记选择 TCP 连接目标。
Q7:MySQL 01 恢复后,Nginx 是否会自动重新使用它?
后续新连接可能重新优先选择 MySQL 01,但原先连接到 MySQL 02 的会话不会自动迁移,数据库写入安全需要独立保证。
十二、总结
这套架构的核心可以用三句话概括:
Keepalived 管入口: 决定 VIP 当前位于哪个 Nginx 节点。
Nginx 管连接: 按照 upstream 中的 backup 和失败判定规则,决定新连接使用哪个 MySQL 后端。
MySQL 管数据: 双向复制负责数据同步,但写入角色仲裁和数据一致性保障需要单独设计。
最重要的是明确两类切换:
-
VIP 漂移: 发生在 Nginx 代理入口层,由 Keepalived 负责。
-
数据库连接转发: 发生在 MySQL 代理层,由 Nginx Stream 负责。
这两层高可用相互独立。即使全部配置正常,也不能将 TCP 自动重连等同于数据库无损切换。
未来部署类似架构时,首先保存 Keepalived 主配置、Nginx 主配置和 MySQL Stream 子配置,再结合数据库复制管理、应用重连和生产网络条件完成整体高可用设计。