Wirtualny adres klastra, który skacze: skutki, przyczyny i diagnoza
Adres API przeskakuje między węzłami, a za nim restartują się wtyczka sieciowa, kontrolery i DNS. Jak odróżnić problem samego kube-vip od wolnego etcd, sieci i zasobów węzła oraz co zmienić, żeby adres przestał skakać.
Raz na kilka dni pipeline wdrożeniowy kończy się błędem „Unable to connect to the server: dial tcp 192.0.2.10:6443: i/o timeout”, a ponowne uruchomienie przechodzi. Licznik restartów kube-vip rośnie o kilka w tygodniu. Pewnej nocy, w trakcie kopii zapasowej maszyn wirtualnych, adres API przeskakuje trzy razy w dziesięć minut: wtyczka sieciowa restartuje się na wszystkich węzłach, CoreDNS przestaje zgłaszać gotowość, a aplikacje zwracają błędy rozwiązywania nazw.
Skaczący adres wirtualny rzadko jest problemem samym w sobie. Zwykle jest pierwszym widocznym objawem wolnego etcd, zbyt krótkiej dzierżawy, braku zasobów albo kłopotów z ARP. Wybór mechanizmu adresu i konfigurację klientów opisuje wpis Wirtualny adres API klastra; ten pokazuje, jak ustalić, która warstwa zawodzi.
Objawy
kubectli zadania CI co jakiś czas zwracają „The connection to the server 192.0.2.10:6443 was refused” albo przekroczenie czasu.- Liczniki restartów kube-vip rosną na kilku węzłach, a adres zmienia węzeł bez restartu węzłów.
- W tych samych minutach restartują się agenty wtyczki sieciowej, kontrolery albo operatory.
Po utracie dzierżawy kube-vip usuwa adres i kończy działanie, więc liczba jego restartów jest zgrubnym licznikiem przeskoków.
Kaskada: dlaczego kilka sekund to za dużo
Pody łączą się z API przez usługę kubernetes (ClusterIP), która prowadzi do rzeczywistych adresów serwerów API. Adresu wirtualnego używają klienci spoza klastra i komponenty, którym wpisano go jawnie: agent wtyczki sieciowej (zwłaszcza zastępującej kube-proxy), a w kubeadm także kubelet i kube-proxy.
- Lider kube-vip nie odnawia dzierżawy na czas, usuwa adres i restartuje się. Inny węzeł przejmuje adres po czasie zbliżonym do długości dzierżawy.
- Przez ten czas, a przy problemach z ARP dłużej, API pod adresem wirtualnym nie odpowiada.
- Agent wtyczki sieciowej traci API. Zależnie od wtyczki nie przechodzi sondy żywotności, jest uruchamiany ponownie i przy starcie czeka na API. Nowe pody nie dostają sieci.
- Pody CoreDNS startujące w tym czasie nie zgłaszają gotowości bez synchronizacji z API. Aplikacje dostają błędy nazw i połączeń.
Równolegle działa druga gałąź: jeśli przyczyną jest wolne etcd, dzierżaw nie odnawiają też kube-controller-manager, kube-scheduler i operatory z wyborem lidera, choć nie korzystają z adresu wirtualnego.
Przyczyny
Zależność od samego API. Dzierżawa (obiekt Lease) jest zapisywana przez API w etcd, więc spowolnienie API lub etcd trafia w mechanizm, który ma utrzymywać dostęp do API. W trybie --inCluster kube-vip łączy się z API przez usługę kubernetes, czyli także przez reguły kube-proxy albo wtyczki sieciowej.
Zbyt krótka dzierżawa. Domyślnie kube-vip używa 5 s długości dzierżawy, 3 s terminu odnowienia i 1 s między próbami, więc lider traci adres po około 3 sekundach bez udanego odnowienia. kube-controller-manager i kube-scheduler domyślnie używają 15, 10 i 2 sekund.
Opóźnienia etcd. etcd zatwierdza zapis po synchronicznym zapisie na dysk (fsync). Dysk współdzielony z kopiami zapasowymi, przeciążony procesor albo czas procesora zabierany maszynie wirtualnej przez hiperwizor wydłużają to z milisekund do sekund. Defragmentacja etcd na czas trwania blokuje danego członka.
Brak zasobów i priorytetu poda. Pod kube-vip bez żądań zasobów przegrywa rywalizację o procesor, a z limitem CPU podlega dławieniu. Odnowienie spóźnia się, choć API odpowiada.
ARP. Nowy właściciel ogłasza adres pakietem gratuitous ARP. Router lub zapora, która go ignoruje, wysyła ruch na adres MAC starego węzła, dopóki wpis nie wygaśnie. Adres pozostawiony na starym węźle daje dwie maszyny z tym samym adresem.
Zegar. Wybór lidera mierzy upływ czasu lokalnie, więc stałe przesunięcie zegarów mu nie szkodzi. Szkodzi wstrzymanie maszyny wirtualnej (zapis stanu, migracja) dłuższe niż dzierżawa.
Diagnoza krok po kroku
Czy kube-vip traci dzierżawę
kubectl -n kube-system get pods -o wide | grep kube-vip
kubectl -n kube-system logs <pod-kube-vip> --previous --timestamps | grep -iE 'lease|lock|leader' | tail -n 20
kubectl -n kube-system get pod <pod-kube-vip> \
-o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{" "}{.status.containerStatuses[0].lastState.terminated.finishedAt}{"\n"}'
Komunikaty „error retrieving resource lock” i „failed to renew lease” (z biblioteki client-go) oznaczają, że API nie obsłużyło odnowienia na czas. OOMKilled wskazuje na limit pamięci.
Czy problem dotyczy tylko kube-vip
Dzierżawy komponentów z wyborem lidera leżą w kube-system, a pole leaseTransitions liczy zmiany właściciela.
kubectl -n kube-system get lease \
-o custom-columns=NAME:.metadata.name,HOLDER:.spec.holderIdentity,TRANSITIONS:.spec.leaseTransitions,ACQUIRED:.spec.acquireTime
NAME HOLDER TRANSITIONS ACQUIRED
kube-controller-manager k8s-cp-02_4f6c0e1a-2b7d-4c1e-9a55-0d3e8f61b2c4 37 2026-10-14T02:07:41.512033Z
kube-scheduler k8s-cp-01_9b2d77c3-5e10-4a8f-b6d2-71c9a0e4f3d8 35 2026-10-14T02:07:43.208761Z
plndr-cp-lock k8s-cp-03 52 2026-10-14T02:07:39.917420Z
plndr-cp-lock to domyślna nazwa dzierżawy adresu API w kube-vip. W przykładzie trzy dzierżawy zmieniły właściciela w ciągu czterech sekund: wspólną przyczyną jest API lub etcd, a kube-vip jest jedną z ofiar. Jeśli rośnie tylko licznik plndr-cp-lock, przyczyna leży bliżej kube-vip: w dzierżawie, zasobach poda albo sieci.
etcd, API i węzeł
W k3s API, kontrolery i wbudowany etcd piszą do dziennika usługi k3s; w kubeadm etcd działa jako statyczny pod.
# k3s, na każdym serwerze
journalctl -u k3s --since "2 hours ago" --no-pager \
| grep -E 'apply request took too long|slow fdatasync|failed to send out heartbeat|leaderelection lost|failed to renew lease'
# kubeadm
kubectl -n kube-system logs etcd-k8s-cp-01 --since=2h | grep -E 'apply request took too long|slow fdatasync'
journalctl -u kubelet --since "2 hours ago" --no-pager | grep -i 'lease'
Pojedyncze „apply request took too long” zdarzają się przy dużych zapisach. Serie „slow fdatasync” w minutach przeskoków wskazują dysk.
kubectl get --raw='/readyz/etcd'; echo
time kubectl get --raw='/readyz' >/dev/null
vmstat 1 10 # kolumny wa (oczekiwanie na dysk) i st (czas zabrany przez hiperwizor)
timedatectl | grep -E 'synchronized|NTP service'
st stale powyżej kilku procent (jeśli platforma je raportuje) albo wa powyżej kilkunastu w czasie przeskoków oznacza, że węzeł czeka na hosta lub dysk.
Sieć i ARP
Po przeskoku klient powinien w ciągu sekund widzieć adres MAC nowego węzła (ip -br link show dev eth0 na tym węźle). Stary adres MAC widoczny przez minuty oznacza ignorowany gratuitous ARP.
ip neigh show 192.0.2.10
Get-NetNeighbor -IPAddress 192.0.2.10
Objaw, przyczyna, sprawdzenie
| Objaw | Prawdopodobna przyczyna | Co sprawdzić |
|---|---|---|
| Restarty kube-vip, „failed to renew lease” w logach | wolne API lub etcd, krótka dzierżawa | dzierżawy innych komponentów, ostrzeżenia etcd |
| Wszystkie dzierżawy zmieniają właściciela w tych samych sekundach | etcd lub API, najczęściej dysk | „slow fdatasync”, harmonogram kopii |
Zmienia się tylko plndr-cp-lock | zasoby lub priorytet poda, krótka dzierżawa | requests, priorityClassName, zmienne vip_* |
| Po przeskoku adres długo nie odpowiada | ignorowany gratuitous ARP | ip neigh, tablica ARP routera |
arping pokazuje dwa adresy MAC | adres na dwóch maszynach | ip -br addr na węzłach |
| Przeskoki po zapisie stanu lub migracji maszyny | wstrzymanie lidera dłuższe niż dzierżawa | dziennik hiperwizora |
Wysokie st lub wa w czasie przeskoków | przeciążony host lub dysk | vmstat, obciążenie hosta |
Środki zaradcze
Dzierżawa z zapasem
Zmienne vip_leaseduration, vip_renewdeadline i vip_retryperiod ustawia się w kontenerze kube-vip. client-go wymaga, żeby długość dzierżawy przekraczała termin odnowienia, a termin odnowienia 1,2 × odstęp między próbami.
| Profil (długość / odnowienie / próby) | Lider traci adres po zatrzymaniu API dłuższym niż | Przejęcie adresu po awarii węzła (orientacyjnie) |
|---|---|---|
| 5 / 3 / 1 s (domyślny) | ok. 3 s | ok. 5–6 s |
| 15 / 10 / 2 s (jak kube-controller-manager) | ok. 10 s | ok. 15–17 s |
kubectl -n kube-system get pod <pod-kube-vip> \
-o jsonpath='{range .spec.containers[0].env[*]}{.name}={.value}{"\n"}{end}' | grep -E '^vip_(leaseduration|renewdeadline|retryperiod)='
Dłuższa dzierżawa nie usuwa przyczyny, tylko daje zapas na krótkie zatrzymania etcd.
Priorytet i zasoby poda
Żądania zasobów zapewniają kube-vip procesor przy przeciążeniu węzła, a klasa priorytetu zmniejsza ryzyko usunięcia poda. Limit CPU jest celowo pominięty, bo dławienie opóźnia odnowienie.
spec:
template:
spec:
priorityClassName: system-node-critical
containers:
- name: kube-vip
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
memory: 256Mi
W statycznym podzie te same pola trafiają do manifestu poda. kubectl -n kube-system get pod <pod-kube-vip> -o jsonpath='{.spec.priorityClassName}{" "}{.status.qosClass}' powinno zwrócić system-node-critical Burstable, a nie BestEffort.
etcd na szybkim dysku
Katalog danych etcd (k3s: /var/lib/rancher/k3s/server/db/etcd, kubeadm: /var/lib/etcd) powinien leżeć na dysku SSD lub NVMe, którego nie obciążają kopie zapasowe. Kopie i snapshoty maszyn control plane rozkłada się w czasie, żeby nie obejmowały wszystkich węzłów naraz.
Osobne role i mechanizm niezależny od API
Gdy kube-vip z parametrem --services obsługuje też adresy usług LoadBalancer, restart po utracie dzierżawy API przerywa również je. Adresy usług lepiej powierzyć innemu mechanizmowi, np. ogłaszaniu L2 w Cilium albo MetalLB.
Gdy etcd nie da się szybko przyspieszyć, keepalived na węzłach albo para HAProxy z keepalived usuwa zależność od API: wolne etcd spowalnia API, ale nie przenosi adresu (oba warianty). Rozwiązaniem nie jest wpisanie wtyczce sieciowej adresu jednego węzła: przeskoki znikną, ale awaria tego węzła stanie się awarią sieci klastra.
Monitorowanie zmian właściciela
Alerty obejmują restarty kube-vip (kube-state-metrics) i opóźnienie zapisu etcd (w kubeadm i k3s metryki etcd są lokalnie pod http://127.0.0.1:2381/metrics):
increase(kube_pod_container_status_restarts_total{namespace="kube-system", pod=~"kube-vip.*"}[1h]) > 0
histogram_quantile(0.99, sum by (instance, le) (rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m]))) > 0.01
Do tego sonda spoza klastra co 10–15 sekund na https://192.0.2.10:6443/readyz i cykliczny odczyt .spec.leaseTransitions dzierżawy plndr-cp-lock z alertem przy każdym wzroście.
Sprawdź u siebie
Pierwszy blok pokazuje pody w kube-system, najczęściej restartowane na końcu.
kubectl -n kube-system get pods -o wide --sort-by='.status.containerStatuses[0].restartCount'
Dobry wynik: restarty kube-vip i agentów wtyczki sieciowej odpowiadają planowanym pracom. Zły: restarty kube-vip na kilku węzłach i agentów wtyczki z podobnym czasem „… ago”.
Drugi blok pokazuje właściciela dzierżawy adresu i błędy odnowienia z ostatniej doby w każdym podzie kube-vip.
kubectl -n kube-system get lease plndr-cp-lock \
-o jsonpath='{.spec.holderIdentity}{" "}{.spec.leaseTransitions}{" "}{.spec.renewTime}{"\n"}'
for p in $(kubectl -n kube-system get pods -o name | grep kube-vip); do
printf '%s: ' "$p"; kubectl -n kube-system logs "$p" --since=24h | grep -ciE 'error retrieving resource lock|failed to renew lease'
done
renewTime powinien być sprzed kilku sekund, leaseTransitions następnego dnia nie powinien wzrosnąć bez prac serwisowych, a liczniki błędów powinny wynosić 0. NotFound oznacza inną nazwę dzierżawy (lista: kubectl -n kube-system get lease).
Trzeci blok sprawdza, który węzeł trzyma adres i kto odpowiada na niego w sieci.
# Na węzłach control plane: adres powinien być dokładnie na jednym
for n in k8s-cp-01 k8s-cp-02 k8s-cp-03; do
printf '%s: ' "$n"; ssh "$n" "ip -br -4 addr show | grep -F '192.0.2.10/'" || echo 'brak adresu'
done
# Z innej maszyny w tym samym segmencie (iputils-arping); wysyła tylko zapytania ARP, niczego nie zmienia
sudo arping -c 3 -I eth0 192.0.2.10
Dobry wynik: jeden węzeł z wierszem w rodzaju eth0 UP 192.0.2.11/24 192.0.2.10/32, pozostałe „brak adresu”, a odpowiedzi arping z jednym adresem MAC, zgodnym z interfejsem tego węzła. Dwa różne adresy MAC oznaczają adres na dwóch maszynach.
Czwarty blok przez minutę mierzy odpowiedź API pod adresem wirtualnym.
for i in $(seq 1 60); do
curl -k -s -o /dev/null --max-time 3 -w "$(date +%T) %{http_code} %{time_total}s\n" https://192.0.2.10:6443/readyz
sleep 1
done
Dobry wynik: stały kod (200 albo 401 przy wyłączonym dostępie anonimowym, jak w k3s) i czasy rzędu dziesiątek milisekund. 000 oznacza brak odpowiedzi, a czasy w sekundach wolne API lub etcd.