fix(keepalived): REVERT preempt_delay → nopreempt (Prod-Ausfall 2026-08-03) — v1.3.15
v1.3.9 hatte auf dem PG-Primary (Prio 200) nopreempt durch preempt_delay ersetzt, damit die VIP zum Primary heimwandert. Das reaktivierte GENAU den Fehlermodus, den nopreempt verhindert: der Prio-200-Node holt die VIP zurück, sobald keepalived ihn für gesund hält — aber die Track-Scripts können "gesund" melden, während der Dienst kaputt ist. Am 2026-08-03 entriss so ein halb-kaputtes utm-1 dem funktionierenden utm-2 die VIP (Log: "Master received advert from .6 with higher priority 200 → Entering BACKUP") und hielt sie fest → Ausfall, bis utm-1 hart abgeschaltet wurde. Zurück auf nopreempt (beide Nodes, wie vor v1.3.9). VIP-Affinität zum Primary erst wieder, wenn der Health-Check "Prozess up aber Dienst kaputt" als FAULT erkennt. Bis dahin: Stabilität > Affinität. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,52 +17,35 @@ func render(t *testing.T, v View) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// testView repräsentiert den bevorzugten Node (PG-Primary, Prio 200):
|
||||
// preempt_delay statt nopreempt (holt die VIP nach stabiler Erholung heim).
|
||||
// Kein Node startet MASTER (sonst würde preempt_delay/nopreempt ignoriert).
|
||||
func testView() View {
|
||||
return View{
|
||||
State: "BACKUP", Interface: "eth0", RouterID: 51, Priority: 200,
|
||||
PreemptDelay: preemptDelaySeconds,
|
||||
SrcIP: "89.163.205.6", PeerIP: "89.163.205.8", AuthPass: "edgeguard",
|
||||
SrcIP: "89.163.205.6", PeerIP: "89.163.205.8", AuthPass: "edgeguard",
|
||||
VIPs: []VIPEntry{{Address: "89.163.205.100", Prefix: 24, Device: "eth0"}},
|
||||
HBInterface: "ens19", HBSrcIP: "169.254.0.1", HBPeerIP: "169.254.0.2", HBRouterID: 52,
|
||||
GWCheckIP: "89.163.205.1",
|
||||
}
|
||||
}
|
||||
|
||||
// Bevorzugter Node: preempt_delay auf beiden Instanzen, KEIN nopreempt.
|
||||
func TestTemplatePreemptDelayOnPreferred(t *testing.T) {
|
||||
// Split-Brain-Schutz (Incident 2026-08-03): BEIDE Instanzen MÜSSEN `nopreempt`
|
||||
// tragen, sonst reißt ein erholter-aber-evtl-kaputter Node die VIP zurück.
|
||||
// nopreempt wirkt nur bei state BACKUP — also darf kein Node MASTER starten.
|
||||
func TestTemplateNopreemptOnBothInstances(t *testing.T) {
|
||||
out := render(t, testView())
|
||||
if n := strings.Count(out, "preempt_delay"); n != 2 {
|
||||
t.Fatalf("erwarte preempt_delay in VI_1 UND VI_HB (2×), gefunden: %d\n%s", n, out)
|
||||
if n := strings.Count(out, "nopreempt"); n != 2 {
|
||||
t.Fatalf("erwarte nopreempt in VI_1 UND VI_HB (2×), gefunden: %d\n%s", n, out)
|
||||
}
|
||||
if strings.Contains(out, "nopreempt") {
|
||||
t.Fatalf("bevorzugter Node (Prio 200) darf KEIN nopreempt tragen:\n%s", out)
|
||||
if strings.Contains(out, "preempt_delay") {
|
||||
t.Fatalf("KEIN preempt_delay erlaubt (Incident 2026-08-03):\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "state MASTER") {
|
||||
t.Fatalf("kein Node darf state MASTER starten:\n%s", out)
|
||||
t.Fatalf("kein Node darf state MASTER starten (nopreempt würde ignoriert):\n%s", out)
|
||||
}
|
||||
if c := strings.Count(out, "state BACKUP"); c != 2 {
|
||||
t.Fatalf("erwarte state BACKUP in beiden Instanzen, gefunden: %d", c)
|
||||
}
|
||||
}
|
||||
|
||||
// Standby (Prio 100, PreemptDelay=0): nopreempt auf beiden Instanzen —
|
||||
// reißt die VIP nie an sich (Split-Brain-Schutz).
|
||||
func TestTemplateNopreemptOnStandby(t *testing.T) {
|
||||
v := testView()
|
||||
v.Priority = 100
|
||||
v.PreemptDelay = 0
|
||||
out := render(t, v)
|
||||
if n := strings.Count(out, "nopreempt"); n != 2 {
|
||||
t.Fatalf("Standby: erwarte nopreempt 2×, gefunden: %d\n%s", n, out)
|
||||
}
|
||||
if strings.Contains(out, "preempt_delay") {
|
||||
t.Fatalf("Standby darf KEIN preempt_delay tragen:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// GARP muss forciert + periodic aufgefrischt werden, sonst altert die
|
||||
// VIP-MAC am Upstream-Switch und die Failover-IP wird unerreichbar.
|
||||
func TestTemplateGARPRefresh(t *testing.T) {
|
||||
@@ -113,12 +96,11 @@ func TestBuildViewStateAlwaysBackup(t *testing.T) {
|
||||
cases := []struct {
|
||||
pgRole, role string
|
||||
wantPrio int
|
||||
wantPreempt int // preempt_delay (bevorzugt) bzw. 0 (=nopreempt)
|
||||
}{
|
||||
{"primary", "primary", 200, preemptDelaySeconds},
|
||||
{"standby", "primary", 100, 0},
|
||||
{"", "primary", 200, preemptDelaySeconds},
|
||||
{"", "", 100, 0},
|
||||
{"primary", "primary", 200},
|
||||
{"standby", "primary", 100},
|
||||
{"", "primary", 200},
|
||||
{"", "", 100},
|
||||
}
|
||||
for _, c := range cases {
|
||||
local := &models.HANode{ID: "n1", PGRole: c.pgRole, Role: c.role, PublicIP: &pub}
|
||||
@@ -129,8 +111,5 @@ func TestBuildViewStateAlwaysBackup(t *testing.T) {
|
||||
if v.Priority != c.wantPrio {
|
||||
t.Errorf("pg_role=%q role=%q: Priority=%d, erwarte %d", c.pgRole, c.role, v.Priority, c.wantPrio)
|
||||
}
|
||||
if v.PreemptDelay != c.wantPreempt {
|
||||
t.Errorf("pg_role=%q role=%q: PreemptDelay=%d, erwarte %d", c.pgRole, c.role, v.PreemptDelay, c.wantPreempt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user