Отказоустойчивый Kubernetes на BGP. Часть 8: проверка отказоустойчивости и разбор полётов

Всё построено, пора проверить на прочность. В этой части — какие отказы кто и за какое время замечает, как их воспроизвести, шпаргалка по диагностике BGP и правила обслуживания, которые я вывел из собственных ошибок.

Серия «Отказоустойчивый Kubernetes на BGP»

  1. Схема и план
  2. Готовим узлы Ubuntu 24.04
  3. BGP на роутере (FRR)
  4. kube-vip в режиме BGP и первый мастер
  5. Calico с eBPF и рабочие узлы
  6. Адреса LoadBalancer из Calico по BGP
  7. Traefik за BGP и externalTrafficPolicy Local
  8. Проверка отказоустойчивости и разбор полётов (эта статья)

Какой отказ кто закрывает

Что случилось Кто замечает За сколько
Завис или упал kube-apiserver на мастере, сам узел жив health-check kube-vip около 15 с (3 проверки по 5 с)
Упал под kube-vip или узел остановлен штатно разрыв TCP-сессии BGP практически сразу
Мастер или воркер умер «жёстко» (питание, сеть) hold-таймер BGP на роутере до 30 с
Под Traefik удалён или не проходит readiness Calico снимает /32 с этого узла по readiness
Упал calico-node на воркере разрыв сессии, затем hold-таймер сразу или до 30 с

Два общих замечания.

Во время окна обнаружения часть запросов теряется. Пока роутер не знает, что узел умер, он продолжает отправлять на него часть потоков (ECMP). Для API это терпимо: клиенты kubectl и kubelet повторяют запросы. Для сайтов — несколько секунд ошибок у части посетителей. Если это не устраивает, смотрите в сторону BFD (быстрое обнаружение отказов), он умеет работать с FRR, но я его пока не пробовал.

Рост hold не бесплатен. Таймеры 10 30 (keepalive/hold) я выбрал как компромисс. Меньше — и сессия будет падать от кратковременных потерь пакетов.


Проверка

Ниже — сценарии в том виде, в котором их стоит прогнать, и для каждого указан результат, которого нужно ожидать. Прогоняйте их до того, как положитесь на схему.

Перед экспериментами откройте в отдельном окне постоянный пробник:

while true; do
  printf '%s ' "$(date +%T)"
  curl -sk -o /dev/null -m 2 -w '%{http_code}\n' https://192.168.20.1:6443/readyz
  sleep 1
done

И второй — на роутере, чтобы видеть ECMP:

watch -n1 'ip route show 192.168.20.1; ip route show 192.168.20.65'

1. Apiserver умер, узел жив

Самый коварный сценарий: BGP-сессия цела, а API нет. Это ровно то, для чего нужен health-check.

# на k8s-master-2
sudo mv /etc/kubernetes/manifests/kube-apiserver.yaml /root/

Ожидаемо: примерно через 15 секунд kube-vip на этом мастере перестанет анонсировать маршрут (в журнале исчезнут строки announcing route), а в таблице роутера у 192.168.20.1 останется два nexthop вместо трёх. Пробник за это время может выдать несколько ошибок (треть потоков была направлена на мёртвый узел), затем снова 200.

sudo mv /root/kube-apiserver.yaml /etc/kubernetes/manifests/

Когда apiserver поднимется и health-check пройдёт, мастер вернётся в ECMP.

Это проверка, которую обязательно нужно делать до того, как вы положитесь на схему: без неё вы не узнаете, что защита от чёрной дыры реально работает.

2. Перезапуск kube-vip

sudo crictl stop $(sudo crictl ps --name kube-vip -q)

kubelet поднимет новый контейнер за секунды. Сессия на это время оборвётся, роутер уберёт мастера из ECMP, и вернёт, как только сессия поднимется. Остальные два мастера в это время продолжают обслуживать VIP.

3. Жёсткое выключение мастера

Выключаем виртуальную машину без остановки ОС (в Hyper-V — Stop-VM -TurnOff).

