10 min czytania

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ć.

Rys. 1. Kilka sekund bez adresu wirtualnego wystarcza, żeby wtyczka sieciowa i kontrolery straciły API, a za nimi DNS i aplikacje; przyczyny leżą zwykle niżej: w dzierżawie, etcd albo ARP.

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

  • kubectl i 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.

  1. 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.
  2. Przez ten czas, a przy problemach z ARP dłużej, API pod adresem wirtualnym nie odpowiada.
  3. 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.
  4. 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

ObjawPrawdopodobna przyczynaCo sprawdzić
Restarty kube-vip, „failed to renew lease” w logachwolne API lub etcd, krótka dzierżawadzierżawy innych komponentów, ostrzeżenia etcd
Wszystkie dzierżawy zmieniają właściciela w tych samych sekundachetcd lub API, najczęściej dysk„slow fdatasync”, harmonogram kopii
Zmienia się tylko plndr-cp-lockzasoby lub priorytet poda, krótka dzierżawarequests, priorityClassName, zmienne vip_*
Po przeskoku adres długo nie odpowiadaignorowany gratuitous ARPip neigh, tablica ARP routera
arping pokazuje dwa adresy MACadres na dwóch maszynachip -br addr na węzłach
Przeskoki po zapisie stanu lub migracji maszynywstrzymanie lidera dłuższe niż dzierżawadziennik hiperwizora
Wysokie st lub wa w czasie przeskokówprzeciążony host lub dyskvmstat, 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 sok. 5–6 s
15 / 10 / 2 s (jak kube-controller-manager)ok. 10 sok. 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.