fix(cluster): selbstheilende public_ip + advert_int 2 (Flapping) — v1.2.107

Zwei keepalived-Flapping-Restursachen:
1) Strukturell: der Cluster-Push (autoRegister, Primary→Secondary) trägt KEINE
   public_ip → der Empfänger AgentRegisterPeer ließ sie NULL → Peer fehlte im
   nft-peer_ipv4-Set → VRRP-Adverts (eth0/VI_1) nur via conntrack → Flapping.
   (utm-1 lernte utm-2 korrekt via Joiner-Client-IP in preRegisterJoiner; die
   Gegenrichtung fehlte.) Fix: AgentRegisterPeer fällt bei leerem req.PublicIP
   auf die mTLS-Client-IP (c.ClientIP()) zurück — die EIGEN-IP des Peers, nicht
   die VIP. Selbstheilend, überlebt Re-Joins/Failover.
2) advert_int 1 → 2 (Master-Down ~6s statt ~3s): reißt nicht mehr bei kurzen
   VM-/Heartbeat-Hiccups (VI_HB). Trade-off: Failover-Erkennung ~6s.

Tests: advert_int 2 im Render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-06-12 13:42:21 +02:00
parent b2fc7b7dee
commit f3c76f6d18
4 changed files with 23 additions and 3 deletions

View File

@@ -1 +1 @@
1.2.106 1.2.107

View File

@@ -925,6 +925,14 @@ func (h *ClusterHandler) AgentRegisterPeer(c *gin.Context) {
if req.PublicIP != "" { if req.PublicIP != "" {
v := req.PublicIP v := req.PublicIP
n.PublicIP = &v n.PublicIP = &v
} else if ip := c.ClientIP(); ip != "" {
// Der Push-Payload (autoRegister, Primary→Secondary) trägt KEINE
// public_ip → sonst bliebe sie NULL und der Peer fehlt im nft-
// peer_ipv4-Set → VRRP-Adverts nur via conntrack → Flapping. Der
// pushende Peer verbindet sich über mTLS von seiner EIGEN-IP (nicht
// der VIP — der Kernel nimmt die primäre Interface-IP als Source),
// genau wie preRegisterJoiner die Joiner-IP übernimmt. Selbstheilend.
n.PublicIP = &ip
} }
if req.InternalIP != "" { if req.InternalIP != "" {
v := req.InternalIP v := req.InternalIP

View File

@@ -44,7 +44,7 @@ vrrp_instance VI_1 {
interface {{ .Interface }} interface {{ .Interface }}
virtual_router_id {{ .RouterID }} virtual_router_id {{ .RouterID }}
priority {{ .Priority }} priority {{ .Priority }}
advert_int 1 advert_int 2
nopreempt nopreempt
{{ if .SrcIP }} unicast_src_ip {{ .SrcIP }} {{ if .SrcIP }} unicast_src_ip {{ .SrcIP }}
unicast_peer { unicast_peer {
@@ -71,7 +71,7 @@ vrrp_instance VI_HB {
interface {{ .HBInterface }} interface {{ .HBInterface }}
virtual_router_id {{ .HBRouterID }} virtual_router_id {{ .HBRouterID }}
priority {{ .Priority }} priority {{ .Priority }}
advert_int 1 advert_int 2
nopreempt nopreempt
{{ if .HBSrcIP }} unicast_src_ip {{ .HBSrcIP }} {{ if .HBSrcIP }} unicast_src_ip {{ .HBSrcIP }}
unicast_peer { unicast_peer {

View File

@@ -54,6 +54,18 @@ func TestTemplateGARPRefresh(t *testing.T) {
} }
} }
// advert_int 2 (statt 1): Master-Down ~6s — reißt nicht bei kurzen
// VM-/Heartbeat-Hiccups (Flapping-Schutz im virtualisierten Cluster).
func TestTemplateAdvertInt(t *testing.T) {
out := render(t, testView())
if strings.Contains(out, "advert_int 1\n") {
t.Fatalf("advert_int sollte 2 sein (nicht 1):\n%s", out)
}
if c := strings.Count(out, "advert_int 2"); c != 2 {
t.Fatalf("erwarte advert_int 2 in beiden Instanzen, gefunden: %d", c)
}
}
// Track-Scripts dürfen KEIN weight haben: in einer vrrp_sync_group ignoriert // Track-Scripts dürfen KEIN weight haben: in einer vrrp_sync_group ignoriert
// keepalived gewichtete Scripts → Health-Checks wären wirkungslos. Ohne weight // keepalived gewichtete Scripts → Health-Checks wären wirkungslos. Ohne weight
// wirken sie als FAULT-Trigger. // wirken sie als FAULT-Trigger.