Gossip Protocol Ghost Nodes: IP Reuse Strašiaci Váš Cluster
Ghost nody su zvlastny druh strasidelnosti, hlavne ked sa recykluju IP. “Nový pod naplánovaný, ale existujúce pody odmietajú s ním komunikovať.” Príčina: nový pod dostal IP ktorú používal predchádzajúci pod, a gossip protokol si stále pamätá túto IP ako “failed” alebo patriacu inému nodu.
Prostredie: Consul/Serf/Memberlist gossip, Kubernetes s agresívnym pod recyclingom, NAT alebo IP address pool reuse
Problém
Incident Odmietnutého Nodu
Časová os:
T+0s Pod A (10.0.1.50) sa pripája ku clusteru, gossip zdravý
T+60s Pod A padne natvrdo (bez graceful leave)
T+61s Gossip protokol označí 10.0.1.50 ako "failed"
T+62s Ostatné nody pridajú 10.0.1.50 do suspicion listu
T+65s IP Pod A sa vracia do Kubernetes IP poolu
T+120s Pod B štartuje, dostáva IP 10.0.1.50 (rovnaká IP!)
T+121s Pod B sa pokúša pripojiť ku gossip clusteru
T+122s Existujúce nody: "10.0.1.50? To je ten mŕtvy node!"
Odmietajú join pokusy Pod B alebo smerujú traffic
na cached stav pre "starý" 10.0.1.50
Výsledok: Pod B je izolovaný napriek tomu že je zdravý
Symptómy
# Logy nového podu ukazujú:
# "Failed to join cluster: membership rejected"
# "No response from seed nodes"
# "Connection refused by peer"
# Logy existujúcich podov ukazujú:
# "Received message from failed node 10.0.1.50"
# "Ignoring join from node with conflicting incarnation"
# "Suspect node attempting to rejoin"
# Consul/Serf špecifické:
consul members
# Ukazuje OBA starý (failed) aj nový node s rovnakou IP!
# node-abc-old 10.0.1.50:8301 failed
# node-xyz-new 10.0.1.50:8301 alive (ale nedostáva traffic)
Príčina
Gossip Protocol State Machine
Gossip node lifecycle:
┌─────────────────────────────────────────────────────────────┐
│ ALIVE → SUSPECT → DEAD → (odstránený po timeoutu) │
│ │
│ Problém: IP reuse nastáva PREDTÝM než je dead node │
│ odstránený │
│ │
│ T+0: Node A (10.0.1.50) = ALIVE, incarnation=1 │
│ T+60: Node A = SUSPECT │
│ T+90: Node A = DEAD (stále trackovaný 5 minút!) │
│ T+120: Node B (10.0.1.50) = ??? konflikt! │
│ │
│ Gossip vidí: Rovnaká IP, iné meno nodu, nižšie │
│ incarnation číslo → musí byť staré/stale │
└─────────────────────────────────────────────────────────────┘
Konflikty Incarnation Numbers
// Gossip protokoly používajú incarnation numbers na detekciu
// ktorá informácia o node je novšia
type Node struct {
Name string
Addr net.IP
Incarnation uint32 // Monotónne rastúce
}
// Keď nový node B sa pripojí s rovnakou IP ako mŕtvy node A:
// Node A mal incarnation=5 keď zomrel
// Node B štartuje čerstvý s incarnation=1
// Ostatné nody myslia: "incarnation 1 < incarnation 5"
// "Toto musí byť stale/stará informácia, ignoruj"
// Ešte horšie: ak nody cachovali A's stav s vysokým incarnation,
// budú odmietať B's legitímne správy ako "zastarané"
Kubernetes IP Pool Dynamika
# Kubernetes IP alokácia je agresívna v reuse
# Malé IP ranges = rýchlejší reuse
spec:
podCIDR: 10.0.1.0/28 # Len 14 použiteľných IP!
# Rýchly pod churn = častý IP recycling
# Predstav si: 100 podov, 14 IP, pody reštartujú každých 10 minút
# IP kolízia je GARANTOVANÁ
# CNI pluginy sa líšia v reuse správaní:
# - Calico: Používa IPAM s dlhšími hold časmi
# - Flannel: Agresívnejší reuse
# - AWS VPC CNI: Limitovaný ENI/IP kvótami
Diagnostika
Skontroluj Duplicitné Node Záznamy
# Consul
consul members -detailed | grep -E "(failed|left)" | awk '{print $2}' | sort | uniq -d
# Serf
serf members | awk '{print $2}' | cut -d: -f1 | sort | uniq -d
# Memberlist (cez aplikáciu)
curl localhost:7946/debug/members | jq '.[] | .Addr' | sort | uniq -d
Porovnaj Node Start Časy
# Ak dva záznamy zdieľajú IP, skontroluj ich join časy
consul members -detailed | grep "10.0.1.50"
# Mal by ukazovať jeden záznam; viac = ghost node problém
# Skontroluj skutočný start čas podu
kubectl get pod -o jsonpath='{.status.startTime}' pod-name
# Porovnaj s gossip's zaznamenaným join časom
# Ak gossip si myslí že node sa pripojil PRED štartom podu = stale záznam
Riešenie
Možnosť 1: Graceful Leave pri Pod Shutdown
# Kubernetes: Pridaj preStop hook pre graceful gossip leave
spec:
containers:
- name: app
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
# Povedz gossipu aby graceful odišiel
curl -X POST localhost:8500/v1/agent/leave
# Alebo pre memberlist-based:
kill -SIGTERM 1
sleep 5 # Daj čas na propagáciu leave
// Aplikačný kód: Spracuj shutdown gracefully
func main() {
// ... setup memberlist ...
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh
log.Println("Vypínam, opúšťam cluster...")
// Graceful leave broadcastuje všetkým nodom
if err := memberlist.Leave(10 * time.Second); err != nil {
log.Printf("Nepodarilo sa čisto odísť: %v", err)
}
memberlist.Shutdown()
}
Možnosť 2: Unikátny Node Identifier (Nie IP-Based)
// PRED: Node identita viazaná na IP
config.Name = fmt.Sprintf("node-%s", ip)
// PO: Použi unikátny identifikátor ktorý prežije IP zmeny
config.Name = fmt.Sprintf("node-%s-%d", hostname, time.Now().UnixNano())
// Alebo použi pod UID:
config.Name = os.Getenv("POD_UID") // Nastavené cez downward API
// Takto nový node s rovnakou IP má inú identitu
// Gossip ho vidí ako úplne nový node, nie konfliktný
# Kubernetes: Injektuj pod UID ako node name
env:
- name: POD_UID
valueFrom:
fieldRef:
fieldPath: metadata.uid
- name: GOSSIP_NODE_NAME
value: "$(POD_UID)"
Možnosť 3: Rýchlejší Dead Node Pruning
// Zníž čas ako dlho dead nody zostávajú v membership
config := memberlist.DefaultConfig()
// Default: Dead nody zostávajú 30 sekúnd
// Zníž pre fast-recycling prostredia:
config.GossipToTheDeadTime = 5 * time.Second
// Rýchlejšia failure detekcia (tradeoff: viac false positives)
config.ProbeInterval = 500 * time.Millisecond
config.ProbeTimeout = 200 * time.Millisecond
config.SuspicionMult = 2 // Default je 4
Možnosť 4: IP Lease Rozšírenie
# Calico: Predĺž IP hold time po zmazaní podu
apiVersion: crd.projectcalico.org/v1
kind: IPPool
metadata:
name: default-pool
spec:
cidr: 10.0.0.0/16
# Drž IP rezervované dlhšie po uvoľnení
# Dáva gossipu čas na prune dead záznamov
Monitoring
groups:
- name: gossip-health
rules:
- alert: GossipDuplicateNodes
expr: |
count by (ip) (gossip_member_info) > 1
for: 1m
labels:
severity: warning
annotations:
summary: "Viaceré gossip nody zdieľajú IP {{ $labels.ip }}"
- alert: GossipNodeRejections
expr: |
rate(gossip_join_rejections_total[5m]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Gossip cluster odmieta join pokusy"
Checklist
## Gossip Ghost Nodes
### Symptómy
- [ ] Nové pody sa nemôžu pripojiť k existujúcemu clusteru
- [ ] Gossip ukazuje viacero nodov s rovnakou IP
- [ ] "Conflicting incarnation" chyby v logoch
- [ ] Sieť funguje ale gossip membership zlyháva
### Diagnostika
- [ ] Skontroluj duplicitné IP záznamy v membership
- [ ] Porovnaj pod start time s gossip join time
- [ ] Zapni gossip debug logging
- [ ] Skontroluj IP pool size vs pod churn rate
### Riešenia
- [ ] Implementuj graceful leave pri shutdown
- [ ] Použi unikátny node ID (pod UID) nie IP-based name
- [ ] Zníž dead node retention time
- [ ] Predĺž IP lease/hold time
- [ ] Monitoruj duplicitné membership záznamy
Záver
Lekcia: gossip protokoly predpokladajú že node identita je stabilná, ale Kubernetes IP alokácia toto negarantuje. “Ghost” z mŕtveho podu môže strašiť jeho IP adresu minúty po smrti.
Kľúčové princípy:
- Node identita by nemala byť IP-based v dynamických prostrediach
- Graceful leave je kritický - povedz clusteru že umieraš
- Dead node pruning musí byť rýchlejší než IP reuse
- Monitoruj membership anomálie
Súvisiace články
- Conntrack Stale NAT Mapping - Ďalšia IP reuse pasca
- etcd Watch Replay Storms - Cluster koordinačné problémy
Súvisiace články
Kubernetes Ghost Connections: Zastarané Conntrack DNAT Záznamy
Service vracia zlé pod IP po škálovaní. Príčina: Linux conntrack drží DNAT záznamy dlhšie ako existujú pody, smeruje traffic na zmazané endpointy.
Ghost Pod: Prečo váš Service stále posiela traffic na mŕtve endpointy
Náhodné ECONNRESET na niektorých nodoch. Endpointy vyzerajú správne. Vinník: conntrack NAT záznamy držia dlhodobé spojenia pripnuté k podom, ktoré už neexistujú.
Kubernetes Headless Service DNS: Zastarané Záznamy Po Zmazaní Podu
Requesty idú na neexistujúce pody. Príčina: headless service DNS záznamy pretrvávajú v klient DNS cache po zmazaní podov, pred propagáciou endpoints update.
Traffic Ide na Mŕtve Pody: Conntrack Zastaralé NAT Mapovanie
Deploy spôsobuje 503 presne 2 minúty. Problém: conntrack drží NAT mapovanie na staré pod IP aj po tom čo Kubernetes odstráni endpointy.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.