Отказоустойчивый Kubernetes на BGP. Часть 8: проверка отказоустойчивости и разбор полётов
Всё построено, пора проверить на прочность. В этой части — какие отказы кто и за какое время замечает, как их воспроизвести, шпаргалка по диагностике BGP и правила обслуживания, которые я вывел из собственных ошибок.
Серия «Отказоустойчивый Kubernetes на BGP»
- Схема и план
- Готовим узлы Ubuntu 24.04
- BGP на роутере (FRR)
- kube-vip в режиме BGP и первый мастер
- Calico с eBPF и рабочие узлы
- Адреса LoadBalancer из Calico по BGP
- Traefik за BGP и externalTrafficPolicy Local
- Проверка отказоустойчивости и разбор полётов (эта статья)
Какой отказ кто закрывает
| Что случилось | Кто замечает | За сколько |
|---|---|---|
Завис или упал 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 и новее она ровная.
Что у меня сломалось по дороге
Чтобы вам не пришлось повторять:
- Утечка памяти в kube-vip (v1.2.1–v1.2.3): 18 МБ в час на каждом мастере, пока я не нашёл
deferвнутри бесконечного цикла health-check. Исправлено в 1.2.4 (kube-vip#1778). - Потерянный адрес по DHCP после обновления
glibcи перезапускаsystemd-networkd— 17 часов недоступного мастера. Теперь статические адреса и запретneedrestart. - Единственный DNS уронил весь кластер: kubelet'ы обращались к API по имени. Теперь
/etc/hosts. - Висячая ссылка
/etc/resolv.confна/run/systemd/resolve/— после перезагрузки узел не мог создать ни одного пода. - Per-CPU память Hyper-V (240 возможных процессоров): 1,3–1,5 ГБ на узле потрачено на карты eBPF впустую. Лечится
nr_cpus. - Зависания загрузок на
Cluster— потери в NAT-туннеле eBPF между хостами Hyper-V. ЛечитсяexternalTrafficPolicy: Local. - Разворот трафика и conntrack: ping работает, TCP-рукопожатие работает, данные не ходят.
- Аннотация
loadBalancerIPsстрокой вместо JSON-массива: сервис молча висит в<pending>.
Итог
Что получилось:
- API кластера отвечает на одном адресе, который анонсируют три мастера, и пропадает с мёртвого узла за секунды;
- сервисам выдаёт адреса и анонсирует их Calico — без MetalLB, без ARP и без L2-сегмента;
- балансировка между узлами выполняется роутером (ECMP), а отказ узла — это просто исчезновение маршрута.
За это платим тем, что роутер стал частью control plane, и тем, что без правила разворота в файрволе схема молча не работает.
На этом серия заканчивается. Если вы дочитали до сюда — спасибо. Вопросы и замечания пишите в комментариях.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.