这个框架的最大价值是防跑偏。 工单 A 里团队花了大量时间在第 3 层,而故障在第 2 层,只要在第 2 层抓一次包就能定位。反过来也有大量案例是报文完全正常、第 4 层的交换机 ARP 老化没更新。 八、实战:配置一个正确的双主 + 健康检查环境下面直接给一份可用于生产的双主配置,然后在解读里说明每一行的作用和常见改法。 8.1 拓扑与前置检查
前置检查(改配置之前必须跑完,这些是后面的基线):
风险提示:如果第 2 步发现 VIP 已被占用,不要直接启动 Keepalived。两个设备同时持有同一个 IP 会立刻造成 IP 冲突和 ARP 抖动,影响面可能超出你自己的系统。必须先定位占用者并处理。 8.2 完整配置:
|
| 退出码 | Keepalived 的判定 |
|---|---|
| 0 | 成功。weight 为负时优先级不变;weight 为 0 时无动作 |
| 1 | 失败。按 fall/rise 计数,达到阈值后应用 weight 或进 FAULT |
| 2 | 不改变状态(部分版本支持)。用于"无法判断"的场景,例如依赖服务暂时不可达 |
| 其他(如 127) | 行为不确定,不要依赖。命令不存在、权限不足时 shell 会返回 126/127 |
输出约定:脚本的 stdout/stderr 会被 Keepalived 记录到日志。所以脚本应该打印可诊断的简短信息,但不要打印大量内容 —— 每 2 秒执行一次、大量输出会迅速填满日志。
下面是 check_haproxy.sh 的完整实现,涵盖常见的写错点。
#!/usr/bin/env bash
# /etc/keepalived/scripts/check_haproxy.sh
# 用途:检查本机 HAProxy 是否存活且能接受连接
# 依赖:curl(HTTP 方式)、pgrep(进程方式)
# 退出码:0=健康 1=不健康 2=无法判断(不改状态)
set -uo pipefail
# 说明:这里刻意不加 -e。-e 会在 pgrep 返回 1(没找到进程,正是我们要判断的情况)
# 时直接退出整个脚本,导致脚本提前结束、退出码语义混乱。
readonly LOG_TAG="chk_haproxy"
readonly HA_SOCK="/var/run/haproxy/admin.sock" # HAProxy 2.x 的默认 stats socket
readonly HEALTH_URL="http://127.0.0.1:8404/healthz"
log() {
# 只输出一行摘要,避免刷日志。优先级用 syslog 风格前缀便于 grep
printf '%s %s\n' "$LOG_TAG" "$*"
}
# ---------- 检查 1:进程是否存在 ----------
if ! pgrep -x haproxy >/dev/null 2>&1; then
log "FAIL: haproxy process not found"
exit 1
fi
# ---------- 检查 2:stats socket 是否可查(最可靠,能拿到真实状态)----------
if command -v socat >/dev/null 2>&1 && [[ -S "$HA_SOCK" ]]; then
# 用 echo 管道到 socat,避免在脚本里处理交互式输入
ha_state="$(printf 'show stat\n' | timeout 2 socat - "UNIX-CONNECT:${HA_SOCK}" 2>/dev/null \
| awk -F, '$1=="backend" && $2=="bk_app" {print $18; exit}')"
# 第 18 列是 status 字段;字段位置随 HAProxy 版本可能变化,先验证再上线:
# printf 'show stat\n' | socat - UNIX-CONNECT:$HA_SOCK | head -2
case "${ha_state}" in
UP) log "OK: backend bk_app UP"; exit 0 ;;
DOWN|MAINT|NOLB)
log "FAIL: backend bk_app state=${ha_state}"; exit 1 ;;
"") log "WARN: cannot parse backend state, falling through" ;;
*) log "WARN: unknown backend state=${ha_state}, falling through" ;;
esac
fi
# ---------- 检查 3:HTTP 端点兜底 ----------
if command -v curl >/dev/null 2>&1; then
# -s 静默 -f HTTP 非 2xx 返回错误 -m 超时 -o /dev/null 丢弃 body
if curl -sfm 2 -o /dev/null "$HEALTH_URL"; then
log "OK: ${HEALTH_URL} reachable"
exit 0
fi
log "FAIL: ${HEALTH_URL} unreachable"
exit 1
fi
# 走到这里说明既没有 socat 也没有 curl —— 属于环境缺陷,不是服务故障
log "WARN: neither socat nor curl available, cannot verify"
exit 2
这段脚本里每一个设计选择都值得说明:
set -uo pipefail 而不加 -e:加了 -e 会让 pgrep 的"未找到"直接把脚本干掉,退出码变成 1 但日志里什么都不会打,排查时看不到原因。这是 H8 级别的高频错误。timeout 2:所有外部命令都要限时。Keepalived 的 timeout 参数超时后会 SIGKILL 整个脚本,但 SIGKILL 不会给你清理机会;自己加 timeout 能得到更干净的行为和更好的日志。command -v 而不是 which:which 在某些最小化系统上不存在,而且它对别名和函数的处理不可靠。check_backend.sh(检查后端可达性,注意它只应该影响 VIP 的策略要慎重):
#!/usr/bin/env bash
# /etc/keepalived/scripts/check_backend.sh
# 用途:采样检查后端 real server 的可用比例
# 注意:本脚本用于"后端大面积不可达时让本机退出",
# 不要用它判断单个后端 —— 单个后端故障应该由 LVS real_server 检查处理,
# 否则会造成 VIP 在两点间来回漂移(见 6.4)
# 退出码:0=健康 1=不健康 2=无法判断
set -uo pipefail
readonly BACKENDS=("10.x.x.21" "10.x.x.22" "10.x.x.23")
readonly PORT=80
readonly MIN_HEALTHY=2 # 至少 2 个后端健康才算本机健康
healthy=0
for ip in "${BACKENDS[@]}"; do
# /dev/tcp 需要 bash(不是 sh)。用 -w 指定连接超时
if timeout 2 bash -c "echo > /dev/tcp/${ip}/${PORT}" 2>/dev/null; then
healthy=$((healthy + 1))
fi
done
printf 'chk_backend %d/%d healthy\n' "$healthy" "${#BACKENDS[@]}"
if (( healthy >= MIN_HEALTHY )); then
exit 0
fi
exit 1
脚本权限与安全(Keepalived 2.x 有专门的安全机制,配错会让脚本静默不执行):
# 1. 属主和权限:必须 root 拥有,且不能对其他用户可写
sudo install -o root -g root -m 0755 /dev/null /etc/keepalived/scripts/check_haproxy.sh
sudo chown root:root /etc/keepalived/scripts/check_haproxy.sh
sudo chmod 0755 /etc/keepalived/scripts/check_haproxy.sh
# 为什么要这么严:Keepalived 以 root 执行这些脚本。
# 如果脚本对其他用户可写,等于给了任何本地用户一个 root 提权通道。
# 2. 校验 Keepalived 是否接受了脚本(2.x 会检查属主/权限/路径)
sudo journalctl -u keepalived -n 50 | grep -i -E 'script|security'
# 异常表现:出现 "Cannot find script" 或
# "Script is not secure (owned by ...)" / "path is not secure"
# 判断依据:2.x 开启 enable_script_security 后,
# 脚本所在目录链上的每一级都不能是全局可写的(/tmp 下的脚本必被拒)
# 3. 手工验证脚本的三种退出码都符合预期(配完必做)
sudo -u root /etc/keepalived/scripts/check_haproxy.sh ; echo "healthy-case exit=$?"
# 预期:exit=0,且输出一行 "chk_haproxy OK: ..."
sudo systemctl stop haproxy
sudo -u root /etc/keepalived/scripts/check_haproxy.sh ; echo "failed-case exit=$?"
# 预期:exit=1,且输出一行 "chk_haproxy FAIL: ..."
sudo systemctl start haproxy
风险提示:在生产环境上手工停 haproxy 验证脚本会真实影响业务。正确做法是在变更窗口内、且先确认对端能正常接管;或者在一个与生产隔离的演练环境上验证脚本逻辑。如果是主备架构,可以只在一个节点上停服务,切换后立即恢复并确认状态。
notify_* 脚本是排查时最有价值的观测点,但也很容易被写坏。几个硬要求:
#!/usr/bin/env bash
# /etc/keepalived/scripts/notify.sh
# 参数:$1 = 状态(master|backup|fault)
# Keepalived 会按 type 传不同数量的参数,不要假设参数个数固定
set -uo pipefail
readonly STATE="${1:-unknown}"
readonly TS="$(date -Is)"
readonly LOGFILE="/var/log/keepalived/notify.log"
# 关键:目录必须先存在,否则重定向失败、通知静默丢弃
[[ -d "$(dirname "$LOGFILE")" ]] || mkdir -p "$(dirname "$LOGFILE")"
# Keepalived 传进来的完整参数全部记录下来,便于事后还原现场
{
printf '%s state=%s args=%s\n' "$TS" "$STATE" "$*"
} >> "$LOGFILE"
# 【重要】通知脚本必须快速返回。默认 timeout 是 60 秒(可配),
# 但通知脚本里面做重活会推迟状态迁移,造成"日志有记录但 VIP 迟迟不到位"。
# 所以:只做记录和异步投递,不要在里面 curl 外部接口、不要做健康检查。
exit 0
三个必须注意的点:
$LOGFILE 所在目录要先存在。 未创建目录时 shell 重定向失败,脚本可能返回非 0,而这个失败不会让 Keepalived 报警,只会让通知静默消失。这是"演练时发现通知没发出来"的常见原因。script_timeout 或按脚本设置控制。默认值较大(60s),在需要快速迁移的场景要显式调小。Keepalived 的日志是排查的第一手材料,但它默认的日志量和信息量都不理想:要么太吵,要么正好缺了你需要的那一行。
默认情况下 Keepalived 以 -D 才输出详细日志,而系统服务单元文件通常不带这个选项。查看当前实际用的启动参数:
# 查看服务单元里实际传了什么参数
systemctl cat keepalived | grep -E 'ExecStart|ExecReload'
# 预期输出形如:
# ExecStart=/usr/sbin/keepalived $KEEPALIVED_OPTIONS
# ExecReload=/bin/kill -HUP $MAINPID
# 注意:很多发行版通过 /etc/sysconfig/keepalived 或 /etc/default/keepalived 传参
cat /etc/sysconfig/keepalived 2>/dev/null || cat /etc/default/keepalived 2>/dev/null
# 预期:KEEPALIVED_OPTIONS="-D" 或含 -D
开启详细日志的方法(用覆盖文件,不要直接改发行版自带的单元文件,升级会被覆盖):
sudo systemctl edit keepalived
# 在打开的编辑器里写入(注意 systemd 的覆盖语法需要 [Service] 段):
# [Service]
# Environment="KEEPALIVED_OPTIONS=-D -l"
# 说明:-D 详细日志 -l 日志同时输出到 syslog 的 LOG_LOCAL0(不同发行版配置不同)
sudo systemctl daemon-reload
sudo systemctl restart keepalived
风险提示:restart Keepalived 会触发一次真实的状态迁移——进程退出时如果配了 notify_*,会执行 fault 通知;重新启动后实例从 BACKUP 开始重新选举。在双主架构下,这通常意味着 VIP 会在两点间搬一次。必须在变更窗口执行,并提前确认对端健康。 更温和的替代方案是用 reload:
# reload 不中断 VRRP 实例,只重新读取配置(部分配置项 reload 不生效,见 10.3)
sudo systemctl reload keepalived
# 或者直接发 SIGHUP
sudo kill -HUP "$(cat /run/keepalived.pid)"
Keepalived 的日志里有四类关键事件。用 journalctl 配合 grep 提取,比翻全文高效得多:
# 1. 状态迁移(最重要)
sudo journalctl -u keepalived --since "2 hours ago" | grep -E 'Entering (MASTER|BACKUP|FAULT) STATE'
# 预期输出示例:
# VRRP_Instance(VI_100) Entering MASTER STATE
# VRRP_Instance(VI_101) Entering BACKUP STATE
# 判断依据:
# - 短期内在同一实例上反复出现 MASTER/BACKUP 交替 → 抖动,见故障案例 B
# - 一直只有 BACKUP 没有任何 MASTER → 从来没抢到过主,看优先级和报文
# 2. 优先级变化(健康检查是否真的生效)
sudo journalctl -u keepalived --since "2 hours ago" | grep -E 'effective priority|priority'
# 预期输出示例:
# VRRP_Instance(VI_100) Changing effective priority from 150 to 120
# 判断依据:
# - 完全没有这一行,但脚本失败日志有 → 权重方向配反了
# - 变化后仍然高于对端 → 权重不够大,不会触发切换
# 3. 健康检查结果
sudo journalctl -u keepalived --since "2 hours ago" | grep -E 'VRRP_Script'
# 预期输出示例:
# VRRP_Script(chk_haproxy) succeeded
# VRRP_Script(chk_backend) failed
# 判断依据:如果连 "VRRP_Script" 都搜不到,说明脚本根本没被调度,
# 去查 script 段是否被解析(--config-test)和权限是否被拒
# 4. VRRP 报文相关(收包、丢弃、校验失败)
sudo journalctl -u keepalived --since "2 hours ago" | grep -iE 'VRRP_Packet|packet|checksum|digest|Received'
# 预期/异常:
# "VRRP_Instance(VI_100) Received advert with higher priority" → 正常竞争
# "VRRP_Instance(VI_100) Dropping received VRRP packet" → 报文被丢弃,看原因
# "invalid checksum" / "invalid TTL" → 报文被中间设备改写或跨了三层
按时间对齐两个节点的日志,这是排查 VRRP 问题的关键动作,因为 VRRP 是两方协议:
# 在两个节点上分别导出同一时间窗的日志,然后并排比较
# 前提:两节点时间同步(timedatectl 确认过)
ssh lb1 "sudo journalctl -u keepalived --since '2026-04-02 03:00' --until '2026-04-02 03:10' -o short-iso" > /tmp/lb1.log
ssh lb2 "sudo journalctl -u keepalived --since '2026-04-02 03:00' --until '2026-04-02 03:10' -o short-iso" > /tmp/lb2.log
# -o short-iso 让时间戳是 ISO 格式,便于排序和 diff
# 把两个文件按时间戳合并排序,人工查看事件交错
sort -k1,2 /tmp/lb1.log /tmp/lb2.log | grep -E 'Entering|priority|VRRP_Script' | head -40
这一步能直接暴露"两方对同一件事的描述不一致" ——例如 lb1 说自己进了 FAULT,lb2 却说收到了 lb1 的通告。那说明有第三方在冒充 lb1 发通告(见故障案例 B 的根因)。
Keepalived 本身可以从多个维度暴露状态。三种方式,按可靠性排序:
方式一(推荐):从通知脚本写 textfile,由 node_exporter 暴露。 这是最可靠的,因为它是"事件驱动",不依赖状态查询的采样时机。
#!/usr/bin/env bash
# /etc/keepalived/scripts/notify.sh (指标部分)
# 追加到 8.5 的 notify.sh 里
set -uo pipefail
readonly STATE="${1:-unknown}"
readonly TEXTFILE="/var/lib/node_exporter/textfile/keepalived_state.prom"
readonly TMPFILE="$(mktemp "${TEXTFILE}.XXXXXX")"
# 说明:先写临时文件再原子 mv,避免 node_exporter 读到写了一半的文件
# 注意 tmp 文件必须和目标在同一文件系统,否则 mv 不是原子的
# 状态映射成数值,便于告警表达式直接比较
case "$STATE" in
master) code=2 ;;
backup) code=1 ;;
fault) code=0 ;;
*) code=-1 ;;
esac
{
printf '# HELP keepalived_vrrp_state VRRP instance state (0=fault 1=backup 2=master -1=unknown)\n'
printf '# TYPE keepalived_vrrp_state gauge\n'
printf 'keepalived_vrrp_state{instance="VI_100"} %s\n' "$code"
printf 'keepalived_vrrp_state{instance="VI_101"} %s\n' "$code"
} > "$TMPFILE"
# 权限:node_exporter 以 node_exporter 用户运行,要能读到
chmod 0644 "$TMPFILE"
mv -f "$TMPFILE" "$TEXTFILE"
exit 0
注意这段是简化示意:真实环境里 notify_master 只会在 VI_100 变 master 时调用,无法同时写出 VI_101 的状态,所以指标会在两个实例之间互相覆盖。正确做法有两种:
VI_100 用 notify_master_100),各自只写自己的指标。方式二:用检查脚本周期性写指标。 直接让一个 interval 10 的脚本查询当前 VIP 归属并写指标:
#!/usr/bin/env bash
# /etc/keepalived/scripts/export_state.sh
# 不由 VRRP 的 notify 驱动,而是独立 timer 或 interval 调度
# 采集当前机器上实际持有的 VIP,这是"结果导向"的指标,比状态更可信
set -uo pipefail
TEXTFILE="/var/lib/node_exporter/textfile/keepalived_vip.prom"
TMPFILE="$(mktemp "${TEXTFILE}.XXXXXX")"
{
printf '# HELP keepalived_vip_held Whether this node currently holds the VIP\n'
printf '# TYPE keepalived_vip_held gauge\n'
for vip in 10.x.x.100 10.x.x.101; do
if ip -o -4 addr show | grep -qw "$vip"; then
printf 'keepalived_vip_held{vip="%s"} 1\n' "$vip"
else
printf 'keepalived_vip_held{vip="%s"} 0\n' "$vip"
fi
done
} > "$TMPFILE"
chmod 0644 "$TMPFILE"
mv -f "$TMPFILE" "$TEXTFILE"
这个指标比状态指标更有价值,原因是它反映事实(网卡上到底有没有这个 IP),而不是 Keepalived 的自我认知。脑裂时两个节点都会报 "held=1",告警立刻能抓到;而状态指标在脑裂时两边都说自己是 MASTER,单看一台机器的指标看不出异常。
node_exporter 启动时必须带 textfile collector 并指向该目录:
# 检查 node_exporter 是否已启用 textfile collector
ps aux | grep node_exporter | tr ',' '\n' | grep -E 'textfile|collector'
# 预期:能看到 --collector.textfile.directory=/var/lib/node_exporter/textfile
# 异常:没有该参数 → 指标不会被暴露,需要修改启动参数
# 验证指标确实暴露了
curl -s http://127.0.0.1:9100/metrics | grep keepalived_
# 预期:keepalived_vip_held{vip="10.x.x.100"} 1
# keepalived_vip_held{vip="10.x.x.101"} 0
# 异常:无输出 → 目录权限不对,或 node_exporter 未配置 textfile collector
ls -la /var/lib/node_exporter/textfile/
# 判断依据:文件属主/权限必须让 node_exporter 用户可读
方式三:直接抓 ip addr 用 blackbox 或自写 exporter。 灵活性最高,但需要额外维护。仅在方式一、二不能满足时使用。
下面这些规则直接对应本文讨论的故障类型。阈值是起点,必须结合你的业务基线和切换实测耗时调整:
# /etc/prometheus/rules/keepalived.yml
groups:
- name: keepalived
rules:
# ---- 脑裂:最高优先级告警 ----
# 同一 VIP 被多个节点持有。用 sum by (vip) 判断,出现 2 或以上即为脑裂
- alert: KeepalivedSplitBrain
expr: sum by (vip) (keepalived_vip_held) > 1
for: 10s
labels:
severity: critical
annotations:
summary: "疑似脑裂:VIP {{ $labels.vip }} 被多个节点同时持有"
runbook: "立即执行:两端 ip -br addr 确认;抓包看是否双向 VRRP 通告;检查是否有多余节点使用同一 VRID"
# for: 10s 而不是 5m。脑裂影响面大且每一秒都在产生异常连接,
# 宁可偶发误报也要快速发现。误报通常来自采集抖动,可配合后续规则确认。
# ---- VIP 完全无人持有 ----
# 所有节点都不持有该 VIP 才是异常。注意要有至少一个节点有人持有
- alert: KeepalivedVipMissing
expr: |
(sum by (vip) (keepalived_vip_held) == 0)
and on(vip) (count by (vip) (up{job="node"} == 1) >= 2)
for: 30s
labels:
severity: critical
annotations:
summary: "VIP {{ $labels.vip }} 没有任何节点持有"
runbook: "查两端 journalctl -u keepalived | grep -E 'FAULT|priority';常见原因是两端都进了 FAULT"
# ---- 状态抖动 ----
# 10 分钟内状态变化超过 5 次视为抖动。keepalived_vrrp_state 需要是事件累加的,
# 用 changes() 统计序列变化次数(要求指标是周期性采集的,不能用事件覆盖式)
- alert: KeepalivedFlapping
expr: changes(keepalived_vrrp_state[10m]) > 5
for: 0m
labels:
severity: warning
annotations:
summary: "VRRP 实例 {{ $labels.instance }} 在 10 分钟内切换超过 5 次"
runbook: "查健康检查是否在临界点抖动(fall/rise 配得太小);查网络是否丢包"
# ---- 长时间处于 BACKUP ----
# 如果某个实例应该持主却长期 BACKUP,说明选举结果不符合设计
- alert: KeepalivedStuckBackup
expr: max_over_time(keepalived_vrrp_state{instance="VI_100"}[30m]) == 1
for: 5m
labels:
severity: warning
annotations:
summary: "VI_100 已持续 30 分钟处于 BACKUP,与预期不符"
runbook: "确认是否符合 nopreempt 的预期;检查优先级算术和 track_script"
# ---- 健康检查失败 ----
- alert: KeepalivedScriptFailed
expr: keepalived_script_ok == 0
for: 3m
labels:
severity: warning
annotations:
summary: "健康检查脚本 {{ $labels.script }} 持续失败"
runbook: "手工执行脚本确认退出码;确认 weight 方向;确认 VIP 是否已按预期迁移"
# ---- 处于 FAULT ----
- alert: KeepalivedInFault
expr: keepalived_vrrp_state == 0
for: 1m
labels:
severity: critical
annotations:
summary: "VRRP 实例 {{ $labels.instance }} 处于 FAULT 状态"
runbook: "FAULT 表示实例主动放弃服务。查 track_interface 的网卡是否 down、track_script 是否返回 1"
关于 keepalived_script_ok 指标:它不是 Keepalived 原生暴露的,需要你在健康检查脚本里顺手写 textfile(和 export_state.sh 同样的做法)。如果你不采集这个指标,KeepalivedScriptFailed 这条规则会被 no data 状态困住——Prometheus 的 no data 不等于健康,这条规则的实际覆盖要靠 absent() 补充,或者干脆依赖 KeepalivedStuckBackup 来兜底。
ip / arp 命令做现场确认指标和日志会滞后或撒谎,ip 命令不会。任何怀疑时刻都应该直接确认现场:
# 当前持有哪些 VIP —— 这是唯一可信的事实来源
ip -o -4 addr show | awk '{print $2, $4, $5}'
# 输出示例:eth0 10.x.x.11/24 brd ... 和 eth0 10.x.x.100/24 ...
# 判断依据:VIP 是否真的在任何网卡的地址列表里。没有就是没有,不要相信日志
# 确认 VIP 的网卡与 scope
ip addr show dev eth0 | grep -A2 '10.x.x.100'
# 预期:inet 10.x.x.100/24 scope global secondary eth0
# 异常:scope host → 配成了 /32 且没有 dev 声明,ARP 不会应答,上游设备学不到 MAC
# 查看本机 ARP / 邻居表(确认能学到对端)
ip neigh show dev eth0 | grep -E '10.x.x.1[12]'
# 预期:10.x.x.12 dev eth0 lladdr xx:xx:xx:xx:xx:xx REACHABLE
# 异常:FAILED / INCOMPLETE → 二层不通,VRRP 就不可能正常
# 确认 VIP 对应的 MAC 是否为本机期望的网卡
ip link show eth0 | grep link/ether
# 预期:切主后,上游交换机上 VIP 应指向这个 MAC(需要在交换机侧确认)
# 检查内核是否还有残留的 VIP(进程被 kill -9 时可能出现)
ip addr show | grep -c '10.x.x.100'
# 预期:0 或 1(取决于本机是否持有)
# 异常:某个节点上有 VIP 但 Keepalived 进程已挂 → 残留地址,
# 会导致下次启动时出现 "IP address already exists" 并可能双持
# 残留处理【高风险操作,必须先确认没有其他节点持有同一 VIP】
# 影响范围:删除本机网卡上的 VIP,依赖该 VIP 的本机连接会立刻中断
# 前置检查:确认对端确实持有该 VIP(否则会造成服务完全不可用)
sudo ip addr del 10.x.x.100/24 dev eth0
# 验证:ip -br addr show eth0 | grep 10.x.x.100 → 无输出
# 回滚:sudo ip addr add 10.x.x.100/24 dev eth0 (需要 root,立即生效)
下面每个案例都按同一套闭环走:现象 → 初判 → 检查 → 指标 → 根因 → 修复 → 验证 → 回滚预案 → 复盘。案例来自真实工单,细节做了脱敏。
现象:lb1 上的 HAProxy 进程崩溃,被 systemd 反复拉起又崩溃。lb2 一直处于 BACKUP,VIP 10.x.x.100 始终在 lb1 上。业务通过 lb1 全部失败,中断约 40 分钟直至人工介入。lb1 的 Keepalived 日志里没有任何健康检查失败的记录,只有启动时的一行 Entering MASTER STATE。
初判:VIP 不漂移有两种可能:健康检查没触发,或者触发了但选举没完成。既然日志里连"检查失败"都没有,先查第一类。
检查过程(按四层顺序走):
# ---- 第 1 层:状态机与配置 ----
sudo journalctl -u keepalived --since "2 hours ago" | grep -E 'Entering|VRRP_Script|priority'
输出只有三行,且都是很久以前的:
VRRP_Instance(VI_100) Entering MASTER STATE
VRRP_Script(chk_haproxy) succeeded
关键判断:chk_haproxy 的最后一条记录是 succeeded,此后再无任何输出。但脚本的 interval 是 2 秒,正常情况下日志里应该有大量 succeeded 记录(Keepalived 每次执行都会记一行)。沉默本身就是异常信号。
# 既然怀疑脚本没有被调度,直接搜启动阶段的脚本相关日志
sudo journalctl -u keepalived -b | grep -iE 'script|permission|security|cannot|disabl'
这一步直接给出了根因:
Keepalived_vrrp[4821]: VRRP_Script(chk_haproxy) cannot be executed: Permission denied
Keepalived_vrrp[4821]: VRRP_Script(chk_haproxy) removing script from instance
判断依据:Keepalived 在启动阶段尝试加载脚本时发现它不可执行,于是把这个脚本从实例上移除,并且在之后的整个生命周期里不再尝试执行它。日志只打这一次,所以定期查看日志的人会完全错过。
# 确认脚本权限,找到为什么不可执行
sudo ls -la /etc/keepalived/scripts/
-rw-r--r--. 1 root root 1284 Apr 1 03:12 check_haproxy.sh ← 缺少执行位
-rwxr-xr-x. 1 root root 978 Apr 1 03:12 check_backend.sh
根因:check_haproxy.sh 没有执行位(0644 而不是 0755)。追溯变更记录发现,这台机器在前一周做过一次从备份恢复的操作,恢复脚本用的是 tar -xzf 且没有带 -p 参数(--preserve-permissions),导致所有文件的权限位被重置为 umask 的结果。check_backend.sh 因为恢复后有人手工 chmod 过而幸免。
根因成立的关键证据链(缺一条都不能定论):
| 证据 | 命令 | 本次取值 |
|---|---|---|
| 脚本在启动时被拒绝 | journalctl -u keepalived -b | grep -i script |
cannot be executed: Permission denied |
| 脚本文件确实没有执行位 | ls -la /etc/keepalived/scripts/ |
-rw-r--r-- |
| 检查确实没被调度 | 日志中 VRRP_Script(chk_haproxy) 记录的时间间隔 |
最后一条在两天前,之后全无 |
| 状态机的行为与"检查未生效"一致 | lb1 优先级日志 |
始终 150,从未出现过 Changing effective priority |
修复:
# 第一步:恢复执行位
sudo chown root:root /etc/keepalived/scripts/check_haproxy.sh
sudo chmod 0755 /etc/keepalived/scripts/check_haproxy.sh
# 第二步:手工验证脚本行为符合预期(三种退出码都要试)
sudo -u root /etc/keepalived/scripts/check_haproxy.sh ; echo "healthy exit=$?"
# 预期:exit=0 且输出一行 OK
一个必须说清楚的行为差异:修改文件权限不会让 Keepalived 重新加载脚本。原因就是上面的日志——脚本在启动阶段已被"从实例上移除"。所以仅仅 chmod 是不够的,必须让 Keepalived 重新读取配置:
# 先校验配置,再重启
sudo keepalived --config-test -f /etc/keepalived/keepalived.conf ; echo "config-test exit=$?"
# 预期:exit=0
# 【高风险操作】重启会触发一次真实的状态迁移
# 前置检查:
# 1) 确认 lb2 健康且能接管
ssh lb2 "systemctl is-active keepalived haproxy && ip -br addr show eth0 | grep -c 10.x.x.100"
# 2) 备份当前配置与脚本目录
sudo cp -a /etc/keepalived/keepalived.conf /backup/keepalived/keepalived.conf.$(date +%Y%m%d-%H%M)
sudo tar czf /backup/keepalived/scripts.$(date +%Y%m%d-%H%M).tar.gz -C /etc/keepalived scripts
# 影响范围:lb1 持有的 10.x.x.100 会短暂中断并[漂移到 lb2](https://yunpan.plus/t/1916-1-1)
# 执行窗口:业务低峰期。权限:root 或受限 sudo
sudo systemctl restart keepalived
# 确认脚本这次真的被加载了
sudo journalctl -u keepalived -b --since "1 min ago" | grep -iE 'VRRP_Script|script'
# 预期:不再有 "cannot be executed";随后每 2 秒出现一条 VRRP_Script(chk_haproxy) succeeded
验证(这一步必须做,否则不能确认修复有效):
# 1. 确认脚本在被周期调度(等 10 秒看是否有多条记录)
sleep 10
sudo journalctl -u keepalived --since "1 min ago" | grep -c 'VRRP_Script(chk_haproxy)'
# 预期:≥ 4(interval 2 秒,10 秒内约 5 次)
# 异常:0 → 脚本仍未被调度,回去看启动日志
# 2. 演练真实切换
sudo systemctl stop haproxy
sleep 10
sudo journalctl -u keepalived --since "1 min ago" | grep -E 'effective priority|Entering'
# 预期:
# VRRP_Script(chk_haproxy) failed
# VRRP_Instance(VI_100) Changing effective priority from 150 to 90
# VRRP_Instance(VI_100) Entering BACKUP STATE
ip -br addr show eth0 | grep 10.x.x.100 ; echo "lb1-exit=$?"
# 预期:lb1-exit=1(VIP 已离开 lb1)
ssh lb2 "ip -br addr show eth0 | grep 10.x.x.100"
# 预期:lb2 上持有 VIP
# 3. 流量侧验证(不要只验证 VIP,要验证跨网段真的通)
curl -sf -o /dev/null -w '%{http_code}\n' --max-time 5 http://10.x.x.100/healthz
# 预期:200
# 4. 恢复
sudo systemctl start haproxy
# 配了 nopreempt,VIP 不会自动切回,这是预期行为
修复过程中发现的第二个缺陷(防火墙丢弃协议 112):
在验证阶段抓包时发现了另一个问题:
# 演练期间在 lb1 上抓包,本意是确认通告正常发出
sudo timeout 10 tcpdump -i eth0 -n -e vrrp
结果:只在 lb1 上抓到了 lb1 自己发的通告,而同一时刻在 lb2 上抓不到任何 VRRP 报文。这说明 lb2 从来没收到过 lb1 的通告。
# 在 lb2 上确认是否有规则丢弃 VRRP
sudo firewall-cmd --list-rich-rules | grep -i vrrp
sudo nft list ruleset 2>/dev/null | grep -i -B2 -A2 vrrp
输出显示加固脚本加入的规则:
chain input {
type filter hook input priority filter; policy drop;
...
ip protocol vrrp drop
}
为什么这个缺陷在修复前没暴露出来:lb2 收不到通告时,它的 Master Down Timer(约 3.4 秒)会超时,然后自己升主——那会表现为脑裂而不是静默。之所以没发生,是因为防火墙规则是在上次 lb1 重启之后才加的,而 lb2 在那之后的日志里已经出现过一次 Entering MASTER STATE(说明它确实误升过主),随后又被 lb1 的通告压回 BACKUP——因为 lb1 的出向通告没被拦。也就是说,这个缺陷当时已经造成过几秒钟的 VIP 双持,只是没被监控抓到。
这是本次排查里最有价值的一个发现:它印证了 9.3 节的观点——状态指标会撒谎,keepalived_vip_held 这种结果导向的指标才抓得住这类问题。
修复(防火墙部分):
# 前置检查:导出当前完整规则集作为回滚依据
sudo firewall-cmd --list-all > /backup/firewalld/all-$(date +%Y%m%d-%H%M).txt
sudo nft list ruleset > /backup/nft/ruleset-$(date +%Y%m%d-%H%M).nft
# 记录现有业务端口,reload 后逐项对比
sudo firewall-cmd --list-ports ; sudo firewall-cmd --list-services
# 在两台机器上放行 VRRP,只允许对端 IP(最小权限,不要对全网开放)
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.x.x.12/32" protocol value="vrrp" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.x.x.11/32" protocol value="vrrp" accept'
sudo firewall-cmd --reload
# 生效验证
sudo firewall-cmd --list-rich-rules
如果环境用的是裸 nftables(加固脚本直接写 nft):
# 注意 nft 的 add rule 默认追加到链尾,而链的默认策略是 drop,
# 所以必须用 insert 插到 drop 之前,或者显式指定位置
sudo nft insert rule inet filter input ip saddr 10.x.x.11 ip protocol vrrp accept
sudo nft insert rule inet filter input ip saddr 10.x.x.12 ip protocol vrrp accept
# 验证插入位置正确(必须出现在 drop 之前)
sudo nft list chain inet filter input
# 【持久化】运行时 nft 规则重启即丢,必须写进 /etc/nftables.conf
sudo nft list ruleset | sudo tee /etc/nftables.conf >/dev/null
sudo systemctl enable --now nftables
验证(防火墙部分):
# 在 lb2 上抓包,确认能收到 lb1 的通告
sudo timeout 10 tcpdump -i eth0 -n -e vrrp
# 预期:能看到 10.x.x.11 > 224.0.0.18: VRRPv2, Advertisement, vrid 100, prio 150
# 关键验证:确认不再出现"误升主"
sudo journalctl -u keepalived --since "10 min ago" | grep -c 'Entering MASTER STATE'
# 预期:0(稳定期间不应有任何升主动作)
# 异常:> 0 → 通告仍不完整,继续查
回滚预案:
sudo chmod 0644 /etc/keepalived/scripts/check_haproxy.sh。注意回滚会让检查重新被禁用,等于回到故障状态,所以这个回滚只应在"新权限导致脚本行为异常"时使用,而不是常规回退手段。sudo firewall-cmd --permanent --remove-rich-rule='...' && sudo firewall-cmd --reload。nftables 环境回灌备份:sudo nft -f /backup/nft/ruleset-<时间戳>.nft。lb2 上做(它是备节点),确认抓包正常后再做 lb1。两台不要同时改。复盘:
# 需要有日志采集(如 promtail/loki 或自写 exporter)把关键字转成指标
increase(keepalived_script_load_failure_total[10m]) > 0
keepalived_vip_held(第 9.3 节)。故障期间它本可以立刻暴露 VIP 双持。-p,或者用配置管理工具重新下发文件而不是裸 tar 解包。同时在恢复后加一步权限校验。cannot be executed / removing script,有则告警。这条规则的价值极高,因为它是唯一能在"检查静默失效"时立刻发现的机制。现象:监控在凌晨 2 点连续告警:10.x.x.100 出现 IP 冲突,且 ARP 对应 MAC 每隔几秒变化一次。业务侧表现为偶发 502、部分长连接被重置。
初判:ARP 抖动 + 同 VIP 双持 = 脑裂。但要先确认是"两个节点都认为自己是主"还是"有一个不该存在的第三方"。
检查过程:
# ---- 第 1 层:两端各自的状态 ----
for h in lb1 lb2; do
echo "===== $h ====="
ssh "$h" "sudo journalctl -u keepalived -n 20 | grep -E 'Entering|priority'"
ssh "$h" "ip -br addr show eth0 | grep 10.x.x.100"
done
结果:两端都显示 Entering MASTER STATE,两端都持有 10.x.x.100。脑裂成立。
# ---- 第 2 层:抓包看通告来源 ----
sudo timeout 8 tcpdump -i eth0 -n -e vrrp
输出里有三段不同的源地址:
10.x.x.11 > 224.0.0.18: VRRPv2, Advertisement, vrid 100, prio 150
10.x.x.12 > 224.0.0.18: VRRPv2, Advertisement, vrid 100, prio 100
10.x.x.13 > 224.0.0.18: VRRPv2, Advertisement, vrid 100, prio 150
10.x.x.13 —— 这是一台不在 Keepalived 拓扑里的机器。它也用 vrid 100 且优先级 150,与 lb1 打平(按 IP 大小,13 大于 11,所以 13 认为它应该赢)。
# 确认 10.x.x.13 是什么
ping -c 1 -W 1 10.x.x.13
nmap -sn 10.x.x.13 2>/dev/null || echo "nmap 不可用,用 arping 替代"
sudo arping -I eth0 -c 2 10.x.x.13
# 判断依据:能响应说明这台机器活着。接着要查它是谁
# 通过 ARP 表拿到 MAC,再从交换机侧定位接入端口
ip neigh show 10.x.x.13
根因:这是一台开发人员自己搭的测试机,用于验证一套负载均衡方案,直接从生产环境克隆了 keepalived.conf,没有改 virtual_router_id 也没有改 router_id。它开机自启了 Keepalived,于是成了一个"幽灵节点",用同一个 VRID 参与选举,优先级还和生产主节点打平。
日志证据:lb1 的日志里有大量这样的记录:
VRRP_Instance(VI_100) Received advert with lower priority 100
VRRP_Instance(VI_100) Received advert with higher priority 150 ← 来自幽灵节点
为什么 lb1 会频繁让位:当 lb1 收到优先级 150(与它自己相同)的通告时,按 VRRP 规则比较主 IP,10.x.x.13 > 10.x.x.11,所以 lb1 会退让;但 10.x.x.13 收到 lb1 的通告时也会做同样的比较并把 lb1 视为对等……两边都在"让位",然后发现没人接管又都升回主。这就是 ARP 抖动的来源。
修复:
# 第一步【止血】:隔离幽灵节点,不是先改配置。
# 在接入交换机侧关闭该端口(最快的止血手段),或者在幽灵节点上停掉 keepalived
# ---- 在 10.x.x.13 上(如果能拿到它的登录权限)----
sudo systemctl stop keepalived
sudo systemctl disable keepalived
# 影响范围:仅该测试机,无业务影响
# 如果拿不到权限 → 在交换机上 shutdown 对应端口,联系安全团队处理
# 第二步:确认生产两端恢复稳定
ssh lb1 "sudo journalctl -u keepalived -f" | grep -E 'Entering|higher priority'
# 预期:幽灵节点消失后,不再出现 "Received advert with higher priority"
验证:
# 1. 通告源只有两个
sudo timeout 8 tcpdump -i eth0 -n vrrp | awk '{print $3}' | sort -u
# 预期:只有 10.x.x.11 和 10.x.x.12
# 2. VIP 单持
for h in lb1 lb2; do echo "$h: $(ssh $h "ip -br addr show eth0 | grep -c 10.x.x.100")"; done
# 预期:一个为 1,另一个为 0(合计为 1)
回滚预案:本案例的修复是"停止一个不该运行的进程",没有配置回滚问题。但隔离动作本身有风险——如果误判,停掉的是某个真正需要的节点,会造成 VIP 完全无人持有。所以隔离前必须完成"确认它不在拓扑清单里"这一步(查 CMDB、查机器归属),不能只凭 IP 不认识就停。
复盘:
# 需要自定义 exporter 或 textfile 上报"通告源数量"
# 期望值为拓扑中的节点数(本例为 2)
keepalived_advert_source_count != 2
virtual_router_id 分配清单:把 VRID 和 router_id 纳入 CMDB 统一分配,避免冲突;并在配置管理里加校验(router_id 必须唯一)。priority 拉开差距来防脑裂。把优先级改成 200 只解决了与幽灵节点的竞争,解决不了 VRID 冲突本身。VRID 冲突会导致更严重的问题:两个不同业务的 VIP 用同一 VRID 时,认证密码相同就会互相干扰,这是很多"莫名其妙的切换"的隐藏原因。现象:运维把 chk_haproxy 的 weight 从 -30 改成 -60,执行 systemctl reload keepalived,观察日志确认 reload 成功。之后做故障演练,停掉 haproxy,但 VIP 依然不漂移。优先级日志显示:
VRRP_Instance(VI_100) Changing effective priority from 150 to 120
优先级只降了 30,不是 60 —— 配置没生效。
初判:reload 是否真的重新读取了 vrrp_script 的 weight?这需要看 Keepalived 对 SIGHUP 的处理逻辑。
检查过程:
# 1. 确认磁盘上的配置确实是新值
sudo grep -A6 'vrrp_script chk_haproxy' /etc/keepalived/keepalived.conf
# 预期:weight -60
# 如果这里是 -30 → 改的是另一个文件(多实例部署时常见),或者有 include 覆盖
# 2. 确认配置文件语法正确
sudo keepalived --config-test -f /etc/keepalived/keepalived.conf ; echo "exit=$?"
# 预期:exit=0
# 3. 确认进程实际加载的是哪份配置
sudo cat /proc/"$(pgrep -o keepalived)"/cmdline | tr '\0' ' ' ; echo
# 预期:能看到 -f /etc/keepalived/keepalived.conf
# 异常:fi 指向另一路径(例如 /etc/keepalived/keepalived.conf.d/ 下的合并文件)
# 4. 关键:确认 reload 是否重新读取了脚本定义
sudo journalctl -u keepalived | grep -iE 'reload|Reloading|SIGHUP|re-reading'
# 输出示例:
# Reloading Keepalived configuration file...
# 注意:有这一行不代表所有配置项都被重新应用
根因:Keepalived 收到 SIGHUP 后会重新解析配置文件,但部分配置项只在启动时生效,不会在 reload 时重新应用。vrrp_script 的 weight 就是其中之一(它影响的是实例的跟踪结构,属于初始化时建立的绑定关系)。
判断依据(可复现的验证方法):对比 reload 前后进程的启动时间与配置文件修改时间:
# 配置文件修改时间
stat -c '%y %n' /etc/keepalived/keepalived.conf
# 进程启动时间(注意 MainPID 才是主进程)
systemctl show keepalived -p MainPID -p ExecMainStartTimestamp
# 判断依据:如果配置修改时间晚于进程启动时间,而行为仍是旧值 →
# reload 没有应用这项配置,必须 restart
修复:
# 必须用 restart,不能用 reload
# 前置检查(重要):
# 1. 确认对端健康(restart 会触发本机退出选举,VIP 会漂到对端)
ssh lb2 "systemctl is-active keepalived haproxy && ip -br addr show eth0 | grep -E '10.x.x.10[01]'"
# 2. 确认现在是业务低峰
# 3. 备份当前配置(回滚依据)
sudo cp -a /etc/keepalived/keepalived.conf /backup/keepalived/keepalived.conf.$(date +%Y%m%d-%H%M)
# 执行(在变更窗口内,一端一端做,不要同时重启两端)
sudo systemctl restart keepalived
sudo journalctl -u keepalived -f
# 预期日志:
# VRRP_Instance(VI_100) Entering BACKUP STATE (重启后先从 BACKUP 起)
# VRRP_Instance(VI_100) Entering MASTER STATE (如果本机优先级更高)
风险提示:restart 的影响范围是本机持有的全部 VIP 都会短暂中断并漂移到对端。在双主架构下,两台机器各持一个 VIP,重启其中一台会让它持有的那个 VIP 漂到对端(对端变成双持),此时对端承担全部流量。所以必须确认对端有足够的容量承接,否则会造成对端过载。这是"高可用切换反而引发雪崩"的典型场景。
验证:
# 演练一次,确认新权重生效
sudo systemctl stop haproxy
sleep 6
sudo journalctl -u keepalived -n 30 | grep -E 'effective priority'
# 预期:Changing effective priority from 150 to 90 (150-60=90)
ip -br addr show eth0 | grep 10.x.x.100 ; echo "exit=$?"
# 预期:exit=1,VIP 已漂移
sudo systemctl start haproxy
回滚预案:
restart。注意回滚同样需要 restart,同样会触发一次漂移。keepalived --config-test。复盘:
vrrp_script 定义、virtual_router_id、interface、track_* 结构的变更都必须 restart;只改变通知脚本内容和日志级别等外围项时 reload 足够。--config-test 作为 CI 流水线的一部分,配置变更提交时自动校验语法。现象:规划内的主备切换演练。lb1 主动让位,lb2 顺利接管。两端 ip addr 都正确,lb2 上的日志明确打印 Entering MASTER STATE,GARP 也发出去了。但业务侧监控显示请求成功率从 99.9% 掉到 60%,持续约 3 分钟后自行恢复。
初判:既然 VIP 迁移和日志都正常,且能自行恢复,指向上游网络设备的 ARP 表更新延迟。但需要证据,不能凭猜测下结论。
检查过程:
# ---- 第 4 层:从不同位置验证 VIP 可达性 ----
# 1. 在 lb2 本机(回环和本机接口,一定通)
curl -sf -o /dev/null -w '%{http_code} %{time_total}\n' http://10.x.x.100/healthz
# 预期:200,很快。这一步只能证明本机服务正常,证明不了流量可达
# 2. 在同一网段的第三台机器上测(关键:绕开上游设备)
ssh app-host "curl -sf -o /dev/null -w '%{http_code} %{time_total}\n' --max-time 3 http://10.x.x.100/healthz"
# 预期:如果这台机器与 lb2 同网段且能直接 ARP,应该通
# 3. 在跨网段的客户端上测(这才是业务真实路径)
ssh client-host "curl -sf -o /dev/null -w '%{http_code} %{time_total}\n' --max-time 3 http://10.x.x.100/healthz; echo exit=$?"
# 预期:如果这里失败而第 2 步成功 → 问题在网关/上游设备的 ARP 或路由
结果:第 1 步通、第 2 步通、第 3 步超时。这组结果直接定位到上游设备。
# 4. 取证:确认 VIP 的 MAC 是否正确,以及客户端侧的 ARP 缓存
# 在失败时刻从客户端抓包(用 tcpdump 存 pcap 便于事后分析)
ssh client-host "sudo timeout 10 tcpdump -i eth0 -n host 10.x.x.100 -w /tmp/vip.pcap"
# 事后分析:看 ARP 请求的目标 MAC 是否仍是旧节点的 MAC
# 5. 从客户端侧看 ARP 缓存(Linux 客户端)
ip neigh show 10.x.x.100
# 异常表现:lladdr 是 fb1:旧的 MAC,状态 REACHABLE
# 判断依据:客户端的邻居表还指着旧节点 → 它发出的包走错地方
# 6. 关键证据:确认新节点的 MAC,和客户端缓存对比
ssh lb2 "ip link show eth0 | awk '/link\/ether/{print \$2}'"
# 预期:客户端缓存里的 MAC 应该等于这个值。不等就是 ARP 未更新
根因:lb2 升主后发送了 GARP(garp_master_delay 1),但上游三层交换机配置了 ARP 表项的老化策略和防 ARP 欺骗的端口安全(port security),对 GARP 的处理被限速。同时跨网段客户端的 ARP 缓存里是网关的 MAC(不是 VIP 的 MAC),所以客户端侧看不到问题——真正需要更新的是网关设备上的 ARP 表项,它把 VIP 映射到了旧的 MAC。
约 3 分钟后自愈,是因为网关的 ARP 老化定时器到点,重新发起 ARP 请求并学到了新 MAC。
修复与缓解(分层次,从立即生效到长期方案):
# ---- 立即缓解:增强 GARP 的发送强度和频率 ----
vrrp_instance VI_100 {
...
# 升主后立刻开始发 GARP,不要延迟
garp_master_delay 0
# 每次发多个 GARP(不同版本默认值不同,显式写出)
garp_master_repeat 5
# 持主期间周期性重发,对抗上游的 ARP 限速和缓存老化
# 语义:距上次发 GARP 超过这个秒数且期间没发过通告,就再发一次
garp_master_refresh 10
garp_master_refresh_repeat 2
# 如果上游对 ARP 限速严格,可以额外让 Keepalived 用更"暴力"的方式
# 注意:某些交换机会把这种模式识别为攻击并端口隔离,启用前必须在测试环境验证
# vrrp_garp_interval 0
# vrrp_gna_interval 0
}
# ---- 上游设备侧(需要网络团队配合,必须有变更审批)----
# 在网关交换机上确认并调整:
# 1. 查看 ARP 表项老化时间(不同厂商命令不同,以 Cisco 为例)
# show ip arp 10.x.x.100
# show run interface Vlan<id> | include arp
# 2. 确认端口安全(port security)是否对 GARP 做了限速
# show port-security interface <port>
# 3. 如果需要,把 VIP 做成静态 ARP 绑定(最可靠但需要运维成本)
# arp 10.x.x.100 <lb-mac> ARPA
验证:
# 1. 再次演练,从跨网段客户端持续探测,记录成功率曲线
# 用循环探测而不是单次,才能看到切换窗口内的丢包情况
for i in $(seq 1 60); do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 http://10.x.x.100/healthz)
printf '%s code=%s\n' "$(date +%T)" "$code"
sleep 1
done
# 预期:切换瞬间可能有 1~3 个失败(TCP 重建 + ARP 更新),随后全部 200
# 异常:持续数十秒大量失败 → GARP 没生效,或者上游设备没接受 GARP
# 2. 从网关侧确认 ARP 表项已指向新 MAC(需要网络团队执行)
# show ip arp 10.x.x.100 → MAC 应为 lb2 的 MAC
# 3. 用 Prometheus 记录整个切换窗口的指标变化
# (关键:把探测做成 blackbox_exporter 的探针,长期保留数据)
# blackbox_exporter 的探测配置(prometheus.yml 片段)
# 目的:把"VIP 可达性"变成一个长期可查的 SLI,而不是靠临时 curl
scrape_configs:
- job_name: 'vip-probe'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- http://10.x.x.100/healthz
- http://10.x.x.101/healthz
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9115 # blackbox_exporter 地址
# 采集间隔设短一些,切换窗口只有几秒,30s 采样会漏掉
scrape_interval: 5s
对应的告警:
- alert: VipProbeFailing
expr: probe_success{job="vip-probe"} == 0
for: 1m
labels: {severity: critical}
annotations:
summary: "VIP {{ $labels.instance }} 探测连续失败超过 1 分钟"
runbook: "先看是否在切换窗口内;若不在,检查 ARP/GARP 和上游设备"
# 切换窗口内的短暂失败是可接受的,用这个规则区分"切换"和"真故障"
- alert: VipProbeFlapping
expr: changes(probe_success{job="vip-probe"}[15m]) > 4
labels: {severity: warning}
annotations:
summary: "VIP {{ $labels.instance }} 探测结果频繁变化,可能存在 ARP 抖动"
回滚预案:garp_master_* 参数属于低风险调整,回滚就是改回原值并 restart(注意 restart 本身会触发一次切换)。上游设备的静态 ARP 绑定如果需要回滚,必须有网络团队参与并有回滚脚本——静态绑定配错会导致 VIP 完全不可达,因为没有 ARP 应答。
复盘:
blackbox_exporter 做长期探测,记录每次演练的丢包窗口时长,形成基线。异常时要能对比。garp_master_refresh 设一个非零值。默认值因版本而异,显式配置能避免"默认值在我的环境恰好是最差的"这种问题。现象:白天业务高峰期,VIP 在两个节点之间每隔 10~30 秒切换一次,持续近一小时。两端日志都有大量 Entering MASTER/BACKUP STATE。业务表现为间歇性 502。
初判:抖动(flapping)通常有三个来源:健康检查在临界点抖动、网络丢包导致超时、两个节点的优先级过于接近。需要先确定是"检查结果在变"还是"报文没到"。
检查过程:
# ---- 第 3 层优先:健康检查是否在抖动 ----
sudo journalctl -u keepalived --since "1 hour ago" | grep -E 'VRRP_Script' | tail -40
输出显示两个脚本都在成功和失败之间反复跳:
VRRP_Script(chk_haproxy) failed
VRRP_Script(chk_haproxy) succeeded
VRRP_Script(chk_backend) failed
VRRP_Script(chk_backend) succeeded
而 fall 2 / rise 2 的配置意味着 2 次连续失败就该切换,2 次连续成功就该恢复——按每 2 秒一次的执行频率,这个抖动周期正好是 4~8 秒,与观察到的切换频率吻合。
# 手工执行脚本,看真实情况
for i in $(seq 1 10); do
sudo -u root /etc/keepalived/scripts/check_haproxy.sh
printf 'exit=%s\n' "$?"
sleep 1
done
结果:大部分是 0,偶尔出现 1。说明脚本本身有时候真的会失败。
# 找到脚本失败的真正原因 —— 看它到底卡在哪一步
# 用 strace 或者加详细日志手工跑
sudo bash -x /etc/keepalived/scripts/check_haproxy.sh 2>&1 | tail -30
# 关注哪个命令耗时最久或返回非零
# 检查 HAProxy 的 stats socket 响应时间(脚本最可能卡在这里)
time (printf 'show stat\n' | timeout 3 socat - UNIX-CONNECT:/var/run/haproxy/admin.sock | wc -l)
# 异常表现:耗时接近或超过 2 秒 → 脚本的 timeout 2 会随机触发失败
根因:check_haproxy.sh 里用 socat 查询 HAProxy 的 stats socket,而 socat - UNIX-CONNECT:... 在 socket 响应慢时会一直等待直到超时。业务高峰期 HAProxy 的连接数很高,show stat 的响应时间从平时的几毫秒涨到 1.5~2.5 秒,撞上了脚本里的 timeout 2,于是随机失败。因为 fall 2,两次连续失败就触发切换;切换后流量转移到另一节点,那台机器的 HAProxy 负载升高,show stat 也变慢,于是它也失败——形成了自我强化的振荡。
这是最典型也最隐蔽的抖动模式:健康检查的开销本身成为了故障源。
指标证据(从 HAProxy 和系统指标对齐时间):
# HAProxy 连接数在抖动期间是高位(这个指标由 haproxy_exporter 提供)
haproxy_frontend_connections_rate{frontend="fe_app"}
# 与脚本执行时间对齐后可以确认:连接数越高,脚本越容易超时
# 关键证据:脚本失败率的上升与连接数上升同步
# 需要在脚本里上报执行耗时(见下方修复)
rate(keepalived_script_duration_seconds_count{script="chk_haproxy"}[5m])
修复(三层,从治标到治本):
# ---- 第一层:调大 timeout 并放宽 fall/rise(快速止血)----
vrrp_script chk_haproxy {
script "/etc/keepalived/scripts/check_haproxy.sh"
interval 2
timeout 5 # 从 3 提到 5:容纳峰值时的 stats 查询延迟
weight -60
fall 4 # 从 2 提到 4:连续 4 次失败(约 8 秒)才判定失败
rise 3
}
vrrp_instance VI_100 {
...
# 调大 advert_int 能降低抖动敏感度,但会增加故障检测时间
# 这是权衡,不是免费的。2 秒意味着最长 6 秒以上才检测到故障
advert_int 2
}
# ---- 第二层:消除脚本的阻塞点(治本)----
# 把"查询 stats socket"换成更轻量的检查方式:
# 1) 只检查进程存在 + 端口监听(不需要 stats socket)
# 2) stats socket 查询放在后台任务里,用缓存结果
#!/usr/bin/env bash
# /etc/keepalived/scripts/check_haproxy_fast.sh
# 轻量版:只用内核能直接回答的问题,不依赖 socket 往返
# 退出码:0=健康 1=不健康 2=无法判断
set -uo pipefail
readonly PORT=80
# 检查 1:进程存在(几乎零开销)
if ! pgrep -x haproxy >/dev/null 2>&1; then
printf 'chk_haproxy FAIL: process not found\n'
exit 1
fi
# 检查 2:监听端口存在(读 /proc/net/tcp,比 connect 更轻)
# 十六进制 80 = 0x0050
if ! awk 'NR>1 {split($2,a,":"); if (a[2]=="0050" && $4=="0A") found=1} END{exit !found}' /proc/net/tcp; then
printf 'chk_haproxy FAIL: no listener on port %s\n' "$PORT"
exit 1
fi
# 检查 3:TCP 连接能否真正建立(一次 connect,1 秒超时)
# 这是最接近真实业务路径的检查,开销可控
if timeout 1 bash -c "echo > /dev/tcp/127.0.0.1/${PORT}" 2>/dev/null; then
printf 'chk_haproxy OK: port %s accepts connections\n' "$PORT"
exit 0
fi
printf 'chk_haproxy FAIL: port %s refuses connection\n' "$PORT"
exit 1
# ---- 第三层:给脚本加可观测性(防止下次再靠猜)----
# 在脚本里记录执行耗时,写入 textfile 供 Prometheus 采集
# 在脚本开头和结尾加计时(追加到上面的 fast 版本里)
_start_ns=$(date +%s%N)
# ... 检查逻辑 ...
_end_ns=$(date +%s%N)
_dur_ms=$(( (_end_ns - _start_ns) / 1000000 ))
_metric_dir="/var/lib/node_exporter/textfile"
_tmp="$(mktemp "${_metric_dir}/chk_haproxy.XXXXXX")"
{
printf '# HELP keepalived_script_duration_ms Last execution duration of a check script\n'
printf '# TYPE keepalived_script_duration_ms gauge\n'
printf 'keepalived_script_duration_ms{script="chk_haproxy"} %s\n' "$_dur_ms"
} > "$_tmp"
chmod 0644 "$_tmp"
mv -f "$_tmp" "${_metric_dir}/chk_haproxy.prom"
# 注意:这个指标是"最后一次执行耗时",抓取时机与执行时机不同步,
# 但用于观察趋势足够,且成本极低
验证(抖动是否真的停止):
# 持续监控状态迁移次数,正常应该为 0
watch -n 5 'sudo journalctl -u keepalived --since "5 min ago" | grep -c "Entering MASTER STATE"'
# 预期:稳定在 0(或 1,刚启动那一次)
# 异常:持续增长 → 抖动未消除,回到第 3 层继续查
# 同时观察脚本耗时指标
curl -s http://127.0.0.1:9100/metrics | grep keepalived_script_duration_ms
# 预期:在几十毫秒量级。若上百毫秒,说明检查本身太重,还要继续优化
回滚预案:
vrrp_script 参数调整需要 restart,会触发漂移。回滚同样是改参数 + restart。timeout/fall/rise 这些参数在 vrrp_script 段里,改动需要 restart。lb1 上替换脚本(改文件即可),观察一段时间;确认稳定后再处理 lb2 的参数。复盘:
timeout 的取值应该基于实测的 P99 执行耗时,而不是凭感觉给一个数。经验值:timeout >= 3 × P99。fall / rise 的次数与 interval 的乘积决定了抖动抗性,也决定了故障检测时间。必须在两者之间显式权衡并记录决策理由。本例中 fall 4 × interval 2 = 8 秒 是最长检测时间,这是可接受的;如果业务要求 3 秒内切换,就不能靠加大 fall 来抗抖动,只能优化脚本本身。双主(Active-Active)看着比主备更"高级",但它有三个前提条件,缺一个就不要用:
前提一:两个节点要能互相接管全部 VIP。 意味着每个节点都要有足够的容量同时承载两个 VIP 的流量。如果按"各承担 50%"做容量规划,切换后一台机器要扛 100%,这会直接过载。容量必须按 100% 准备。
前提二:业务连接要能容忍 IP 变化。 VIP 迁移后,已建立的 TCP 连接会中断(除非用 LVS 的 connection sync 做会话同步)。如果业务是长连接且不能重连,双主不适用。
前提三:必须有 vrrp_sync_group。 否则会出现两个 VIP 压在同一台机器上的中间态,让容量规划失效。
virtual_router_id 与 router_id 必须纳入清单管理案例二已经说明后果。具体要求:
virtual_router_id(1~255)在同一个二层广播域内必须全局唯一。跨网段的多个 VRRP 组可以用相同的 VRID,但为了省心,建议全局唯一。router_id 是通知和日志里标识节点的字符串,必须唯一且稳定。不要用 IP 当 router_id——换机器后历史日志会对不上,排障时会多一层困惑。# 上线前扫描同网段是否存在相同 VRID 的 VRRP 通告(做一次基线)
sudo timeout 15 tcpdump -i eth0 -n vrrp 2>/dev/null | \
awk '{print $5, $7}' | sort -u
# 预期输出示例:
# vrid 100, 10.x.x.11
# vrid 101, 10.x.x.12
# 判断依据:每个 VRID 只应对应一个源 IP(当前持有者)。
# 若某个 VRID 出现多个源 IP → 有冲突,必须先解决再上线
VRRP 的 auth_type PASS 不是加密,是明文摘要保护。它只能防"意外的误配混入",防不了有意的攻击——攻击者能抓到通告就能提取信息。所以:
123456)。0600 权限的文件。include 把密码部分拆到单独文件,该文件不进版本库。# 密码文件的权限要求
sudo chown root:root /etc/keepalived/vrrp.auth
sudo chmod 0600 /etc/keepalived/vrrp.auth
# 判断依据:Keepalived 以 root 运行,配置里不应有任何非 root 可读的凭据
ls -la /etc/keepalived/
# 异常:keepalived.conf 是 0644 且内含 auth_pass → 任何本地用户能读到
VRRPv3 的差异:v3 可以用 auth_type AH(IPsec 认证头),但支持度和复杂度都更高。生产环境仍然普遍用 PASS。这一点必须在文档里写清楚,避免有人以为配了 PASS 就"安全了"。
global_defs 里的 notification_email 需要本机有可用的 MTA(sendmail/postfix)。大多数最小化安装的系统没有 MTA,此时 Keepalived 不会报错,只是通知静默丢失。
# 检查是否有 MTA
command -v sendmail postfix exim 2>/dev/null
systemctl is-active postfix 2>/dev/null || echo "postfix not active"
# 判断依据:都没有 → notification_email 是无效配置
# 确认 Keepalived 是否在尝试发邮件
sudo journalctl -u keepalived | grep -iE 'smtp|mail'
# 异常:出现 "SMTP connection failed" / "cannot connect to smtp server"
建议:生产环境不要依赖 Keepalived 的邮件通知。改用通知脚本 + Prometheus 告警。邮件通知的问题不只是可能失效,还有:无重试、无分级、无抑制、无法做值班轮转。它是 2000 年代的方案。
当 VIP 后面的服务并发很高时,有几个参数需要动:
# 内核参数:VIP 迁移时允许绑定到非本地地址(LVS 场景常见)
# 注意:这属于"允许绑定非本地 IP",会带来安全影响,只应在前端机器上开启
# /etc/sysctl.d/99-keepalived.conf
# net.ipv4.ip_nonlocal_bind = 1
# net.ipv4.ip_forward = 1 # 仅 LVS 转发节点需要
# ARP 相关:VIP 迁移时的 ARP 应答行为
# net.ipv4.conf.all.arp_ignore = 1
# net.ipv4.conf.all.arp_announce = 2
# 说明:arp_ignore=1 表示只应答目标 IP 是本地地址的 ARP 请求;
# arp_announce=2 表示总是使用最佳本地地址做 ARP 通告。
# 这两个参数在 LVS DR 模式(real server 侧)是必需的,
# 在普通 Keepalived + nginx/HAProxy 场景不是必需的,
# 但配上可以减少"VIP 迁移后出现来源地址错误的 ARP"这类问题。
# 应用的优雅降级:VIP 迁移前让服务先停止接受新连接
# 这是最难但最有效的一环,需要应用配合
# 思路:notify_backup / notify_fault 脚本里触发"排空"逻辑
#!/usr/bin/env bash
# /etc/keepalived/scripts/drain.sh —— 在让位前排空连接
# 只在 notify_backup / notify_fault 中调用,通知脚本本身必须快速,
# 所以这里把耗时的排空动作放到后台并立即返回
set -uo pipefail
readonly HA_SOCK="/var/run/haproxy/admin.sock"
# 后台执行,不阻塞通知脚本
(
# 1. 把后端标记为维护状态,停止接受新连接
printf 'set server bk_app/srv1 state maint\n' | \
timeout 3 socat - "UNIX-CONNECT:${HA_SOCK}" >/dev/null 2>&1 || true
# 2. 等待既有连接自然结束(有上限,不能无限等)
for _ in $(seq 1 10); do
conns=$(printf 'show info\n' | timeout 2 socat - "UNIX-CONNECT:${HA_SOCK}" 2>/dev/null \
| awk '/CurrConns/{print $2}')
[[ -z "${conns:-}" || "${conns}" == "0" ]] && break
sleep 1
done
) &
# 关键:& 让它后台运行,父脚本立刻退出
exit 0
风险提示:排空逻辑如果写错,会导致 VIP 已经漂移但旧节点还在接新连接(双写),或者旧节点永久不恢复。所以这个脚本必须有超时上限,并且在预发环境充分验证。不建议在第一次上线时就引入排空逻辑,应该先确保基础的漂移工作正常,再逐步加入。
Keepalived 相关的变更几乎都属于高风险,因为它们直接决定入口可用性。执行规范:
必须走变更窗口:restart keepalived、修改 priority/weight/advert_int、修改 virtual_router_id、防火墙规则变更、内核参数变更。
可以随时做:日志级别调整、通知脚本内容修改(不改参数)、Prometheus 告警规则调整。
灰度执行顺序(两端架构):
stop 服务触发真实切换),这是唯一能确认配置真的生效的方法。权限控制:
# 建议用 sudo 限制具体命令,而不是给整个 shell 的 root 权限
# /etc/sudoers.d/keepalived-ops
# %ops ALL=(root) NOPASSWD: /usr/bin/systemctl restart keepalived
# %ops ALL=(root) NOPASSWD: /usr/bin/systemctl reload keepalived
# %ops ALL=(root) NOPASSWD: /usr/sbin/keepalived --config-test *
# 注意:不要给 NOPASSWD: /usr/bin/systemctl * (等于给了整个服务的控制权)
# 也不要给 NOPASSWD: /usr/bin/bash(等于给了 root shell)
sudo visudo -c -f /etc/sudoers.d/keepalived-ops
# 预期:/etc/sudoers.d/keepalived-ops: parsed OK
每次变更前必须准备(不是"最好准备"):
# 1. 配置备份(带时间戳,保留至少 30 天)
sudo mkdir -p /backup/keepalived
sudo cp -a /etc/keepalived/keepalived.conf \
"/backup/keepalived/keepalived.conf.$(date +%Y%m%d-%H%M%S)"
# 2. 脚本目录一起备份(脚本和配置强耦合)
sudo tar czf "/backup/keepalived/scripts.$(date +%Y%m%d-%H%M%S).tar.gz" \
-C /etc/keepalived scripts
# 3. 记录当前状态作为回滚基线
{
echo "=== $(date -Is) ==="
ip -br addr show
systemctl show keepalived -p MainPID -p ExecMainStartTimestamp
sudo journalctl -u keepalived -n 20 --no-pager
} | sudo tee "/backup/keepalived/state.$(date +%Y%m%d-%H%M%S).txt" >/dev/null
# 4. 校验备份文件可读(不要跳过)
sudo tar tzf /backup/keepalived/scripts.*.tar.gz | head
# 预期:列出 check_haproxy.sh 等文件
回滚动作:
# 回滚 = 恢复配置 + restart。注意 restart 会再次触发漂移
sudo cp -a /backup/keepalived/keepalived.conf.<时间戳> /etc/keepalived/keepalived.conf
sudo keepalived --config-test -f /etc/keepalived/keepalived.conf || {
echo "回滚后的配置校验失败,停止操作" >&2
exit 1
}
sudo systemctl restart keepalived
回滚失败的兜底:如果 restart 后 Keepalived 起不来,VIP 会完全消失。此时的手工兜底是把 VIP 临时加到一台节点上(用最原始的方式保住业务):
# 【高风险】手工接管 VIP。前置检查:确认对端没有持有同一 VIP,否则会造成冲突
ip -br addr show | grep 10.x.x.100 && { echo "VIP 已存在,勿重复添加" >&2; exit 1; }
sudo ip addr add 10.x.x.100/24 dev eth0
sudo ip neigh flush dev eth0 # 触发邻居表重建
# 验证
ping -c 2 -W 1 10.x.x.100
curl -sf -o /dev/null -w '%{http_code}\n' http://10.x.x.100/healthz
# 说明:这是应急手段,必须记入事件报告并尽快恢复 Keepalived 管理
部署或变更完成后逐项确认。每一项都给了命令和明确的预期/异常对照,不能只打勾。
# 1. 版本符合假设
keepalived --version | head -1
# 预期:v2.2.x(或你知道的版本)。若是 v1.3.x,注意 state 必填、netfilter 走 iptables
# 2. 配置语法校验通过
sudo keepalived --config-test -f /etc/keepalived/keepalived.conf ; echo "exit=$?"
# 预期:exit=0
# 异常:exit 非 0 → 有语法错误,不要继续。常见是少了一个花括号或写错关键字名
# 3. 网卡名与配置一致
ip -br addr show | awk '{print $1}'
grep -E '^\s*interface' /etc/keepalived/keepalived.conf
# 预期:配置里的网卡名出现在 ip 命令输出里
# 异常:不一致(例如配了 eth0 但实际是 ens18)→ 实例会停在 INIT
# 4. 脚本权限符合安全要求
sudo find /etc/keepalived/scripts -type f -printf '%m %u:%g %p\n'
# 预期:0755 root:root(或 0700)
# 异常:含 2(组可写)或最后一位是 6/7(其他用户可写)→ 2.x 会拒绝执行
# 5. 脚本的三种退出码符合预期(逐个脚本验证)
sudo /etc/keepalived/scripts/check_haproxy.sh ; echo "exit=$?"
# 预期:exit=0(健康时)。停掉服务再试应得 exit=1
# 6. 双向能看到 VRRP 通告
sudo timeout 8 tcpdump -i eth0 -n -e vrrp
# 预期:能看到当前 MASTER 的周期通告,形如
# xx:xx:xx:xx:xx:xx > 01:00:5e:00:00:12, ethertype IPv4
# 10.x.x.11 > 224.0.0.18: VRRPv2, Advertisement, vrid 100, prio 150
# 异常:什么都没有 → 依次排查:对端进程是否在跑、防火墙是否拦 112、
# 组播是否被虚拟网络丢弃(若是,改用 unicast_peer)
# 7. 通告源只有拓扑内的节点
sudo timeout 15 tcpdump -i eth0 -n vrrp 2>/dev/null | awk '{print $3}' | sort -u
# 预期:只有两个节点 IP。多出任何其他 IP → 幽灵节点,见案例二
# 8. VRID 无冲突
sudo timeout 15 tcpdump -i eth0 -n -e vrrp 2>/dev/null | grep -o 'vrid [0-9]*' | sort -u
# 预期:每个 VRID 只对应一个源 IP
# 9. TTL 正确(跨三层的证据)
sudo timeout 8 tcpdump -i eth0 -n -vv vrrp | grep -i ttl
# 预期:ttl 255
# 异常:ttl 不是 255 → 报文跨了三层设备,VRRP 在这种情况下不可靠
# 10. 实例状态符合设计(双主:一台持 100、另一台持 101)
for h in lb1 lb2; do
echo "===== $h ====="
ssh "$h" "ip -br addr show eth0 | grep -oE '10\.x\.x\.10[01]' | sort -u"
done
# 预期:
# lb1: 10.x.x.100
# lb2: 10.x.x.101
# 异常:
# 两端都有 10.x.x.100 → 脑裂,立即按案例二处理
# 两端都没有某个 VIP → VIP 消失,查是不是两端都进了 FAULT
# 11. 无抖动
sudo journalctl -u keepalived --since "30 min ago" | grep -c 'Entering MASTER STATE'
# 预期:0(或 1,如果刚重启过)
# 异常:> 2 → 抖动,按案例五排查
# 12. 优先级算术正确(做一次故障注入)
sudo systemctl stop haproxy
sleep 8
sudo journalctl -u keepalived --since "2 min ago" | grep -E 'effective priority|Entering'
# 预期:出现 "Changing effective priority from 150 to 90" 和 "Entering BACKUP STATE"
# 异常:只有脚本 failed,没有 priority 变化 → weight 方向配反(见 6.5)
# 13. 恢复
sudo systemctl start haproxy
# 注意:配了 nopreempt 时 VIP 不会自动切回,这是预期行为
# 14. 跨网段探测(最关键的一项:验证流量真的跟着走)
# 从一台真实客户端,按秒探测,记录成功率和时间
for i in $(seq 1 30); do
printf '%s %s\n' "$(date +%T)" \
"$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 http://10.x.x.100/healthz)"
sleep 1
done
# 预期:切换瞬间 1~3 个失败(000 或超时),随后稳定 200
# 异常:持续大量失败 → ARP 未更新(案例四)或后端未就绪
# 15. 上游设备侧的 ARP 表项(需要网络团队配合)
# 在网关/核心交换机上查看 VIP 的 ARP 表项,确认指向新节点的 MAC
# 验证命令随厂商不同,例如 Cisco: show ip arp 10.x.x.100
# 预期:MAC 等于新持有者网卡的 MAC
# 16. 指标已采集
curl -s http://127.0.0.1:9100/metrics | grep -E 'keepalived_'
# 预期:keepalived_vip_held{vip="10.x.x.100"} 1
# keepalived_vip_held{vip="10.x.x.101"} 0
# 异常:无输出 → textfile collector 未启用,或目录权限不对
# 17. 告警规则已被 Prometheus 加载
curl -s http://127.0.0.1:9090/api/v1/rules | grep -o 'Keepalived[A-Za-z]*'
# 预期:列出 KeepalivedSplitBrain / KeepalivedVipMissing / KeepalivedFlapping 等
# 异常:缺失 → 规则文件未加载,检查 prometheus.yml 的 rule_files 和规则语法
promtool check rules /etc/prometheus/rules/keepalived.yml
# 预期:SUCCESS
# 18. 告警能真的触发(在测试环境做故障注入)
# 临时在测试环境制造脑裂场景,确认告警在 30 秒内触发
# 验证后必须还原,并确认告警恢复(Alertmanager 里能看到 resolved 通知)
这部分是唯一能验证"预案真的可用"的方法。每季度至少做一次,并记录结果。
| 演练项 | 操作 | 预期结果 | 观察指标 |
|---|---|---|---|
| 服务进程故障 | systemctl stop haproxy(在一端) |
VIP 在 8 秒内漂移,跨网段探测成功率恢复 | 探测失败窗口 ≤ 4 秒 |
| Keepalived 进程故障 | systemctl stop keepalived(在一端) |
对端接管,无脑裂 | 通告源只剩一个 |
| 网卡故障 | ip link set eth0 down(在一端,谨慎) |
该端进 FAULT,对端接管 | 无 IP 冲突告警 |
| 配置回滚 | 故意把权重配错,再回滚 | 演练能发现问题并成功回滚 | 回滚耗时 ≤ 5 分钟 |
# 演练脚本的一部分:记录切换窗口的精确时间线
# 目的:把"切换耗时"变成可比较的数据,而不是"感觉挺快"
START=$(date +%s.%N)
sudo systemctl stop haproxy
# 等待 VIP 离开本机
while ip -o -4 addr show | grep -qw 10.x.x.100; do
sleep 0.2
# 超时保护,避免死循环
if (( $(echo "$(date +%s.%N) - $START > 30" | bc -l) )); then
echo "TIMEOUT: VIP 未在 30 秒内漂移" >&2
exit 1
fi
done
END=$(date +%s.%N)
printf 'VIP 漂移耗时: %.2f 秒\n' "$(echo "$END - $START" | bc -l)"
# 预期:≤ 8 秒(advert_int 1 + fall 2 + timeout 3 的累加)
# 把这个数值记入基线,后续每次演练对比
sudo systemctl start haproxy
风险提示:演练会在生产环境制造真实的服务切换。必须满足三个条件才能做:知情(业务方知晓)、窗口(低峰期)、可回退(对端健康且容量充足)。不要在没有任何准备的情况下在生产演练。
把全文压成可执行的判断规则:
unicast_peer,而不是想办法"绕过"这些限制。priority 和 weight 的方向必须一起验算。 weight -30 配在 150 vs 100 的节点上不会触发切换,这是配置层最常见的隐蔽错误。用 weight 0(失败即 FAULT)能彻底避开这类算术问题。vrrp_script 和 real_server 检查是两件完全不同的事。 前者决定 VIP 漂移,后者只摘除后端。用错会导致"后端故障引发 VIP 抖动"或"本机进程死了也不漂移"。state 只是初始状态,nopreempt 只在 state BACKUP 下有效。 要靠实际版本验证,不要照抄。vrrp_script 定义、virtual_router_id、interface、track_* 的变更必须 restart,而 restart 会触发真实漂移。virtual_router_id 必须有唯一性清单。 幽灵节点、跨业务 VRID 冲突会产生最难排查的"莫名其妙切换"。garp_master_refresh 要设非零值,验证必须从跨网段客户端做。timeout 要基于实测 P99 而不是感觉。vrrp_sync_group。最后一条经验:Keepalived 的配置很短,短到让人松懈。但它的每个参数都直接决定入口是否可用,而且大部分失效模式是静默的——没有报错、没有告警、进程还活着,只是不再工作了。对抗这种静默失效的唯一手段不是更仔细地读文档,而是把状态变成指标、把切换变成定期演练、把配置变更变成带校验和回滚的流程。这也是云栈社区上很多一线运维同行反复讨论过的共识。