Ожидаемо: сессия живёт до истечения hold-таймера — до 30 секунд, потом роутер убирает узел. etcd остаётся с двумя членами из трёх, кластер работает. Не выключайте два мастера сразу: потеря кворума останавливает кластер.

4. Воркер с Traefik

kubectl drain k8s-node-1.example.com --ignore-daemonsets --delete-emptydir-data

Из-за PodDisruptionBudget Traefik отдаст не больше одного пода, а /32 с узла пропадёт вместе с подом. Пробуйте эту проверку с постоянным пробником к сайту:

while true; do curl -s -o /dev/null -m 3 -w '%{http_code} %{time_total}\n' https://www.example.com/; sleep 0.5; done

После — kubectl uncordon.


Шпаргалка по диагностике

На роутере (через vtysh)

sudo vtysh -c 'show bgp summary'                                   # соседи и их состояние
sudo vtysh -c 'show bgp ipv4 unicast'                              # что приняли
sudo vtysh -c 'show bgp ipv4 unicast 192.168.20.65'               # кто анонсирует адрес
sudo vtysh -c 'show bgp ipv4 unicast neighbors 192.168.10.20 received-routes'   # что сосед прислал
sudo vtysh -c 'show bgp ipv4 unicast neighbors 192.168.10.20 filtered-routes'   # что мы выкинули
sudo vtysh -c 'show bgp ipv4 unicast neighbors 192.168.10.20 advertised-routes' # что мы отдаём (пусто)
sudo vtysh -c 'show route-map FROM-CLUSTER'                        # счётчик Invoked
ip route show 192.168.20.65                                       # что попало в ядро
sudo vtysh -b                                                      # применить frr.conf без рестарта
sudo vtysh -c 'clear bgp 192.168.10.20 soft in'                   # переспросить, не разрывая сессию
sudo vtysh -c 'show bgp neighbor 192.168.10.20' | grep -A4 'Last reset'
sudo journalctl -u frr -n 50

Как читать show bgp summary: число в колонке State/PfxRcd — сессия жива, и столько префиксов принято; слово (Active, Connect, Idle) — сессии нет. Up/Down показывает возраст сессии: сбросилась недавно — значит, падала. В таблице show bgp ipv4 unicast строка *> — лучший путь, *= — участник ECMP.

Полный clear bgp * без soft рвёт все сессии, и для VIP API это скажется сразу. Пользуйтесь soft in.

Со стороны кластера

kubectl get tigerastatus
kubectl -n calico-system exec ds/calico-node -c calico-node -- birdcl show protocols
kubectl -n calico-system exec ds/calico-node -c calico-node -- birdcl show route export Node_192_168_10_1
kubectl get bgpconfiguration default -o yaml
kubectl get bgppeers.projectcalico.org -o yaml
kubectl -n kube-system logs kube-vip-k8s-master-1.example.com --tail=20

В журнале kube-vip ключевые строки: BGP health check passed, announcing route и BGP_FSM_ESTABLISHED.

Типовые сценарии

Сервис не отвечает снаружи. Сначала show bgp ipv4 unicast <адрес> на роутере: есть ли путь вообще. Есть — смотреть проброс и ip route show. Нет — идти в кластер: жив ли под, поднята ли сессия на его узле.

Отвечает с перебоями. Для сервисов с Local похоже на ECMP на узлы без пода. Проверить, что serviceLoadBalancerAggregation: Disabled, и сравнить список путей в show bgp с тем, где реально живут поды.

show bgp пуст при живых сессиях. Опечатка в имени route-map (при включённой bgp ebgp-requires-policy) — маршруты молча отбрасываются. Смотреть счётчик Invoked у route-map.

Сервис завис в <pending>. Смотреть логи calico-kube-controllers: скорее всего, аннотация loadBalancerIPs записана строкой, а не JSON-массивом.

С узла адрес сервиса «не пингуется» и ip route get ругается. Это нормально, смотрите часть 6: проверять нужно curl.

