Отказоустойчивый Kubernetes на BGP. Часть 7: Traefik за BGP и externalTrafficPolicy Local
Механизм адресов готов. Осталось повесить на него нагрузку, ради которой всё затевалось: входящий HTTP(S)-трафик. В этой части ставим Traefik и разбираемся, почему для него важна одна строка — externalTrafficPolicy: Local.
Серия «Отказоустойчивый Kubernetes на BGP»
- Схема и план
- Готовим узлы Ubuntu 24.04
- BGP на роутере (FRR)
- kube-vip в режиме BGP и первый мастер
- Calico с eBPF и рабочие узлы
- Адреса LoadBalancer из Calico по BGP
- Traefik за BGP и externalTrafficPolicy Local (эта статья)
- Проверка отказоустойчивости и разбор полётов
Про установку Traefik в кластер я уже писал (Как установить traefik в качестве Ingress Controller в Kubernetes), здесь только то, что относится к BGP.
Установка
helm repo add traefik https://traefik.github.io/charts
helm repo update
helm upgrade --install traefik traefik/traefik \
--namespace traefik --create-namespace \
-f traefik-values.yaml
Значения, которые имеют отношение к теме:
# traefik-values.yaml
deployment:
kind: Deployment
replicas: 3
# Обязательное правило: второй под Traefik на узел не встанет,
# лишняя реплика останется в Pending.
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app.kubernetes.io/name: traefik
app.kubernetes.io/instance: traefik-traefik
# Дренаж узла не снимет больше одного пода за раз.
podDisruptionBudget:
enabled: true
minAvailable: 2
service:
annotations:
# Адрес выдаёт и анонсирует Calico. Значение — JSON-массив строк.
projectcalico.org/loadBalancerIPs: '["192.168.20.65"]'
spec:
externalTrafficPolicy: Local
internalTrafficPolicy: Cluster
Метка app.kubernetes.io/instance в чарте Traefik составляется из имени релиза и неймспейса, то есть для релиза traefik в неймспейсе traefik это traefik-traefik. Проверьте её на своём поде: kubectl -n traefik get pod --show-labels.
После установки:
kubectl -n traefik get svc traefik # EXTERNAL-IP 192.168.20.65
kubectl -n traefik get pods -o wide # три пода на трёх разных воркерах
Мастера не принимают подов Traefik, потому что у них стоит taint NoSchedule (мы его не снимали). Так и задумано: входящий трафик мастера не принимают, на них Calico не пирингуется с роутером.
Cluster или Local
От externalTrafficPolicy зависит, что произойдёт, когда пакет, адресованный сервису, приходит на узел.
Cluster (умолчание). Узел-получатель может отдать соединение любому поду сервиса, в том числе расположенному на другом узле. Тогда пакет туннелируется на узел с подом, а ответ возвращается обратно через тот же узел-получатель. В режиме eBPF это NAT-туннель (VXLAN, VNI 0xCA11C0), который сохраняет адрес клиента. Плюс — любой узел может обслужить сервис, и анонс можно делать со всех воркеров.
Local. Узел обслуживает соединение только своим локальным подом. Если пода на узле нет, пакет теряется. Поэтому анонсировать адрес должны только узлы с готовым подом — а это именно то, что обеспечивает отключённая агрегация из части 6.
Почему я перешёл на Local
Сначала Traefik у меня стоял на Cluster и работал вполне прилично. Но через некоторое время обнаружилась неприятность: примерно каждая десятая загрузка сайта снаружи зависала на секунды. В браузере это выглядело как страница, которая «думает» (в сетевых инструментах — состояние Stalled) и потом внезапно открывается.
Разбор показал два фактора. Первый — на роутере не было ограничения MSS для трафика, пришедшего из IPsec-туннелей (это отдельная история). Второй — NAT-туннель eBPF между узлами: между хостами Hyper-V в нём терялись пачки сегментов ответа (GSO), и вместо мгновенного ответа клиент ждал повторной передачи.
Переход на Local убрал межузловой хоп для входящих соединений: роутер по ECMP выбирает узел с готовым подом Traefik, и соединение обслуживает он сам — без туннеля. После перевода 50 загрузок из дома подряд прошли без единой задержки, максимум 0,16 с; до этого 5 из 40 обрывались по таймауту.
Корень проблемы с туннелем я до конца не доказал (оффлоады hv_netvsc, настройки vSwitch) — это остаётся задачей, но сайты от неё больше не зависят.
Что при этом нужно соблюдать
1. Поды на узлах, которые анонсируют адрес. При Local каждый узел из ECMP должен иметь свой готовый под. Anti-affinity с required гарантирует, что два пода не встанут на один узел (иначе пропадёт смысл: три реплики на двух узлах — это два пути, а не три). Если подов будет больше, чем узлов, лишний останется в Pending, а не упадёт на лишний узел.
2. Запас узлов для обновления. Реплик три, воркеров пять: при обновлении maxSurge поднимает новый под до того, как уйдёт старый. Если реплик было бы столько же, сколько узлов, новому поду просто негде было бы встать.
3. PodDisruptionBudget. minAvailable: 2 — дренаж узла не снимет больше одного пода Traefik за раз, и в ECMP всегда остаётся минимум два пути.
4. Deployment вместо DaemonSet. Раньше Traefik был DaemonSet'ом на всех пяти воркерах. Три реплики дают те же три пути, но занимают меньше памяти, а число узлов с подом можно менять значением replicas.
Что видно на роутере
sudo vtysh -c 'show bgp ipv4 unicast 192.168.20.65'
ip route show 192.168.20.65
Три пути — по одному на каждый воркер с подом Traefik:
192.168.20.65 proto bgp metric 20
nexthop via 192.168.10.20 dev eth1 weight 1
nexthop via 192.168.10.22 dev eth1 weight 1
nexthop via 192.168.10.24 dev eth1 weight 1
Сверьте эти адреса с kubectl -n traefik get pods -o wide: список узлов должен совпадать. Узел, который есть среди путей, но не имеет готового пода Traefik, — повод смотреть сначала под, потом calico-node.
Теперь можно посмотреть, как маршрут реагирует на события:
kubectl -n traefik delete pod <один-из-подов>
watch -n1 "ip route show 192.168.20.65"
Пока под пересоздаётся (а PDB и anti-affinity подсказывают планировщику, куда ему встать), маршрут с этого узла пропадает и появляется снова, когда под проходит readiness.
Как трафик попадает снаружи
У меня все публичные имена во внешнем DNS указывают на внешний адрес роутера, а роутер пробрасывает порты 80 и 443 (DNAT) на 192.168.20.65. Внутри сети split-horizon DNS отдаёт для тех же имён сразу 192.168.20.65.
Маршрут клиента из интернета симметричен: пакет приходит на роутер, роутер по ECMP отправляет его воркеру, воркер отвечает на адрес клиента (который не принадлежит подсети узлов), а значит, через роутер. Поэтому для обычных клиентов специальных правил не нужно.
Особый случай — клиент в той же подсети, что и узлы (другая машина в подсети узлов, не из кластера). Он оказывается в ситуации, о которой речь шла в части 3: запрос идёт через роутер, ответ — напрямую. Правило разворота на 192.168.20.0/24 покрывает и адреса LoadBalancer, потому что я сразу написал его на всю подсеть, и расширять его при появлении Traefik не пришлось.
А для самих узлов и подов это вообще не проблема: eBPF превращает соединение к адресу сервиса в соединение к поду раньше, чем пакет дойдёт до маршрутизации. Из любого пода кластера (в том числе на мастерах) внешнее имя сайта открывается как обычно.
Мелкие, но неприятные вещи
Гонка BGP/eBPF. При Local существует известная гонка (calico#11523): когда под появляется на узле, /32 может уйти в анонс раньше, чем eBPF запрограммирует карты. Окно короткое и случается только при перезапуске или выкатке пода Traefik. Знать о нём стоит, особенно если выкатываете Traefik в часы нагрузки.
Новый воркер получит трафик не сразу. Только когда на нём станет готов под Traefik. Это нормально и как раз то, что нам нужно.
При Local нет межузлового хопа, но он остаётся для других сервисов. Если появятся другие сервисы LoadBalancer или NodePort на Cluster, они продолжат использовать туннель eBPF.
В последней, восьмой части, ломаем узлы и смотрим, насколько всё это действительно отказоустойчиво, а также собираем шпаргалку для диагностики.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.