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:
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user