1. 这套组合拳到底在防什么:先说清每一层承担的风险
业务量一旦上来,最怕的不是代码出 bug,而是值班半夜被叫醒——先是 Tomcat 服务挂了,然后发现负载均衡节点也断了,数据库前一条慢 SQL 就把连接池打满,监控面板还是黑的。几年前我接过一个内部项目,高峰期一千多并发就能把单台 Tomcat 压死在 90%,于是决定把整套架构从单点往高可用方向改造。改造方案就是今天要说的这套:HAProxy 做入口负载均衡,Keepalived 负责 VIP 漂移,后端 MariaDB 和 Tomcat 承担业务,Prometheus 抓指标、Grafana 出面板。
1.1 组件各自解决什么问题
很多人第一次看这套架构会觉得组件太多、很重,其实拆开来看,每一层都在处理一个真实存在的故障点:
- Keepalived:解决“入口 IP 挂了怎么办”。它通过 VRRP 协议让两台负载均衡器共享一个虚拟 IP,主节点宕机后虚拟 IP 会自动漂移到备用节点,客户端请求的地址不用改。
- HAProxy:解决“请求往哪台应用服务器转发”。它负责把到达入口的流量按一定调度策略分发到多台 Tomcat,同时做健康检查,发现某台 Tomcat 异常就自动摘除,恢复后再加回集群。
- MariaDB:提供统一的数据存储。多台 Tomcat 连接同一个数据库服务,避免应用层各自持有一份本地数据导致不一致。
- Prometheus + Grafana:解决“系统到底是不是真的高可用”。高可用不是配置完就算数,而是要让核心指标持续可见,进程挂了、节点失联、连接数异常都要能第一时间发现。
组件之间的流量路径大概是:用户访问 VIP 地址,请求先经 HAProxy 主节点,再转发到后端某台 Tomcat,Tomcat 执行业务逻辑后访问 MariaDB。Prometheus 从所有节点采集操作系统、中间件和数据库指标,Grafana 把这些指标以面板形式展示出来。
1.2 这套方案更适合应对哪些故障
我在实际规划时会先列一个故障矩阵,明确这套架构能覆盖哪些问题、不能覆盖哪些问题:
| 故障类型 | 这套架构的应对方式 | 恢复预期 |
|---|---|---|
| HAProxy 主节点宕机 | Keepalived 检测到进程或节点异常,VIP 漂移到备节点 | 2-5 秒 |
| 某台 Tomcat 崩溃或假死 | HAProxy 健康检查失败后自动摘除该节点 | 6-10 秒内摘除 |
| 后端 Tomcat 全部正常但数据库异常 | MariaDB 所在的单点问题仍存在,需要额外数据库高可用方案 | 不在本方案内 |
| 应用数据容量增长、慢查询增多 | 需配合 MySQL 慢查询监控、连接池调优解决 | 监控发现后再动作 |
| Prometheus 所在节点宕机 | 采集中断,不影响业务流量,但建议监控节点也做冗余 | 不影响业务 |
从这张表能看出来,HAProxy 和 Keepalived 承担的是接入层故障转移,Tomcat 集群解决应用层冗余,监控体系让故障能被动发现。整套方案适合中小规模 Web 应用、内部管理系统和有一定 SLA 要求的公共业务平台;如果数据库也是核心瓶颈,那还需要再叠加数据库主从或集群方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前先理顺概念:Keepalived 和 HAProxy 的高可用根本不在一个层面
网上有一个高频问题:HAProxy 和 Keepalived 有什么区别?甚至有人误以为两者都是负载均衡器,随便选一个就行。我在给团队讲架构时经常会打个比方:Keepalived 管的是“这个大门永远能用同一个门牌号找到”,HAProxy 管的是“进门之后把客户分给哪个柜台”。职责不一样,但缺一不可。
2.1 VRRP 做的是故障转移,不是流量分担
Keepalived 的底层是 VRRP(虚拟路由冗余协议)。两台服务器会组成一个虚拟路由器组,对外暴露同一个虚拟 IP。正常情况下虚拟 IP 绑定在主节点上,主节点每隔 1 秒(可配置)向备节点发送 VRRP 心跳报文;备节点一旦连续几个周期没有收到心跳,就认为主节点有问题,立即在自己的网卡上绑定这个虚拟 IP,继续对外提供服务。
这个动作就是常见的“VIP 漂移”。用户请求的 IP 始终是那个虚拟 IP,无论背后是主还是备,对客户端都完全透明。
2.2 HAProxy 是应用流量的分发器
HAProxy 运行在四层或七层,支持 TCP 和 HTTP 转发。同样是两台 HAProxy 节点,当 Keepalived 把 VIP 漂移到备节点后,备节点上的 HAProxy 必须已经提前启动、配置正确,并且能够正常连接后端 Tomcat。否则 VIP 虽然漂移过去了,请求还是进不来。
我给多个项目做过排查,最常见的翻车场景是:Keepalived 切换成功了,但备节点的 HAProxy 服务没有随系统开机自启,或者配置里还指向了旧的后端 IP。VIP 漂移后用户大量报错,这时候去看 VRRP 日志却一切正常。高可用的核心不是 IP 能漂,而是漂过去之后整条链路都是通的。
2.3 为什么两个负载均衡节点之间不需要做数据同步
HAProxy 本身是纯转发层,没有业务状态,配置只有一份文件,所以两台 LB 节点各放一份相同配置即可。Keepalived 的配置也是各写一份,只需要保证虚拟路由 ID、鉴权密码、VIP 等关键字段一致。
这里有一个经常会踩的坑:修改 HAProxy 配置后只在一台节点上执行了 systemctl reload haproxy,备节点还是旧配置。下次故障切换时备节点带着旧配置上线,可能把新加的后端全落在错误网络里。所以我一律要求改配置必须两台同步、两台 reload,并且用 haproxy -c -f /etc/haproxy/haproxy.cfg 先做配置校验。
3. MariaDB 与 Tomcat 部署前必须处理好的三件事
很多人一上来就配 Keepalived 和 HAProxy,却忽略后端节点的初始化。等集群搭好之后才发现,Tomcat 访问数据库失败、MariaDB 只监听了 127.0.0.1、应用没有任何可探测的健康检查路径。这些基础问题不处理,上层高可用做得再漂亮也是空中楼阁。
3.1 让 MariaDB 先走出 127.0.0.1 的默认坑
MariaDB 安装完成之后,默认监听地址通常是 127.0.0.1,只允许本机访问。如果你在多台 Tomcat 上部署同一个应用,这些 Tomcat 要访问数据库,就必须把监听地址改到对外网卡。很多人搜过“ubuntu 让 mariadb 不绑定 127.0.0.1”,问题基本都出在配置文件里的 bind-address。
在 MariaDB 配置文件 /etc/mysql/mariadb.conf.d/50-server.cnf 的 [mysqld] 段里,默认会有一行:
code复制bind-address = 127.0.0.1
将其改为:
code复制bind-address = 0.0.0.0
这里 0.0.0.0 表示监听本机所有 IP 地址。如果服务器上存在多网卡、出于安全考虑只想对外提供数据库服务,也可以写成具体的业务网卡 IP,比如 bind-address = 10.0.1.130。改完之后重启 MariaDB:
code复制systemctl restart mariadb
然后使用 ss -lntp | grep 3306 确认监听表现在是 0.0.0.0:3306 或指定 IP,而不是 127.0.0.1:3306。
注意:改完监听地址后防火墙也要同步放开 3306 端口。如果系统里开了 ufw,需要执行
ufw allow from 10.0.1.0/24 to any port 3306 proto tcp,只允许内网网段访问,不要直接对全互联网开放。
另外,不要用 root 账号给应用远程连接。建议单独创建业务账号:
code复制CREATE USER 'app_user'@'10.0.1.%' IDENTIFIED BY 'Strong_Passw0rd';
GRANT ALL PRIVILEGES ON appdb.* TO 'app_user'@'10.0.1.%';
FLUSH PRIVILEGES;
MariaDB 在部分发行版里还带着一个审计插件 server_audit,如果公司要求数据库操作留痕,可以在 [mariadb] 段启用:
code复制plugin_load_add = server_audit
server_audit_logging = ON
server_audit_output_type = file
server_audit_file_path = /var/log/mysql/server_audit.log
server_audit_events = CONNECT,QUERY
server_audit_file_rotate_size = 100000000
server_audit_file_rotations = 9
启用后所有连接和查询都会写入审计日志,能追溯到是哪个 Tomcat 节点执行了异常 SQL。
3.2 Tomcat 集群节点的基本准备
Tomcat 版本这里以 Tomcat 9 + OpenJDK 11 为例。每台 Tomcat 安装 JDK 后解压 Tomcat 到 /opt/tomcat,通过 server.xml 配置端口。集群中多个 Tomcat 可以保持默认 HTTP 端口一致,因为 HAProxy 是用 IP 区分节点,不会出现端口冲突。
安装完成后我建议先做一个最简单的健康检查页面。在 webapps/ROOT/ 下新建一个 health.html:
html复制<html>
<body>ok</body>
</html>
这个页面不依赖应用和数据库,专门给 HAProxy 探测用。它返回 HTTP 200 就代表 Tomcat 进程和容器基本正常。如果你希望健康检查能反映整个应用的存活状态,可以把这个检查路径指向一个只读接口,例如 /api/health,该接口内部检测数据库连接池和 JVM 状态;但要注意健康检查接口本身的代码必须非常稳定,不能因为一个依赖组件非核心报错就把整个节点误判为不可用。
Tomcat 的连接参数也需要提前调好。默认的 BIO/NIO 配置在小并发下没问题,但既然做集群,就说明流量不止一两百。我一般会在 server.xml 的 Connector 上调大 maxThreads 和 acceptCount:
xml复制<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="300"
acceptCount="200"
maxConnections="10000" />
JVM 内存另外在 bin/setenv.sh 里设置,比如 JAVA_OPTS="-Xms2g -Xmx2g"。核心原则是:让接入层的 HAProxy 能准确感知节点是否健康,让后端 Tomcat 不要因为连接池和线程数太小而成为瓶颈。
3.3 所有节点的时间同步与账号体系
高可用架构里时间不同步是个隐蔽问题。Keepalived 的心跳报文带有时间戳,如果两台 LB 节点时间差太大,会影响 VRRP 的状态判断;Prometheus 拉取指标时,如果节点时间漂移,时间序列数据就会出现错位。所以我要求所有参与架构的节点都配置 chrony 或 systemd-timesyncd 做时间同步。
各节点之间也需要统一的账号体系或密钥管理。比如监控用户要能读取 HAProxy stats socket、MariaDB 需要单独的监控账号,这些账号不要散落写在命令行历史里,建议用环境变量或统一的凭据管理方式维护。账号统一了,后续做自动化巡检和扩容也能省不少事。
4. HAProxy 配置拆解:从四层转发到七层健康检查
HAProxy 的配置可以写在 /etc/haproxy/haproxy.cfg,以 Ubuntu 上使用官方软件源安装的 HAProxy 2.6 为例。这部分的配置直接决定集群的用户体验,需要拆开逐段理解。
4.1 一段可直接落地的后端负载配置
ini复制global
log /dev/log local0
maxconn 5000
user haproxy
group haproxy
daemon
stats socket /run/haproxy/admin.sock mode 660 level admin
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
frontend http_in
bind *:80
default_backend tomcat_cluster
backend tomcat_cluster
balance roundrobin
option httpchk GET /health.html
http-check expect status 200
server web1 10.0.1.111:8080 check inter 3s fall 2 rise 2
server web2 10.0.1.112:8080 check inter 3s fall 2 rise 2
frontend http_in 是请求入口,监听服务器的 80 端口;所有流量统一分发给默认后端 tomcat_cluster。backend tomcat_cluster 定义了真实的 Tomcat 节点。
4.2 健康检查的间隔、失败次数和预期响应是核心
option httpchk GET /health.html 表示 HAProxy 会定期向 Tomcat 发起 HTTP GET 请求,只有返回状态码符合 http-check expect status 200,节点才被判定为可用。
后面的 check inter 3s fall 2 rise 2 参数含义分别是:
inter 3s:每 3 秒探测一次。fall 2:连续失败 2 次后,节点被标记为 DOWN,也就是最坏情况下 6 秒内摘除故障节点。rise 2:节点恢复后,连续成功 2 次才会重新加入集群。
这几个参数要按业务容忍度来调。如果服务很难接受 6 秒的失败窗口,可以把 inter 和 fall 调小,比如 inter 2s fall 2,但太频繁的探测会占用 Tomcat 线程资源。我一般不会把探测间隔调到 1 秒以内,生产上 2-3 秒已经足够。
4.3 调度算法怎么选:什么时候用 roundrobin,什么时候用 source
balance roundrobin 是默认轮询算法,每个新请求按顺序轮流分发给后端的各台 Tomcat,实现简单、负载基本均衡。但如果应用有会话状态,登录用户的 Session 需要固定在一台 Tomcat 上,轮询会让用户每次请求都跳到不同节点,导致 Session 频繁丢失。
解决会话保持通常有两条路线:
一是使用 balance source,按客户端源 IP 做哈希,同一个 IP 的请求固定分发到同一台 Tomcat:
ini复制backend tomcat_cluster
balance source
hash-type consistent
server web1 10.0.1.111:8080 check
server web2 10.0.1.112:8080 check
二是配置 Cookie 会话保持。后端需要能识别带有特定 Cookie 的请求并保持在同一节点,配置如下:
ini复制backend tomcat_cluster
balance roundrobin
cookie SERVER_COOKIE insert indirect nocache
server web1 10.0.1.111:8080 cookie web1 check
server web2 10.0.1.112:8080 cookie web2 check
两种方式各有取舍。基于源的会话保持不依赖 Cookie,但 NAT 后面多个用户共享同一出口 IP 时可能落到同一台节点,造成倾斜;Cookie 方式更精确,但要求应用能正常处理 Cookie。我如果做分布式 Session(如 Redis 统一存储),就直接用 roundrobin 轮询,不搞会话保持,让负载分配最均衡。
4.4 打开统计页面,运维才不用全靠黑屏猜
HAProxy 自带一个 stats 统计页面,用来查看每个后端的实时状态、会话数、流量量、健康检查失败次数,非常实用。在配置里增加:
ini复制frontend stats_page
bind *:8404
stats enable
stats uri /stats
stats realm HAProxy-Statistics
stats auth admin:ChangeMe_Strong
stats refresh 5s
访问 http://10.0.1.121:8404/stats 就能看到面板。我可以在这里直观看到 web1 或 web2 是 UP 还是 DOWN,以及每个节点的请求数。生产环境记得把 stats 的访问权限收紧,不要暴露到公网;如果公司有堡垒机或内网跳板,只允许内网 IP 访问。
5. Keepalived 配置与故障切换逻辑:不只是把 VIP 搬到另一台
Keepalived 节点配置的核心是 VRRP 实例。两台 LB 节点需要一份几乎一样的配置,只有节点角色和优先级不同。
5.1 一份 Keepalived 主节点配置实例
以两台 LB 节点为例,假设主节点是 10.0.1.121,备节点是 10.0.1.122,虚拟 IP 是 10.0.1.210,真实网卡是 ens192。主节点配置如下:
ini复制global_defs {
router_id LVS_MASTER
}
vrrp_script check_haproxy {
script "/usr/local/bin/check_haproxy.sh"
interval 2
weight -50
fall 2
rise 1
}
vrrp_instance VI_1 {
state MASTER
interface ens192
virtual_router_id 51
priority 150
advert_int 1
authentication {
auth_type PASS
auth_pass 9xTf4qPv
}
virtual_ipaddress {
10.0.1.210/24 dev ens192
}
track_script {
check_haproxy
}
}
备节点唯一的区别是 state 改为 BACKUP、priority 改为 130,其他参数保持一致。virtual_router_id 必须相同,否则两台节点无法组建同一个虚拟路由组。auth_pass 也有讲究,VRRP 认证密码实际只取前 8 位,超过 8 位在不同版本下的处理可能不一致,稳妥起见直接写 8 位字符串,两端完全一致。
5.2 为什么要单独写 HAProxy 存活检测脚本,而不是只靠 VRRP
这是很多人会忽略的关键点。如果只配置了 VRRP 虚拟 IP,Keepalived 默认只能感知“这台机器还活着”,无法感知 HAProxy 进程是否挂了。如果主节点的 HAProxy 因为 OOM 或配置错误意外退出,但服务器本身还活着,VRRP 心跳依然正常,备节点永远不会接管,VIP 还留在已经无法提供转发的节点上,整个入口就瘫痪了。
所以必须用 vrrp_script 周期检查 HAProxy 进程,当进程不健康时降低本节点优先级,触发 VIP 漂移。检测脚本内容:
bash复制#!/bin/bash
if [ "$(systemctl is-active haproxy)" = "active" ]; then
exit 0
else
exit 1
fi
脚本放在 /usr/local/bin/check_haproxy.sh 并赋予执行权限:
code复制chmod +x /usr/local/bin/check_haproxy.sh
每次检测间隔 2 秒,连续失败 2 次后,脚本会给当前节点的 VRRP 优先级减去 50。主节点原本优先级 150,减去 50 后变成 100,低于备节点的 130,所以备节点立即接管 VIP。这个“降级触发漂移”的设计比直接杀掉 Keepalived 更平滑,也能避免主节点恢复后出现来回抖动。
5.3 内核参数 net.ipv4.ip_nonlocal_bind 与开机绑定问题
配置 Keepalived 后,HAProxy 的 bind *:80 会监听所有本地地址,所以虚拟 IP 没绑到本机时 HAProxy 也能正常工作。如果 HAProxy 配置里显式声明了 bind 10.0.1.210:80,那就必须在 /etc/sysctl.conf 里设置:
code复制net.ipv4.ip_nonlocal_bind = 1
这个参数允许进程绑定当前机器上并不存在的 IP 地址。Linux 默认情况下,如果 HAProxy 在 VIP 尚未漂移到本机时先启动了,绑定非本地地址会失败并直接退出。多节点架构中,备节点在未接管 VIP 前就需要启动 HAProxy,因此这个内核参数必须提前配置好。
另一个常见问题是主节点恢复正常后 VIP 会“抢”回来。默认 Keepalived 的 MASTER 节点一旦恢复且优先级更高,会主动抢占 VIP。这样能保证主节点配置始终生效,但也会导致切换期间出现短暂中断。如果业务不希望频繁来回切换,可以把两个节点都设为 state BACKUP 并使用 nopreempt 模式,配合不同的 priority,让备份节点在主节点恢复后不主动抢回 VIP,直到下一次主节点故障或人工干预。
5.4 验证 Keepalived 是否正常的最快命令
配置完成后,在两台节点执行:
code复制systemctl restart keepalived
ip addr show dev ens192
正常情况下主节点网卡上能看到 inet 10.0.1.210/24 scope global ens192,备节点没有。再查看 VRRP 日志:
code复制journalctl -u keepalived -f
主节点日志会出现 Entering MASTER STATE,备节点日志出现 Entering BACKUP STATE。如果备节点也绑定了 VIP,说明 virtual_router_id 或优先级配置有问题,需要检查两端配置是否一致。
6. Prometheus + Grafana 接入:监控的目标是让“高可用”可见
没有监控的高可用架构就像没有仪表盘的飞机,切换是否成功、后端是否在服务、数据库连接是否过高,全靠登录服务器一条条敲命令。Prometheus 负责采集指标,Grafana 负责可视化,这一步做完才能说整套系统是闭环的。
6.1 采集器和端口的规划
Prometheus 本身不主动登录服务器,而是通过 exporter 暴露的 HTTP 端口定时拉取指标。参与本次架构的 exporter 和端口如下:
| 组件 | Exporter | 默认监听端口 | 部署节点 |
|---|---|---|---|
| 操作系统基础指标 | node_exporter | 9100 | 所有业务与数据库节点 |
| MariaDB 指标 | mysqld_exporter | 9104 | MariaDB 所在节点 |
| HAProxy 指标 | prometheus-haproxy-exporter | 9101 | 两台 LB 节点 |
这些 exporter 可以用发行版软件包安装,也可以直接下载官方二进制包放到 /usr/local/bin/。如果想省事,用 systemd 管理最稳妥,下面以 node_exporter 的 unit 为例:
ini复制[Unit]
Description=Node Exporter
After=network-online.target
[Service]
User=node_exporter
Group=node_exporter
Type=simple
Restart=always
ExecStart=/usr/local/bin/node_exporter --web.listen-address=:9100
[Install]
WantedBy=multi-user.target
把 unit 文件放到 /etc/systemd/system/node_exporter.service 后执行:
code复制systemctl daemon-reload
systemctl enable --now node_exporter
mysqld_exporter 需要先创建一个只读监控账号。在 MariaDB 里执行:
sql复制CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'Exporter_Password' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';
FLUSH PRIVILEGES;
然后通过环境变量文件启动 mysqld_exporter,避免监控密码出现在进程命令行里:
ini复制[Unit]
Description=MySQL Exporter
After=mariadb.service
[Service]
Type=simple
Restart=always
EnvironmentFile=/etc/mysqld_exporter/mysql_exporter.env
ExecStart=/usr/local/bin/mysqld_exporter --web.listen-address=:9104
[Install]
WantedBy=multi-user.target
环境变量文件 /etc/mysqld_exporter/mysql_exporter.env 内容为:
code复制DATA_SOURCE_NAME=exporter:Exporter_Password@unix(/run/mysqld/mysqld.sock)/
6.2 Prometheus 抓取配置示例
Prometheus 安装完成后,编辑 /opt/prometheus/prometheus.yml:
yaml复制global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'node'
static_configs:
- targets:
- 10.0.1.111:9100
- 10.0.1.112:9100
- 10.0.1.121:9100
- 10.0.1.122:9100
- 10.0.1.130:9100
- job_name: 'mysql'
static_configs:
- targets:
- 10.0.1.130:9104
- job_name: 'haproxy'
static_configs:
- targets:
- 10.0.1.121:9101
- 10.0.1.122:9101
配置中 scrape_interval 默认 15 秒抓取一次。如果做故障演练想看更平滑的曲线,可以缩短到 5 秒;但抓取频率越高,Prometheus 服务器和 exporter 的负载也越高。15 秒对常规高可用场景完全够用。
通过 systemd 管理 Prometheus:
ini复制[Unit]
Description=Prometheus Server
After=network-online.target
[Service]
User=prometheus
Restart=always
ExecStart=/opt/prometheus/prometheus \
--config.file=/opt/prometheus/prometheus.yml \
--storage.tsdb.path=/opt/prometheus/data
[Install]
WantedBy=multi-user.target
启动后访问 http://10.0.1.140:9090/targets,所有 exporter 对应 target 都应该是 UP 状态,如果某个 target 处于 DOWN,需要检查 exporter 进程和防火墙端口。
6.3 Grafana 数据源与常用 Dashboard 选择
Grafana 安装启动后,默认监听 3000 端口,第一次登录用 admin/admin,会强制要求修改密码。接下来要做两件事:添加 Prometheus 数据源和导入 Dashboard。
在 Grafana 左侧菜单进入 Data Sources,选择 Prometheus,URL 填 http://10.0.1.140:9090,保存并测试连接。这一步通不过,大概率是 Prometheus 地址写错、Grafana 所在节点无法访问 9090 端口,或 Prometheus 没启动成功。
Dashboard 不需要从零画,社区有大量可以复用的模板。我最常用的是 Node Exporter Full,ID 为 1860,这个模板覆盖 CPU、内存、磁盘 IO、网络流量和文件系统,基本能一眼看出节点是否异常。MariaDB 和 HAProxy 的模板可以直接在 Grafana 的 Dashboard 搜索框里输入关键词,比如 mysqld_exporter、haproxy,选择下载量和星级最高、说明里带 exporter 版本号的那套模板。注意模板中的查询语句依赖对应 exporter 的指标名,如果 Grafana 面板出现大量空数据,要看 Prometheus 中是否真的抓到了相应指标,必要时在 http://10.0.1.140:9090/graph 里手动查一下 haproxy_server_up{backend="tomcat_cluster"} 这类指标是否存在。
6.4 把核心告警接出来才算真的可用
有了 Grafana 面板,只是解决了“可观测”的问题。生产环境还应该配置告警,把异常事件主动推给值班人员。Prometheus 自带的 Alertmanager 可以配置邮件、企业微信或 Webhook 通知。我建议至少覆盖这几类告警:
- 节点失联:
up == 0,持续 1 分钟。 - VIP 漂移:HAProxy 节点的
node_network_flags变化较难直接判断,可以通过keepalivedexporter 或脚本把 VRRP 状态暴露为指标,当出现双主或长时间无主时立即告警。 - 后端节点被摘除:查询
haproxy_server_up{server!=""}等于 0 时告警,代表某台 Tomcat 已从负载池摘除,需要介入。 - MariaDB 连接数超过阈值:例如
max_connections使用率到达 80% 时提前告警。
告警规则文件可以在 Prometheus 配置里用 rule_files 引入,示例:
yaml复制groups:
- name: ha.alerts
rules:
- alert: NodeDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Instance {{ $labels.instance }} is down"
告警只是入口,收到告警之后还要能快速定位“业务影响面是哪层、哪个节点、什么资源”。Prometheus 数据里都有标签,比如 instance 标签直接显示 IP,便于确定是 HAProxy 节点、Tomcat 节点还是数据库节点出了问题。
7. 故障演练实测:不亲手“杀几次进程”,你永远不知道高可用是否成立
配置全部完成后,我从 CI 角度习惯性地给这套集群做一次故障演练。没有经过实测的“高可用”都是纸面高可用,因为现实中的故障可能是进程假死、端口占用、防火墙策略异常,而不只是干净的服务器宕机。
7.1 场景一:主 HAProxy 进程被 kill
在主节点执行:
code复制systemctl stop haproxy
同时观察备节点的 Keepalived 日志:
code复制journalctl -u keepalived -f
正常情况下备节点会在 2-5 秒内收到主节点 VRRP 心跳中断,并将 VIP 绑定到自己的 ens192 接口。验证命令:
code复制ip addr show dev ens192
curl -I http://10.0.1.210/
如果 VIP 已漂移到备节点,且备节点 HAProxy 健康,curl 仍然返回 HTTP 200,说明接入层故障转移成功。
这里我遇到过一种典型问题:主节点 HAProxy 被 kill 后,虚拟 IP 确实漂移了,但备节点 HAProxy 由于启动参数问题一直报错,VIP 过去后没有任何转发能力。所以每次做完切换演练,我习惯在备节点上直接访问一次 VIP 对应的业务,而不是只看 ip addr 里有没有 VIP。
7.2 场景二:Tomcat 节点进程被 kill
找一台 Tomcat,执行:
code复制kill -9 <tomcat_pid>
观察 HAProxy 统计页面,正常情况下 3 秒检测周期加 2 次失败判定会在约 6 秒后被标记为 DOWN。期间持续向 VIP 发起请求,能发现请求始终由仍存活的那台 Tomcat 响应,不会出现长时间 502。
这个演练还能暴露 HAProxy 配置里是否遗漏 check 参数。如果 server 行没有加 check,HAProxy 不会探测后端节点,即使 Tomcat 进程被杀,它依然会把流量转发过去,用户看到的就是连接失败。
Tomcat 恢复启动后,HAProxy 会在连续 2 次健康检查成功后自动把它加回负载池。这个“自动摘除、自动恢复”的过程就是应用层高可用的价值所在。
7.3 场景三:不要忽略数据库和会话状态的影响
后端 Tomcat 有多台,但所有 Tomcat 都连接同一个 MariaDB。如果你做的应用是无状态部署,所有 Session 都放在 Redis 或共享存储里,那这套架构很干净;如果 Session 保存在 Tomcat 本地内存,那即使 HAProxy 做了会话保持,Tomcat 节点宕机时用户依然会被踢出登录状态。这个必须在演练时一并验证。
数据库本身单点是这套架构里最容易忽略的隐患。我在做演练时会额外确认:如果 MariaDB 进程退出,所有 Tomcat 会出现大量 SQL 异常,HAProxy 和 Keepalived 却都显示正常。这并不代表方案本身有问题,而是说明如果业务对数据库的 SLA 要求很高,后端还需要叠加 MariaDB Galera Cluster 或主从复制方案,配合 HAProxy 的 TCP 四层健康检查对数据库节点做探测。HAProxy 和 Keepalived 解决的是接入口和应用层的高可用,数据库层的高可用必须单独设计、单独测试。
7.4 把演练结果记成文档
每次演练我除了截屏保存,还会记录具体恢复时间。例如某次实测:主 HAProxy 被 kill 后 VIP 漂移耗时约 2.8 秒,curl 整体无失败;Tomcat 节点被 kill 后,第 6 秒左右被摘除,期间无 5xx 错误,但第 4 到第 6 秒之间有两个请求恰好落到了故障节点上,返回了连接重置。这和配置中 inter 3s fall 2 的窗口一致。
如果想进一步缩小失败窗口,可以把 HAProxy 的 server 配置改为 check inter 2s fall 2 rise 2,失败摘除时间从 6 秒缩短到 4 秒。但探测过于频繁会占用 Tomcat 线程,所以核心业务最好做双活部署,让故障切换窗口内的失败请求由另一台节点兜住,这比无限调小探测间隔更有效。把这些量化结果和配置参数一起保存下来,后续扩容或调整时不需要重新猜一次。
我在这套部署上最大的体会是:高可用从来不是一个单独的工具或一套配置能彻底解决的,它是一套需要设计、验证和持续观测的组合方案。HAProxy 负责把流量分给还能干活的节点,Keepalived 负责让入口地址永不失效,MariaDB 和 Tomcat 是真正干活的底座,Prometheus 和 Grafana 则是让这一切不再靠猜的仪表盘。把这五部分装好并完整跑过一轮故障演练之后,再遇到半夜被叫醒的场景,你至少能对着监控面板快速判断出问题出在哪一层,而不是像一个无头苍蝇一样把所有服务器的日志从头翻到尾。
