在多云、跨地域的轻量级高可用场景中,我们经常需要将分散的云主机通过隧道组成一个虚拟内网,并在此之上搭建数据库集群和容器编排系统。本文记录了一次完整实践:在三台 Debian 云主机之间使用 ZeroTier 建立二层网络,部署 Patroni 管理的 PostgreSQL 流复制集群以及 K3s 控制平面,然后利用 Keepalived 和 HAProxy 构建一个虚拟 IP 入口,同时实现 PostgreSQL 读写分离和 K3s API Server 高可用代理。
与常见的 WireGuard 方案不同,ZeroTier 原生提供二层以太网,支持 ARP 和多播,理论上 VRRP 可直接使用多播模式。但本实践为更稳定可控,Keepalived 采用了 单播 VRRP(unicast_src_ip + unicast_peer),避免个别网络环境下多播包传播不稳定带来的误判。
一、背景与集群拓扑
节点信息:
| 主机名 | 内网 IP (ztpp6n6xmz) | 角色 |
|---|---|---|
| hyperbola-server | 10.0.0.10 | Patroni Leader、K3s control-plane |
| hyperbola-txy | 10.0.0.30 | Patroni Replica、K3s control-plane |
| hyperbola-aly | 10.0.0.40 | Patroni Replica、K3s control-plane |
- 三节点通过 ZeroTier 组成
/24的内网,接口名为ztpp6n6xmz(由网络 ID 决定,这里以ztpp6n6xmz为例)。 - PostgreSQL 集群由 Patroni 管理,使用
patronictl list可查看角色。 - K3s 集群有三个 etcd+control-plane 节点。
- 目标:提供一个统一的 VIP 10.0.0.100,对外暴露以下服务:
- 5432 端口 → PostgreSQL 写库(自动指向 Leader)
- 5433 端口 → PostgreSQL 读库(负载均衡到 Replica)
- 6443 端口 → K3s API Server(四层代理)
二、整体架构
┌─────────────────────────────────┐ │ keepalived (VIP 高可用) │ │ HAProxy (四层代理 + 健康检查) │ └──────────┬──────────────────────┘ │ VIP: 10.0.0.100 (ztpp6n6xmz) ┌──────────┼──────────┐ ▼ ▼ ▼ 10.0.0.10 10.0.0.30 10.0.0.40 (server) (txy) (aly) [Leader] [Replica] [Replica] [K3s CP] [K3s CP] [K3s CP]设计要点:
- Keepalived 通过 VRRP 通告(本实践为单播)管理 VIP,选择一台节点作为 MASTER,其余为 BACKUP。接口故障(如
ztpp6n6xmzdown)会立即触发降级,避免等待超时。 - HAProxy 仅在 MASTER 节点上运行,监听 VIP 的三个端口,根据后端健康检查动态路由流量。
- 健康检查直接调用 Patroni REST API(
/master、/replica),实现完全自动化的读写分离。
三、基础准备
3.1 加入 ZeroTier 网络
在所有节点上安装 ZeroTier 并加入同一个网络(示例网络 ID 为 8056c2e21c000001,实际使用您的网络 ID):
curl -s https://install.zerotier.com | sudo bashsudo zerotier-cli join <network_id># 在 ZeroTier Central 管理后台授权节点,并确保它们获得预期的 IP(如 10.0.0.10、10.0.0.30、10.0.0.40)sudo zerotier-cli listnetworks # 查看接口名称,通常为 zt<网络ID前10位>假设生成的接口名为 ztpp6n6xmz,后续所有配置均使用该名称。
3.2 安装 HAProxy 与 Keepalived
sudo apt updatesudo apt install -y haproxy keepalived
# 允许绑定非本机 IP(HAProxy 需要监听 VIP)echo "net.ipv4.ip_nonlocal_bind=1" | sudo tee -a /etc/sysctl.confsudo sysctl -p
# 停止并禁用这两个服务,后续由 keepalived 控制 HAProxy 启停sudo systemctl stop haproxy keepalivedsudo systemctl disable haproxy keepalived四、HAProxy 配置:实现读写分离
文件:/etc/haproxy/haproxy.cfg (三台节点相同)
global log /dev/log local0 maxconn 4096 user haproxy group haproxy daemon
defaults mode tcp timeout connect 5s timeout client 30m timeout server 30m
# ---------- PostgreSQL 读写分离 ----------frontend psql_write bind 10.0.0.100:5432 mode tcp default_backend pg_write
frontend psql_read bind 10.0.0.100:5433 mode tcp default_backend pg_read
# ---------- K3s API Server ----------frontend k8s_api bind 10.0.0.100:6443 mode tcp default_backend k3s_servers
# ---------- 后端定义 ----------backend pg_write # 写库:只有 Patroni Leader 存活 mode tcp option httpchk GET /master http-check expect status 200 default-server inter 3s fall 3 rise 2 server pg10 10.0.0.10:5432 check port 8008 server pg30 10.0.0.30:5432 check port 8008 server pg40 10.0.0.40:5432 check port 8008
backend pg_read # 读库:所有 Replica(Leader 会自动被 /replica 剔除) mode tcp option httpchk GET /replica http-check expect status 200 default-server inter 3s fall 3 rise 2 server pg10 10.0.0.10:5432 check port 8008 server pg30 10.0.0.30:5432 check port 8008 server pg40 10.0.0.40:5432 check port 8008
backend k3s_servers mode tcp balance roundrobin option tcp-check server srv10 10.0.0.10:6443 check server srv30 10.0.0.30:6443 check server srv40 10.0.0.40:6443 check关键机制:
- Patroni 默认在
8008端口提供 REST API,/master仅在 Leader 返回 200,/replica仅在 Replica 返回 200。 - HAProxy 利用
httpchk动态探测,自动将写请求导向 Leader,读请求分发到所有副本。 - K3s 后端仅使用 TCP 端口检查,保证 API Server 可达。
五、Keepalived 配置:VIP 高可用与 HAProxy 联动
ZeroTier 是二层虚拟以太网,理论上原生支持多播、可直接使用默认的 VRRP 多播模式。但本实践为稳定起见采用单播 VRRP:通过 unicast_src_ip 指定本机源 IP,unicast_peer 列出对端节点 IP,三个节点间单播互发通告。这样不依赖多播包在 ZeroTier 中的传播,排障更直观。
5.1 健康检查脚本
文件:/etc/keepalived/chk_haproxy.sh (三台节点相同,需 chmod +x)
#!/bin/bash# 检查 haproxy 进程是否存活if systemctl is-active --quiet haproxy; then exit 0else exit 1fi该脚本供 track_script 调用,如果 haproxy 意外停止,Keepalived 会降低本节点优先级,促使 VIP 漂移到其他健康节点。
5.2 通知脚本
文件:/etc/keepalived/notify.sh (三台节点相同,需 chmod +x)
#!/bin/bashVIP="10.0.0.100"DEV="ztpp6n6xmz" # 使用 ZeroTier 接口名
case "$1" in master) sleep 1 ip addr show $DEV | grep -q $VIP || exit 1 systemctl start haproxy # 发送 gratuitous ARP,通知同网段其他节点 VIP 已漂移到本机 MAC # 避免其他节点 ARP 缓存残留旧 MASTER 的 MAC,导致 ICMP Redirect 或短暂不通 ip neigh flush dev $DEV arping -c 5 -A -I $DEV $VIP 2>/dev/null || \ arping -c 5 -U -I $DEV $VIP 2>/dev/null || true logger "keepalived: transition to MASTER, haproxy started, gratuitous ARP sent" ;; backup) systemctl stop haproxy logger "keepalived: transition to BACKUP, haproxy stopped" ;; fault) systemctl stop haproxy logger "keepalived: transition to FAULT, haproxy stopped" ;; *) echo "Usage: $0 {master|backup|fault}" exit 1 ;;esac注意:
arping由iputils-arping提供,需先安装:sudo apt install -y iputils-arping。-A为免费 ARP(announce),-U为更新通告,部分版本需用其一。
该脚本确保 VIP 到达哪个节点,哪个节点的 HAProxy 就启动,避免多个节点同时监听 VIP 导致端口冲突。
5.3 Keepalived 主配置
三台节点的配置结构完全相同,仅 priority 不同。
下面以 hyperbola-server (10.0.0.10) 为例,配置中包含了详细注释。
/etc/keepalived/keepalived.conf:
# 定义健康检查脚本,供 track_script 引用vrrp_script chk_haproxy { script "/etc/keepalived/chk_haproxy.sh" # 检查 haproxy 是否存活 interval 2 # 每 2 秒执行一次 weight -20 # 失败时优先级减 20,让其他节点更容易抢占 fall 3 # 连续失败 3 次才认为故障 rise 2 # 连续成功 2 次才恢复健康}
vrrp_instance VI_PG { state BACKUP # 所有节点都设为 BACKUP,依靠优先级竞选 Master interface ztpp6n6xmz # ZeroTier 虚拟网卡接口 virtual_router_id 51 # 同一 VRRP 组必须相同 priority 100 # 最高优先级,默认成为 Master advert_int 1 # 通告间隔(秒) unicast_src_ip 10.0.0.10 # 本节点发送 VRRP 通告的源 IP unicast_peer { 10.0.0.30 # 对端节点 IP 10.0.0.40 } authentication { auth_type PASS auth_pass 51275127 # 所有节点必须一致 } virtual_ipaddress { 10.0.0.100/32 dev ztpp6n6xmz # 虚拟 IP,仅 Master 持有 } track_interface { ztpp6n6xmz # 监控 ZeroTier 接口,若 down 立即降级 } track_script { chk_haproxy # 引用上方健康检查脚本 } notify "/etc/keepalived/notify.sh" # 状态变化时调用通知脚本}其他节点修改示例:
- hyperbola-txy:
priority 90,unicast_src_ip 10.0.0.30,unicast_peer { 10.0.0.10 10.0.0.40 } - hyperbola-aly:
priority 80,unicast_src_ip 10.0.0.40,unicast_peer { 10.0.0.10 10.0.0.30 }
重要:所有节点上必须创建
chk_haproxy.sh和notify.sh并赋予执行权限(chmod +x),否则 Keepalived 会因为脚本缺失而进入 FAULT 状态。
六、启动与调优
6.1 启动服务
在三台机器上执行:
sudo systemctl start keepalived不要手动启动 HAProxy,它会被 notify.sh 自动拉起。
查看 VIP 所在节点:
ip addr show ztpp6n6xmz | grep 10.0.0.100正常情况下只有优先级最高的节点显示该 IP。
6.2 ZeroTier 的网络特性
与 WireGuard 不同,ZeroTier 提供的是二层以太网,VIP 的 ARP 能被所有节点学习,无需额外配置路由或允许 IP 段。本实践 Keepalived 使用单播 VRRP(unicast_src_ip + unicast_peer),节点间直接互发通告,不依赖多播。仍需确保:
- 所有节点的 ZeroTier 接口已加入同一网络且 IP 规划正确。
- ZeroTier 后台管理页面已授权所有节点通信(默认桥接模式即可)。
6.3 解决 K3s 证书信任问题
虽然四层代理已经可以转发流量到 API Server,但 TLS 证书未包含 VIP 10.0.0.100,kubectl 会报 x509: certificate is valid for ... not 10.0.0.100 错误。需要在三台 K3s 节点的配置中添加 SAN:
sudo mkdir -p /etc/rancher/k3scat <<EOF | sudo tee /etc/rancher/k3s/config.yamltls-san: - 10.0.0.100EOF
# 删除动态证书缓存并逐台重启 K3s(注意保持 etcd quorum)sudo rm -f /var/lib/rancher/k3s/server/tls/dynamic-cert.jsonsudo systemctl restart k3s七、验证与测试
7.1 PostgreSQL 读写分离
# 写库测试(应返回 f)psql -h 10.0.0.100 -p 5432 -U postgres -c "select pg_is_in_recovery();"
# 读库测试(应返回 t)psql -h 10.0.0.100 -p 5433 -U postgres -c "select pg_is_in_recovery();"实际测试结果:
pg_is_in_recovery------------------- f(1 row)
pg_is_in_recovery------------------- t(1 row)证明写请求准确路由到 Leader,读请求落到了某个 Replica。
7.2 K3s API 高可用
kubectl get nodes --server https://10.0.0.100:6443能够正常列出节点,无证书错误。也可将 kubeconfig 中的 server 地址永久改为 https://10.0.0.100:6443。
7.3 故障模拟
- 停止当前 MASTER 的 haproxy:
sudo systemctl stop haproxy
→ 健康检查失败,优先级降低,VIP 漂移到次高优先级节点,新 MASTER 自动启动 haproxy,服务恢复。 - 断开当前 MASTER 的 ZeroTier 连接(如
sudo zerotier-cli leave <network_id>或关闭接口)
→ 接口监控立即触发降级,VIP 秒级漂移,整个过程对客户端透明。
八、踩坑记录与最佳实践
-
ZeroTier 是 VRRP 的理想承载层
WireGuard 是三层隧道,不支持多播和 ARP 广播,导致 VIP 必须通过单播 VRRP 和额外的路由配置才能工作(sudo wg set),且客户端解析 VIP 时容易出问题。切换为 ZeroTier 后这些问题缓解;本实践沿用单播 VRRP(unicast_src_ip+unicast_peer),比多播更稳定可控。 -
Keepalived 配置必须完整
缺少track_interface会导致接口 down 时无法立即切换;缺少健康检查脚本或track_script引用会使实例进入 FAULT;未配置notify指令则通知脚本永远不会执行。三要素缺一不可。 -
接口名称一致性
ZeroTier 的接口名称由网络 ID 决定,不同设备上可能不同(如zt7ufxgkl3),建议在每台机器上确认接口名,并统一写入 keepalived 配置。或者通过 ZeroTier 的local.conf固定接口名称。 -
VIP 单点存在是正常现象
所有 BACKUP 节点不会持有 VIP,这是 VRRP 的正常表现,不必担心。 -
证书规划
如果 K3s 部署时未预知 VIP 地址,后期添加 SAN 需重启集群,建议在部署初期就规划好所有可能的访问入口。 -
etcd 依赖 ZeroTier 接口:服务已启动不代表地址已就绪 在把 WSL2 节点
10.0.0.20加入 Patroni 后,出现了一个容易误判的故障:patronictl list中没有pg-20,而 Patroni 本身只显示依赖失败。Dependency failed for patroni.service - Patroni for PostgreSQL HA.patroni.service: Job patroni.service/start failed with result 'dependency'.patroni.service配置了Requires=etcd.service,继续检查 etcd 才找到真正的错误:creating peer listener failedlisten tcp 10.0.0.20:3380: bind: cannot assign requested addressetcd.service: Main process exited, code=exited, status=1/FAILURE对照本次启动的 monotonic 日志可以还原竞态过程:
- 开机约 12.2 秒时,etcd 开始绑定
10.0.0.20:3380; - 约 12.4 秒时绑定失败,Patroni 随即因 etcd 依赖失败而停止启动;
- 约 13.9 秒时,ZeroTier 接口
ztpp6n6xmz才 Link UP 并获得地址; - etcd 原配置为
Restart=on-abnormal,退出码 1 不属于该策略覆盖的异常终止,因此之后没有自动重试。
这里有两个不同层次的就绪条件:
zerotier-one.service进入 active;- ZeroTier 完成控制面连接、创建虚拟接口并分配
10.0.0.x。
After=zerotier-one.service只能保证第一层,不能保证第二层。最终在所有 etcd 节点上使用 systemd drop-in:既声明强依赖,也在启动前等待本机的 ZeroTier 地址。/etc/systemd/system/etcd.service.d/zerotier.conf [Unit]Requires=zerotier-one.serviceAfter=zerotier-one.service[Service]ExecStartPre=/bin/sh -c 'for i in $(seq 1 60); do /usr/sbin/ip -4 address show dev ztpp6n6xmz 2>/dev/null | /usr/bin/grep -q "inet 10.0.0.20/" && exit 0; sleep 1; done; echo "Timed out waiting for ZeroTier address 10.0.0.20 on ztpp6n6xmz" >&2; exit 1'Restart=on-failureRestartSec=3s10.0.0.20应替换为各节点在/etc/default/etcd的ETCD_LISTEN_PEER_URLS中配置的地址。使用 drop-in 而不是直接编辑/usr/lib/systemd/system/etcd.service,可以避免软件包升级覆盖修改;systemd 会将 drop-in 合并进原 unit,依赖语义完全相同。本集群通过 Ansible 的
servers组统一发布。由于 etcd 依赖多数派,不能同时重启所有成员,play 必须设置serial: 1,每次仅处理一台,并确认本地端点恢复健康后再继续下一台:---- name: Make etcd wait for the ZeroTier addresshosts: serversbecome: trueserial: 1tasks:- name: Read etcd peer addressansible.builtin.shell: |set -o pipefailsed -n 's|^ETCD_LISTEN_PEER_URLS="http://\([^:]*\):.*|\1|p' /etc/default/etcdargs:executable: /bin/bashregister: etcd_peer_addresschanged_when: falsefailed_when: not (etcd_peer_address.stdout is match('^[0-9]+(\.[0-9]+){3}$'))- name: Create etcd systemd drop-in directoryansible.builtin.file:path: /etc/systemd/system/etcd.service.dstate: directoryowner: rootgroup: rootmode: "0755"- name: Install ZeroTier readiness drop-inansible.builtin.copy:dest: /etc/systemd/system/etcd.service.d/zerotier.confowner: rootgroup: rootmode: "0644"content: |[Unit]Requires=zerotier-one.serviceAfter=zerotier-one.service[Service]ExecStartPre=/bin/sh -c 'for i in $(seq 1 60); do /usr/sbin/ip -4 address show dev ztpp6n6xmz 2>/dev/null | /usr/bin/grep -q "inet {{ etcd_peer_address.stdout }}/" && exit 0; sleep 1; done; echo "Timed out waiting for ZeroTier address {{ etcd_peer_address.stdout }} on ztpp6n6xmz" >&2; exit 1'Restart=on-failureRestartSec=3sregister: etcd_dropin- name: Reload systemd configurationansible.builtin.systemd_service:daemon_reload: truewhen: etcd_dropin.changed- name: Restart etcdansible.builtin.systemd_service:name: etcd.servicestate: restartedenabled: truewhen: etcd_dropin.changed- name: Wait for etcd healthansible.builtin.uri:url: http://127.0.0.1:3379/healthreturn_content: trueregister: etcd_healthretries: 20delay: 1until:- etcd_health.status == 200- "'\"health\":\"true\"' in etcd_health.content"发布后检查 systemd 合并结果和集群状态:
systemctl show etcd -p Requires -p After -p Restart -p ExecStartPrecurl -fsS http://127.0.0.1:3379/healthsudo patronictl -c /etc/patroni/config.yml list最终四台节点的
zerotier-one、etcd、patroni均为active,etcd 健康检查全部返回{"health":"true"};pg-20重新以 Replica 身份加入,状态为streaming,复制延迟为 0 MB。 - 开机约 12.2 秒时,etcd 开始绑定
-
VIP 消失:keepalived 服务被禁用 + notify.sh 残留旧接口名
现象:集群中三台主机的 keepalived 均已安装配置,但ip addr show ztpp6n6xmz上看不到 VIP10.0.0.100,访问10.0.0.100:5432(写)、:5433(读)、:6443(K3s API)全部失败。排查过程:
- 三台
systemctl is-active keepalived均为inactive,is-enabled为disabled—— keepalived 服务根本没运行; - 配置文件和脚本(
keepalived.conf、chk_haproxy.sh、notify.sh)都完好,权限正确; - 启动 keepalived 后 VIP 出现,但 haproxy 未被自动拉起,
5432仍不通。
根因有三个:
- keepalived 服务被禁用:历史操作中(如文档”停止并禁用 haproxy keepalived,由 keepalived 控制 HAProxy 启停”的某次执行)把 keepalived 也停用且未再启用,导致 VRRP 从未运行;
notify.sh中DEV="wg0"残留:从 WireGuard(接口wg0)迁移到 ZeroTier(接口ztpp6n6xmz)后,notify.sh 里的DEV没同步更新。进入 MASTER 状态时执行ip addr show wg0失败 →exit 1→ 永远不会启动 haproxy;priority配置与设计不符:实际为 server=100、txy=100(平级)、aly=30,无法保证 server 优先。应改为 server=100、txy=90、aly=80。
修复步骤:
# 1. 修正三台 notify.sh 的接口名(wg0 → ztpp6n6xmz)sudo sed -i 's/DEV="wg0"/DEV="ztpp6n6xmz"/' /etc/keepalived/notify.sh# 2. 修正优先级(server=100、txy=90、aly=80)# 在 txy 上:sed -i 's/priority 100/priority 90/' /etc/keepalived/keepalived.conf# 在 aly 上:sed -i 's/priority 30/priority 80/' /etc/keepalived/keepalived.conf# 3. 启动并启用三台 keepalivedsudo systemctl start keepalivedsudo systemctl enable keepalived# 4. 解除 chk_haproxy 与 haproxy 绑定的双重死锁# (a) haproxy 监听 VIP,只有持有 VIP 的节点能启动它;# (b) chk_haproxy 检查 haproxy,haproxy 未起则优先级被降低、丢失 MASTER。# 因此需先手动在目标 MASTER 节点绑定 VIP 并启动 haproxy,再重启 keepalived:sudo ip addr add 10.0.0.100/32 dev ztpp6n6xmz # 在期望的 MASTER 节点上sudo systemctl reset-failed haproxy # 清除之前 bind 失败累积的 failed 状态sudo systemctl start haproxy # 现在能绑定 VIP,正常启动sudo systemctl restart keepalived # server(100) 重新竞选并稳定持有 VIP验证:
# MASTER 节点应持有 VIP(应为 server/10.0.0.10)ip addr show ztpp6n6xmz | grep 10.0.0.100# 三个端口均通timeout 3 bash -c "cat < /dev/null > /dev/tcp/10.0.0.100/5432" && echo "5432 通"timeout 3 bash -c "cat < /dev/null > /dev/tcp/10.0.0.100/5433" && echo "5433 通"timeout 3 bash -c "cat < /dev/null > /dev/tcp/10.0.0.100/6443" && echo "6443 通"IMPORTANT网络方案变更后必须检查所有引用旧接口名的脚本。从 WireGuard 切换到 ZeroTier 时,
notify.sh、keepalived.conf、haproxy.cfg中的wg0都可能残留,逐一替换为ztpp6n6xmz。chk_haproxy、notify、haproxy 绑定 VIP 三者存在循环依赖:haproxy 靠 notify 启动,chk_haproxy 又检查 haproxy,而 haproxy 只监听 VIP(无 VIP 绑定即启动失败)。首次部署时需手动ip addr addVIP +systemctl start haproxy打破死锁,此后 keepalived 即可自行管理。priority 必须严格递减且不相等:三台节点优先级需各不相同(如 100/90/80),保证明确的竞选顺序;出现平级时,谁先启动/先恢复健康谁就赢得 MASTER,无法保证指定节点优先。
- 三台
-
VIP 漂移后其他节点 ARP 缓存残留,出现 ICMP Redirect
现象:VIP 从旧 MASTER 漂移到新 MASTER(如从 txy 到 server)后,新加入集群的节点(如 WSL2 的10.0.0.20)ping 10.0.0.100能通,但输出中出现大量:64 bytes from 10.0.0.100: icmp_seq=2 ttl=64 time=32.1 msFrom 10.0.0.30 icmp_seq=2 Redirect Host(New nexthop: 10.0.0.100)原因:
notify.sh的 master 分支只启动了 haproxy,没有发送免费 ARP(gratuitous ARP)。VIP 漂移后,其他节点的 ARP 缓存仍残留旧 MASTER 的 MAC,数据包先发给旧 MASTER,旧 MASTER 回 ICMP Redirect 告知”VIP 同网段,直接发”。ip neigh中10.0.0.100甚至显示FAILED,直到缓存自然过期(STALE→FAILED→重新解析)才恢复。解决:在 notify.sh 的 master 分支加入免费 ARP 通告:
# 安装 arping(iputils-arping 提供)sudo apt install -y iputils-arping# notify.sh master 分支中加入(见 5.2 完整脚本):ip neigh flush dev $DEVarping -c 5 -A -I $DEV $VIP 2>/dev/null || \arping -c 5 -U -I $DEV $VIP 2>/dev/null || true验证:VIP 漂移后,其他节点
ip neigh应立即指向新 MASTER 的 MAC(REACHABLE),ping 10.0.0.100无 ICMP Redirect、0% 丢包。IMPORTANTVIP 漂移后必须主动通告 ARP。Keepalived 自身在 master 时会发免费 ARP,但 ZeroTier 二层网络 + 单播 VRRP 环境下可能不可靠,建议在 notify.sh 显式
arping通告,确保所有节点 ARP 缓存即时更新,避免短暂不通或 ICMP Redirect。 -
系统重启后 keepalived 因 ZeroTier 接口未就绪而启动失败
现象:主机重启后,三台 keepalived 全部failed,VIP 消失,日志报:Non-existent interface specified in configurationpid <PID> exited with permanent error CONFIG. Terminating原因:keepalived 依赖 ZeroTier 接口
ztpp6n6xmz存在才能通过配置校验。但 keepalived 的 systemd unit 只依赖network-online.target,没有声明对zerotier-one.service的依赖。重启时 ZeroTier 尚未把接口创建出来,keepalived 先启动即报接口不存在,随后因 CONFIG 错误退出(status=2/INVALIDARGUMENT)。解决:为 keepalived 添加对 zerotier-one 的启动依赖(drop-in 方式,三台节点均需):
sudo mkdir -p /etc/systemd/system/keepalived.service.dsudo tee /etc/systemd/system/keepalived.service.d/zerotier.conf << 'EOF'[Unit]After=zerotier-one.service network-online.targetWants=zerotier-one.service network-online.targetRequires=zerotier-one.serviceEOFsudo systemctl daemon-reloadIMPORTANT依赖 ZeroTier 接口的服务都需声明启动顺序。与踩坑记录 6(etcd)同理:
keepalived、patroni、etcd等绑定10.0.0.x或依赖ztpp6n6xmz的服务,都应通过 drop-in 添加After=zerotier-one.service+Requires=zerotier-one.service,否则重启后可能因接口未就绪而启动失败。首启竞态:即使依赖配好,keepalived 首次进入 MASTER 时,
chk_haproxy仍可能在 haproxy 就绪前失败,导致 notify 的systemctl start haproxy失败。若 VIP 端口不通,手动systemctl start haproxy一次即可,后续 keepalived 自行管理。
九、总结
通过切换到 ZeroTier 并配合 Keepalived + HAProxy,我们在三节点虚拟二层网络上成功构建了一个更简洁、更稳定的接入层:
- PostgreSQL 读写分离 完全自动化,无需应用修改连接逻辑,跟随 Patroni 角色动态调整。
- K3s API Server 高可用 隐藏了后端节点细节,任意节点宕机不影响 kubectl 访问。
- VIP 高可用 利用 ZeroTier 的二层多播能力,直接使用标准 VRRP,无需单播,配置极简,毫秒级切换。
这套方案轻量、无外部依赖,非常适合中小规模的自建高可用集群。如果后续需要增强读一致性,还可以基于 Patroni 的 /sync 端点添加同步读端口。希望本文能帮助你在类似场景下快速落地。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时
湘公网安备43010402002198号