Адрес переехал, а часть узлов о нём не знает. Сервисы обслуживает eBPF, смотреть calico-node -bpf nat dump на узле — новый адрес должен быть среди фронтендов.

Имя резолвится в старый адрес. Кроме DNS проверьте /etc/hosts на самом хосте: на одном из серверов у меня адрес был зашит в /etc/hosts, и через полчаса после смены адреса сервиса агент на роутере начал сыпать ошибками «No route to host», хотя по IP всё отвечало. Если меняете адрес, ищите его не только в DNS.


Правила обслуживания

Роутер — часть control plane. Перезагрузка роутера роняет рабочие узлы в NotReady, потому что они ходят к API через него. Планируйте перезагрузки роутера как работы, затрагивающие кластер.

Узлы перезагружаем строго по одному. drain → перезагрузка → дождаться Ready → uncordon. Два мастера одновременно кластер не переживёт (потерян кворум etcd). Сначала три мастера (между ними сверяем etcdctl endpoint health --cluster — должно быть 3/3), затем воркеры. Перед началом убедитесь, что у каждого веб-сервиса есть реплики на разных узлах.

Новый узел. Одна строка neighbor <адрес> peer-group WORKERS на роутере (и vtysh -b). На стороне Calico ничего делать не нужно: BGPPeer выбирает узлы по селектору.

kube-vip обновляем по одному мастеру. Поднимаем тег в манифесте, ждём Ready и announcing route, проверяем curl -sk -o /dev/null -w '%{http_code}' https://192.168.20.1:6443/readyz и переходим к следующему.

Не обновлять узлы автоматически. Я уже рассказывал про needrestart, который однажды остановил systemd-networkd и оставил мастер без адреса на 17 часов. Узлы кластера обновляются руками в окно.

Независимость от DNS. /etc/hosts с адресом API на каждом узле — маленькая цена за то, чтобы сбой DNS не клал кластер. При смене адреса VIP не забудьте его поправить.

Метрики kube-vip доступны на узле на :2112/metrics. Следите за памятью: на версии 1.2.4 и новее она ровная.


Что у меня сломалось по дороге

Чтобы вам не пришлось повторять:

  1. Утечка памяти в kube-vip (v1.2.1–v1.2.3): 18 МБ в час на каждом мастере, пока я не нашёл defer внутри бесконечного цикла health-check. Исправлено в 1.2.4 (kube-vip#1778).
  2. Потерянный адрес по DHCP после обновления glibc и перезапуска systemd-networkd — 17 часов недоступного мастера. Теперь статические адреса и запрет needrestart.
  3. Единственный DNS уронил весь кластер: kubelet'ы обращались к API по имени. Теперь /etc/hosts.
  4. Висячая ссылка /etc/resolv.conf на /run/systemd/resolve/ — после перезагрузки узел не мог создать ни одного пода.
  5. Per-CPU память Hyper-V (240 возможных процессоров): 1,3–1,5 ГБ на узле потрачено на карты eBPF впустую. Лечится nr_cpus.
  6. Зависания загрузок на Cluster — потери в NAT-туннеле eBPF между хостами Hyper-V. Лечится externalTrafficPolicy: Local.
  7. Разворот трафика и conntrack: ping работает, TCP-рукопожатие работает, данные не ходят.
  8. Аннотация loadBalancerIPs строкой вместо JSON-массива: сервис молча висит в <pending>.

Итог

Что получилось:

  • API кластера отвечает на одном адресе, который анонсируют три мастера, и пропадает с мёртвого узла за секунды;
  • сервисам выдаёт адреса и анонсирует их Calico — без MetalLB, без ARP и без L2-сегмента;
  • балансировка между узлами выполняется роутером (ECMP), а отказ узла — это просто исчезновение маршрута.

За это платим тем, что роутер стал частью control plane, и тем, что без правила разворота в файрволе схема молча не работает.

На этом серия заканчивается. Если вы дочитали до сюда — спасибо. Вопросы и замечания пишите в комментариях.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.